Repository: netbirdio/netbird

Issue:
relay client not connected
**Describe the problem**

netbird status gives:  reason: relay client not connected
I'm using my own selfhosted netbird server.

**To Reproduce**

Steps to reproduce the behavior:
1. Start traefik
2. Up netbird compose
3. Try to connect netbird server from cli on desktop
4. See error: [rel://netbird.domain.com:33080] is Unavailable, reason: relay client not connected
5. When I tail the "/var/log/netbird/client.log" file: I see: 
6. 2025-01-03T00:27:20+03:00 INFO [relay: rel://netbird.domain.com:33080] relay/client/client.go:171: connecting to relay server
2025-01-03T00:27:45+03:00 ERRO relay/client/dialer/ws/ws.go:36: failed to dial to Relay server 'ws://netbird.domain.com:33080': failed to WebSocket dial: failed to send handshake request: Get "http://netbird.domain.com:33080/relay": d.Dialer.DialContext: dial tcp 92.113.26.143:33080: connect: connection timed out
2025-01-03T00:27:50+03:00 ERRO client/internal/connect.go:249: failed to connect to any relay server: context deadline exceeded
2025-01-03T00:27:50+03:00 INFO relay/client/guard.go:68: try to pick up a new Relay server
2025-01-03T00:27:50+03:00 INFO relay/client/picker.go:72: try to connecting to relay server: rel://netbird.domain.com:33080
2025-01-03T00:27:50+03:00 INFO [relay: rel://netbird.domain.com:33080] relay/client/client.go:165: create new relay connection: local peerID: MfThRZtJfCAzzPvrLMfkjGkY1MNRCq15kejfET54RHw=, local peer hashedID: sha-9J7vtUU2mg/gV75vQ+mS1daMJJz0qj+6xnwGL5GhHbk=


**Expected behavior**

I hope relays' result should be 3/3 connected when I typed "netbird status -d"

**Are you using NetBird Cloud?**

Netbird Selfhosted

**NetBird version**

0.35.1

**NetBird status -dA output:**

Peers detail:
OS: linux/amd64
Daemon version: 0.35.1
CLI version: 0.35.1
Management: Connected to https://netbird.anon-paBa5.domain:443
Signal: Connected to https://netbird.anon-paBa5.domain:80
Relays:
  [stun:netbird.anon-paBa5.domain:3478] is Available
  [turn:netbird.anon-paBa5.domain:3478?transport=udp] is Available
  [rel://netbird.anon-paBa5.domain:33080] is Unavailable, reason: relay client not connected
Nameservers:
FQDN: orion.netbird.selfhosted
NetBird IP: 100.72.198.15/16
Interface type: Kernel
Quantum resistance: false
Routes: -
Networks: -
Peers count: 0/0 Connected

**Do you face any (non-mobile) client issues?**

.....

**Screenshots**
....

**Additional context**

....


Discussion:
@Onur-Cansever: You need to add this section to docker-compose.yml.tmpl.traefik too:
`
# Relay
  relay:
    image: netbirdio/relay:$NETBIRD_RELAY_TAG
    restart: unless-stopped
    environment:
    - NB_LOG_LEVEL=info
    - NB_LISTEN_ADDRESS=:$NETBIRD_RELAY_PORT
    - NB_EXPOSED_ADDRESS=$NETBIRD_RELAY_DOMAIN:$NETBIRD_RELAY_PORT
    # todo: change to a secure secret
    - NB_AUTH_SECRET=$NETBIRD_RELAY_AUTH_SECRET
    ports:
      - $NETBIRD_RELAY_PORT:$NETBIRD_RELAY_PORT
    logging:
      driver: "json-file"
      options:
        max-size: "500m"
        max-file: "2"
`
1- docker compose down under the artifacts directory
2- Edit your docker-compose.yml.tmpl.traefik , then overwirte it with cp command as "docker-compose.yml." Then just run ./configure.sh
3- go to artifacts dir, docker compose up,  then relay should be working.@nazarewk: Hello @Onur-Cansever,

We're currently reviewing our open issues and would like to verify if this problem still exists in the [latest NetBird version](https://github.com/netbirdio/netbird/releases).

Could you please confirm if the issue is still there?

We may close this issue temporarily if we don't hear back from you within **2 weeks**, but feel free to reopen it with updated information.

Thanks for your contribution to improving the project!
@Onur-Cansever: I checked the latest stable files, It seems fixed, thanks for your attention. 

Solution patch:
diff --git a/.github/ISSUE_TEMPLATE/bug-issue-report.md b/.github/ISSUE_TEMPLATE/bug-issue-report.md
index 3633cca4..df670db0 100644
--- a/.github/ISSUE_TEMPLATE/bug-issue-report.md
+++ b/.github/ISSUE_TEMPLATE/bug-issue-report.md
@@ -37,16 +37,21 @@ If yes, which one?
 
 **Debug output**
 
-To help us resolve the problem, please attach the following debug output
+To help us resolve the problem, please attach the following anonymized status output
 
   netbird status -dA
 
-As well as the file created by
+Create and upload a debug bundle, and share the returned file key:
+
+  netbird debug for 1m -AS -U
+
+*Uploaded files are automatically deleted after 30 days.*
+
+
+Alternatively, create the file only and attach it here manually:
 
   netbird debug for 1m -AS
 
-  
-We advise reviewing the anonymized output for any remaining personal information.
 
 **Screenshots**
 
@@ -57,8 +62,10 @@ If applicable, add screenshots to help explain your problem.
 Add any other context about the problem here.
 
 **Have you tried these troubleshooting steps?**
+- [ ] Reviewed [client troubleshooting](https://docs.netbird.io/how-to/troubleshooting-client) (if applicable)
 - [ ] Checked for newer NetBird versions
 - [ ] Searched for similar issues on GitHub (including closed ones)
 - [ ] Restarted the NetBird client
 - [ ] Disabled other VPN software
 - [ ] Checked firewall settings
+
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index c4bd3140..f41bc9e8 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -13,3 +13,5 @@
 - [ ] It is a refactor
 - [ ] Created tests that fail without the change (if possible)
 - [ ] Extended the README / documentation, if necessary
+
+> By submitting this pull request, you confirm that you have read and agree to the terms of the [Contributor License Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md).
diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml
index e727aa4e..cbce3e6e 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]
@@ -164,6 +223,10 @@ jobs:
       - name: Checkout code
         uses: actions/checkout@v4
 
+      - name: Install dependencies
+        if: steps.cache.outputs.cache-hit != 'true'
+        run: sudo apt update && sudo apt install -y gcc-multilib g++-multilib libc6-dev-i386
+
       - name: Get Go environment
         run: |
           echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
@@ -179,13 +242,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
 
@@ -217,6 +273,10 @@ jobs:
       - name: Checkout code
         uses: actions/checkout@v4
 
+      - name: Install dependencies
+        if: steps.cache.outputs.cache-hit != 'true'
+        run: sudo apt update && sudo apt install -y gcc-multilib g++-multilib libc6-dev-i386
+
       - name: Get Go environment
         run: |
           echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
@@ -232,13 +292,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 +339,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
 
@@ -354,13 +400,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
 
@@ -385,7 +424,7 @@ jobs:
           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)"
@@ -449,13 +488,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
 
@@ -520,13 +552,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
 
@@ -541,85 +566,3 @@ jobs:
           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/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml
index bdd508e9..7e6583cc 100644
--- a/.github/workflows/golangci-lint.yml
+++ b/.github/workflows/golangci-lint.yml
@@ -21,7 +21,6 @@ jobs:
         with:
           ignore_words_list: erro,clienta,hastable,iif,groupd,testin,groupe
           skip: go.mod,go.sum
-          only_warn: 1
   golangci:
     strategy:
       fail-fast: false
diff --git a/.github/workflows/test-infrastructure-files.yml b/.github/workflows/test-infrastructure-files.yml
index 174b7d20..8c2d21c8 100644
--- a/.github/workflows/test-infrastructure-files.yml
+++ b/.github/workflows/test-infrastructure-files.yml
@@ -179,6 +179,7 @@ jobs:
           grep -A 7 Relay management.json | grep "rel://$CI_NETBIRD_DOMAIN:33445"
           grep -A 7 Relay management.json | egrep '"Secret": ".+"'
           grep DisablePromptLogin management.json | grep 'true'
+          grep LoginFlag management.json | grep 0
 
       - name: Install modules
         run: go mod tidy
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index d6479763..112659d1 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/client/Dockerfile b/client/Dockerfile
index 35c1d04c..16b2916c 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/anonymize/anonymize.go b/client/anonymize/anonymize.go
index 2fc9d49d..89e65330 100644
--- a/client/anonymize/anonymize.go
+++ b/client/anonymize/anonymize.go
@@ -69,6 +69,22 @@ func (a *Anonymizer) AnonymizeIP(ip netip.Addr) netip.Addr {
 	return a.ipAnonymizer[ip]
 }
 
+func (a *Anonymizer) AnonymizeUDPAddr(addr net.UDPAddr) net.UDPAddr {
+	// Convert IP to netip.Addr
+	ip, ok := netip.AddrFromSlice(addr.IP)
+	if !ok {
+		return addr
+	}
+
+	anonIP := a.AnonymizeIP(ip)
+
+	return net.UDPAddr{
+		IP:   anonIP.AsSlice(),
+		Port: addr.Port,
+		Zone: addr.Zone,
+	}
+}
+
 // isInAnonymizedRange checks if an IP is within the range of already assigned anonymized IPs
 func (a *Anonymizer) isInAnonymizedRange(ip netip.Addr) bool {
 	if ip.Is4() && ip.Compare(a.startAnonIPv4) >= 0 && ip.Compare(a.currentAnonIPv4) <= 0 {
diff --git a/client/cmd/debug.go b/client/cmd/debug.go
index d2e5bdd7..385bd95f 100644
--- a/client/cmd/debug.go
+++ b/client/cmd/debug.go
@@ -87,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
 }
@@ -211,23 +222,19 @@ 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())
 	}
 
-	// Disable network map persistence after creating the debug bundle
-	if _, err := client.SetNetworkMapPersistence(cmd.Context(), &proto.SetNetworkMapPersistenceRequest{
-		Enabled: false,
-	}); err != nil {
-		return fmt.Errorf("failed to disable network map persistence: %v", status.Convert(err).Message())
-	}
-
 	if stateWasDown {
 		if _, err := client.Down(cmd.Context(), &proto.DownRequest{}); err != nil {
 			return fmt.Errorf("failed to down: %v", status.Convert(err).Message())
@@ -242,7 +249,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
 }
diff --git a/client/cmd/login.go b/client/cmd/login.go
index c86d6c63..14abcd03 100644
--- a/client/cmd/login.go
+++ b/client/cmd/login.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 	"os"
+	"runtime"
 	"strings"
 	"time"
 
@@ -55,6 +56,9 @@ var loginCmd = &cobra.Command{
 				return err
 			}
 
+			// update host's static platform and system information
+			system.UpdateStaticInfo()
+
 			ic := internal.ConfigInput{
 				ManagementURL: managementURL,
 				AdminURL:      adminURL,
@@ -95,11 +99,11 @@ var loginCmd = &cobra.Command{
 		}
 
 		loginRequest := proto.LoginRequest{
-			SetupKey:             providedSetupKey,
-			ManagementUrl:        managementURL,
-			IsLinuxDesktopClient: isLinuxRunningDesktop(),
-			Hostname:             hostName,
-			DnsLabels:            dnsLabelsReq,
+			SetupKey:            providedSetupKey,
+			ManagementUrl:       managementURL,
+			IsUnixDesktopClient: isUnixRunningDesktop(),
+			Hostname:            hostName,
+			DnsLabels:           dnsLabelsReq,
 		}
 
 		if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) {
@@ -192,7 +196,7 @@ func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *internal.C
 }
 
 func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *internal.Config) (*auth.TokenInfo, error) {
-	oAuthFlow, err := auth.NewOAuthFlow(ctx, config, isLinuxRunningDesktop())
+	oAuthFlow, err := auth.NewOAuthFlow(ctx, config, isUnixRunningDesktop())
 	if err != nil {
 		return nil, err
 	}
@@ -240,7 +244,10 @@ func openURL(cmd *cobra.Command, verificationURIComplete, userCode string, noBro
 	}
 }
 
-// isLinuxRunningDesktop checks if a Linux OS is running desktop environment
-func isLinuxRunningDesktop() bool {
+// isUnixRunningDesktop checks if a Linux OS is running desktop environment
+func isUnixRunningDesktop() bool {
+	if runtime.GOOS != "linux" && runtime.GOOS != "freebsd" {
+		return false
+	}
 	return os.Getenv("DESKTOP_SESSION") != "" || os.Getenv("XDG_CURRENT_DESKTOP") != ""
 }
diff --git a/client/cmd/root.go b/client/cmd/root.go
index baf444b9..16e445f4 100644
--- a/client/cmd/root.go
+++ b/client/cmd/root.go
@@ -22,23 +22,26 @@ import (
 	"google.golang.org/grpc/credentials/insecure"
 
 	"github.com/netbirdio/netbird/client/internal"
+	"github.com/netbirdio/netbird/upload-server/types"
 )
 
 const (
-	externalIPMapFlag       = "external-ip-map"
-	dnsResolverAddress      = "dns-resolver-address"
-	enableRosenpassFlag     = "enable-rosenpass"
-	rosenpassPermissiveFlag = "rosenpass-permissive"
-	preSharedKeyFlag        = "preshared-key"
-	interfaceNameFlag       = "interface-name"
-	wireguardPortFlag       = "wireguard-port"
-	networkMonitorFlag      = "network-monitor"
-	disableAutoConnectFlag  = "disable-auto-connect"
-	serverSSHAllowedFlag    = "allow-server-ssh"
-	extraIFaceBlackListFlag = "extra-iface-blacklist"
-	dnsRouteIntervalFlag    = "dns-router-interval"
-	systemInfoFlag          = "system-info"
-	blockLANAccessFlag      = "block-lan-access"
+	externalIPMapFlag        = "external-ip-map"
+	dnsResolverAddress       = "dns-resolver-address"
+	enableRosenpassFlag      = "enable-rosenpass"
+	rosenpassPermissiveFlag  = "rosenpass-permissive"
+	preSharedKeyFlag         = "preshared-key"
+	interfaceNameFlag        = "interface-name"
+	wireguardPortFlag        = "wireguard-port"
+	networkMonitorFlag       = "network-monitor"
+	disableAutoConnectFlag   = "disable-auto-connect"
+	serverSSHAllowedFlag     = "allow-server-ssh"
+	extraIFaceBlackListFlag  = "extra-iface-blacklist"
+	dnsRouteIntervalFlag     = "dns-router-interval"
+	systemInfoFlag           = "system-info"
+	enableLazyConnectionFlag = "enable-lazy-connection"
+	uploadBundle             = "upload-bundle"
+	uploadBundleURL          = "upload-bundle-url"
 )
 
 var (
@@ -74,7 +77,9 @@ var (
 	anonymizeFlag           bool
 	debugSystemInfoFlag     bool
 	dnsRouteInterval        time.Duration
-	blockLANAccess          bool
+	debugUploadBundle       bool
+	debugUploadBundleURL    string
+	lazyConnEnabled         bool
 
 	rootCmd = &cobra.Command{
 		Use:          "netbird",
@@ -179,8 +184,11 @@ func init() {
 	upCmd.PersistentFlags().BoolVar(&rosenpassPermissive, rosenpassPermissiveFlag, false, "[Experimental] Enable Rosenpass in permissive mode to allow this peer to accept WireGuard connections without requiring Rosenpass functionality from peers that do not have Rosenpass enabled.")
 	upCmd.PersistentFlags().BoolVar(&serverSSHAllowed, serverSSHAllowedFlag, false, "Allow SSH server on peer. If enabled, the SSH server will be permitted")
 	upCmd.PersistentFlags().BoolVar(&autoConnectDisabled, disableAutoConnectFlag, false, "Disables auto-connect feature. If enabled, then the client won't connect automatically when the service starts.")
+	upCmd.PersistentFlags().BoolVar(&lazyConnEnabled, enableLazyConnectionFlag, false, "[Experimental] Enable the lazy connection feature. If enabled, the client will establish connections on-demand.")
 
 	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, types.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.go b/client/cmd/service.go
index 3560088a..156e67d6 100644
--- a/client/cmd/service.go
+++ b/client/cmd/service.go
@@ -2,6 +2,7 @@ package cmd
 
 import (
 	"context"
+	"runtime"
 	"sync"
 
 	"github.com/kardianos/service"
@@ -27,12 +28,19 @@ func newProgram(ctx context.Context, cancel context.CancelFunc) *program {
 }
 
 func newSVCConfig() *service.Config {
-	return &service.Config{
+	config := &service.Config{
 		Name:        serviceName,
 		DisplayName: "Netbird",
-		Description: "A WireGuard-based mesh network that connects your devices into a single private network.",
+		Description: "Netbird mesh network client",
 		Option:      make(service.KeyValue),
+		EnvVars:     make(map[string]string),
 	}
+
+	if runtime.GOOS == "linux" {
+		config.EnvVars["SYSTEMD_UNIT"] = serviceName
+	}
+
+	return config
 }
 
 func newSVC(prg *program, conf *service.Config) (service.Service, error) {
diff --git a/client/cmd/service_controller.go b/client/cmd/service_controller.go
index 0ddf6c4c..5e3c63e5 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()
 
diff --git a/client/cmd/service_installer.go b/client/cmd/service_installer.go
index 99a4821b..c1d6308c 100644
--- a/client/cmd/service_installer.go
+++ b/client/cmd/service_installer.go
@@ -39,7 +39,7 @@ var installCmd = &cobra.Command{
 			svcConfig.Arguments = append(svcConfig.Arguments, "--management-url", managementURL)
 		}
 
-		if logFile != "console" {
+		if logFile != "" {
 			svcConfig.Arguments = append(svcConfig.Arguments, "--log-file", logFile)
 		}
 
diff --git a/client/cmd/status.go b/client/cmd/status.go
index 0ddba8b2..e466f73a 100644
--- a/client/cmd/status.go
+++ b/client/cmd/status.go
@@ -44,7 +44,7 @@ func init() {
 	statusCmd.MarkFlagsMutuallyExclusive("detail", "json", "yaml", "ipv4")
 	statusCmd.PersistentFlags().StringSliceVar(&ipsFilter, "filter-by-ips", []string{}, "filters the detailed output by a list of one or more IPs, e.g., --filter-by-ips 100.64.0.100,100.64.0.200")
 	statusCmd.PersistentFlags().StringSliceVar(&prefixNamesFilter, "filter-by-names", []string{}, "filters the detailed output by a list of one or more peer FQDN or hostnames, e.g., --filter-by-names peer-a,peer-b.netbird.cloud")
-	statusCmd.PersistentFlags().StringVar(&statusFilter, "filter-by-status", "", "filters the detailed output by connection status(connected|disconnected), e.g., --filter-by-status connected")
+	statusCmd.PersistentFlags().StringVar(&statusFilter, "filter-by-status", "", "filters the detailed output by connection status(idle|connecting|connected), e.g., --filter-by-status connected")
 }
 
 func statusFunc(cmd *cobra.Command, args []string) error {
@@ -127,12 +127,12 @@ func getStatus(ctx context.Context) (*proto.StatusResponse, error) {
 
 func parseFilters() error {
 	switch strings.ToLower(statusFilter) {
-	case "", "disconnected", "connected":
+	case "", "idle", "connecting", "connected":
 		if strings.ToLower(statusFilter) != "" {
 			enableDetailFlagWhenFilterFlag()
 		}
 	default:
-		return fmt.Errorf("wrong status filter, should be one of connected|disconnected, got: %s", statusFilter)
+		return fmt.Errorf("wrong status filter, should be one of connected|connecting|idle, got: %s", statusFilter)
 	}
 
 	if len(ipsFilter) > 0 {
diff --git a/client/cmd/system.go b/client/cmd/system.go
index f628867a..83ce8d21 100644
--- a/client/cmd/system.go
+++ b/client/cmd/system.go
@@ -6,6 +6,8 @@ const (
 	disableServerRoutesFlag = "disable-server-routes"
 	disableDNSFlag          = "disable-dns"
 	disableFirewallFlag     = "disable-firewall"
+	blockLANAccessFlag      = "block-lan-access"
+	blockInboundFlag        = "block-inbound"
 )
 
 var (
@@ -13,6 +15,8 @@ var (
 	disableServerRoutes bool
 	disableDNS          bool
 	disableFirewall     bool
+	blockLANAccess      bool
+	blockInbound        bool
 )
 
 func init() {
@@ -28,4 +32,11 @@ func init() {
 
 	upCmd.PersistentFlags().BoolVar(&disableFirewall, disableFirewallFlag, false,
 		"Disable firewall configuration. If enabled, the client won't modify firewall rules.")
+
+	upCmd.PersistentFlags().BoolVar(&blockLANAccess, blockLANAccessFlag, false,
+		"Block access to local networks (LAN) when using this peer as a router or exit node")
+
+	upCmd.PersistentFlags().BoolVar(&blockInbound, blockInboundFlag, false,
+		"Block inbound connections. If enabled, the client will not allow any inbound connections to the local machine nor routed networks.\n"+
+		"This overrides any policies received from the management service.")
 }
diff --git a/client/cmd/trace.go b/client/cmd/trace.go
index b2ff1f1b..abb73b64 100644
--- a/client/cmd/trace.go
+++ b/client/cmd/trace.go
@@ -17,7 +17,7 @@ var traceCmd = &cobra.Command{
 	Example: `
   netbird debug trace in 192.168.1.10 10.10.0.2 -p tcp --sport 12345 --dport 443 --syn --ack
   netbird debug trace out 10.10.0.1 8.8.8.8 -p udp  --dport 53
-  netbird debug trace in 10.10.0.2 10.10.0.1 -p icmp --type 8 --code 0
+  netbird debug trace in 10.10.0.2 10.10.0.1 -p icmp --icmp-type 8 --icmp-code 0
   netbird debug trace in 100.64.1.1 self -p tcp --dport 80`,
 	Args: cobra.ExactArgs(3),
 	RunE: tracePacket,
diff --git a/client/cmd/up.go b/client/cmd/up.go
index bfe41628..b9781c0d 100644
--- a/client/cmd/up.go
+++ b/client/cmd/up.go
@@ -55,12 +55,11 @@ func init() {
 	upCmd.PersistentFlags().StringVar(&interfaceName, interfaceNameFlag, iface.WgInterfaceDefault, "Wireguard interface name")
 	upCmd.PersistentFlags().Uint16Var(&wireguardPort, wireguardPortFlag, iface.DefaultWgPort, "Wireguard interface listening port")
 	upCmd.PersistentFlags().BoolVarP(&networkMonitor, networkMonitorFlag, "N", networkMonitor,
-		`Manage network monitoring. Defaults to true on Windows and macOS, false on Linux. `+
+		`Manage network monitoring. Defaults to true on Windows and macOS, false on Linux and FreeBSD. `+
 			`E.g. --network-monitor=false to disable or --network-monitor=true to enable.`,
 	)
 	upCmd.PersistentFlags().StringSliceVar(&extraIFaceBlackList, extraIFaceBlackListFlag, nil, "Extra list of default interfaces to ignore for listening")
 	upCmd.PersistentFlags().DurationVar(&dnsRouteInterval, dnsRouteIntervalFlag, time.Minute, "DNS route update interval")
-	upCmd.PersistentFlags().BoolVar(&blockLANAccess, blockLANAccessFlag, false, "Block access to local networks (LAN) when using this peer as a router or exit node")
 
 	upCmd.PersistentFlags().StringSliceVar(&dnsLabels, dnsLabelsFlag, nil,
 		`Sets DNS labels`+
@@ -119,6 +118,124 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error {
 		return err
 	}
 
+	ic, err := setupConfig(customDNSAddressConverted, cmd)
+	if err != nil {
+		return fmt.Errorf("setup config: %v", err)
+	}
+
+	providedSetupKey, err := getSetupKey()
+	if err != nil {
+		return err
+	}
+
+	config, err := internal.UpdateOrCreateConfig(*ic)
+	if err != nil {
+		return fmt.Errorf("get config file: %v", err)
+	}
+
+	config, _ = internal.UpdateOldManagementURL(ctx, config, configPath)
+
+	err = foregroundLogin(ctx, cmd, config, providedSetupKey)
+	if err != nil {
+		return fmt.Errorf("foreground login failed: %v", err)
+	}
+
+	var cancel context.CancelFunc
+	ctx, cancel = context.WithCancel(ctx)
+	SetupCloseHandler(ctx, cancel)
+
+	r := peer.NewRecorder(config.ManagementURL.String())
+	r.GetFullStatus()
+
+	connectClient := internal.NewConnectClient(ctx, config, r)
+	SetupDebugHandler(ctx, config, r, connectClient, "")
+
+	return connectClient.Run(nil)
+}
+
+func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
+	customDNSAddressConverted, err := parseCustomDNSAddress(cmd.Flag(dnsResolverAddress).Changed)
+	if err != nil {
+		return err
+	}
+
+	conn, err := DialClientGRPCServer(ctx, daemonAddr)
+	if err != nil {
+		return fmt.Errorf("failed to connect to daemon error: %v\n"+
+			"If the daemon is not running please run: "+
+			"\nnetbird service install \nnetbird service start\n", err)
+	}
+	defer func() {
+		err := conn.Close()
+		if err != nil {
+			log.Warnf("failed closing daemon gRPC client connection %v", err)
+			return
+		}
+	}()
+
+	client := proto.NewDaemonServiceClient(conn)
+
+	status, err := client.Status(ctx, &proto.StatusRequest{})
+	if err != nil {
+		return fmt.Errorf("unable to get daemon status: %v", err)
+	}
+
+	if status.Status == string(internal.StatusConnected) {
+		cmd.Println("Already connected")
+		return nil
+	}
+
+	providedSetupKey, err := getSetupKey()
+	if err != nil {
+		return fmt.Errorf("get setup key: %v", err)
+	}
+
+	loginRequest, err := setupLoginRequest(providedSetupKey, customDNSAddressConverted, cmd)
+	if err != nil {
+		return fmt.Errorf("setup login request: %v", err)
+	}
+
+	var loginErr error
+	var loginResp *proto.LoginResponse
+
+	err = WithBackOff(func() error {
+		var backOffErr error
+		loginResp, backOffErr = client.Login(ctx, loginRequest)
+		if s, ok := gstatus.FromError(backOffErr); ok && (s.Code() == codes.InvalidArgument ||
+			s.Code() == codes.PermissionDenied ||
+			s.Code() == codes.NotFound ||
+			s.Code() == codes.Unimplemented) {
+			loginErr = backOffErr
+			return nil
+		}
+		return backOffErr
+	})
+	if err != nil {
+		return fmt.Errorf("login backoff cycle failed: %v", err)
+	}
+
+	if loginErr != nil {
+		return fmt.Errorf("login failed: %v", loginErr)
+	}
+
+	if loginResp.NeedsSSOLogin {
+
+		openURL(cmd, loginResp.VerificationURIComplete, loginResp.UserCode, noBrowser)
+
+		_, err = client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode, Hostname: hostName})
+		if err != nil {
+			return fmt.Errorf("waiting sso login failed with: %v", err)
+		}
+	}
+
+	if _, err := client.Up(ctx, &proto.UpRequest{}); err != nil {
+		return fmt.Errorf("call service up method: %v", err)
+	}
+	cmd.Println("Connected")
+	return nil
+}
+
+func setupConfig(customDNSAddressConverted []byte, cmd *cobra.Command) (*internal.ConfigInput, error) {
 	ic := internal.ConfigInput{
 		ManagementURL:       managementURL,
 		AdminURL:            adminURL,
@@ -143,7 +260,7 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error {
 
 	if cmd.Flag(interfaceNameFlag).Changed {
 		if err := parseInterfaceName(interfaceName); err != nil {
-			return err
+			return nil, err
 		}
 		ic.InterfaceName = &interfaceName
 	}
@@ -194,85 +311,29 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error {
 		ic.BlockLANAccess = &blockLANAccess
 	}
 
-	providedSetupKey, err := getSetupKey()
-	if err != nil {
-		return err
-	}
-
-	config, err := internal.UpdateOrCreateConfig(ic)
-	if err != nil {
-		return fmt.Errorf("get config file: %v", err)
+	if cmd.Flag(blockInboundFlag).Changed {
+		ic.BlockInbound = &blockInbound
 	}
 
-	config, _ = internal.UpdateOldManagementURL(ctx, config, configPath)
-
-	err = foregroundLogin(ctx, cmd, config, providedSetupKey)
-	if err != nil {
-		return fmt.Errorf("foreground login failed: %v", err)
+	if cmd.Flag(enableLazyConnectionFlag).Changed {
+		ic.LazyConnectionEnabled = &lazyConnEnabled
 	}
-
-	var cancel context.CancelFunc
-	ctx, cancel = context.WithCancel(ctx)
-	SetupCloseHandler(ctx, cancel)
-
-	r := peer.NewRecorder(config.ManagementURL.String())
-	r.GetFullStatus()
-
-	connectClient := internal.NewConnectClient(ctx, config, r)
-	SetupDebugHandler(ctx, config, r, connectClient, "")
-
-	return connectClient.Run(nil)
+	return &ic, nil
 }
 
-func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
-	customDNSAddressConverted, err := parseCustomDNSAddress(cmd.Flag(dnsResolverAddress).Changed)
-	if err != nil {
-		return err
-	}
-
-	conn, err := DialClientGRPCServer(ctx, daemonAddr)
-	if err != nil {
-		return fmt.Errorf("failed to connect to daemon error: %v\n"+
-			"If the daemon is not running please run: "+
-			"\nnetbird service install \nnetbird service start\n", err)
-	}
-	defer func() {
-		err := conn.Close()
-		if err != nil {
-			log.Warnf("failed closing daemon gRPC client connection %v", err)
-			return
-		}
-	}()
-
-	client := proto.NewDaemonServiceClient(conn)
-
-	status, err := client.Status(ctx, &proto.StatusRequest{})
-	if err != nil {
-		return fmt.Errorf("unable to get daemon status: %v", err)
-	}
-
-	if status.Status == string(internal.StatusConnected) {
-		cmd.Println("Already connected")
-		return nil
-	}
-
-	providedSetupKey, err := getSetupKey()
-	if err != nil {
-		return err
-	}
-
+func setupLoginRequest(providedSetupKey string, customDNSAddressConverted []byte, cmd *cobra.Command) (*proto.LoginRequest, error) {
 	loginRequest := proto.LoginRequest{
-		SetupKey:             providedSetupKey,
-		ManagementUrl:        managementURL,
-		AdminURL:             adminURL,
-		NatExternalIPs:       natExternalIPs,
-		CleanNATExternalIPs:  natExternalIPs != nil && len(natExternalIPs) == 0,
-		CustomDNSAddress:     customDNSAddressConverted,
-		IsLinuxDesktopClient: isLinuxRunningDesktop(),
-		Hostname:             hostName,
-		ExtraIFaceBlacklist:  extraIFaceBlackList,
-		DnsLabels:            dnsLabels,
-		CleanDNSLabels:       dnsLabels != nil && len(dnsLabels) == 0,
+		SetupKey:            providedSetupKey,
+		ManagementUrl:       managementURL,
+		AdminURL:            adminURL,
+		NatExternalIPs:      natExternalIPs,
+		CleanNATExternalIPs: natExternalIPs != nil && len(natExternalIPs) == 0,
+		CustomDNSAddress:    customDNSAddressConverted,
+		IsUnixDesktopClient: isUnixRunningDesktop(),
+		Hostname:            hostName,
+		ExtraIFaceBlacklist: extraIFaceBlackList,
+		DnsLabels:           dnsLabels,
+		CleanDNSLabels:      dnsLabels != nil && len(dnsLabels) == 0,
 	}
 
 	if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) {
@@ -297,7 +358,7 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
 
 	if cmd.Flag(interfaceNameFlag).Changed {
 		if err := parseInterfaceName(interfaceName); err != nil {
-			return err
+			return nil, err
 		}
 		loginRequest.InterfaceName = &interfaceName
 	}
@@ -332,45 +393,14 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
 		loginRequest.BlockLanAccess = &blockLANAccess
 	}
 
-	var loginErr error
-
-	var loginResp *proto.LoginResponse
-
-	err = WithBackOff(func() error {
-		var backOffErr error
-		loginResp, backOffErr = client.Login(ctx, &loginRequest)
-		if s, ok := gstatus.FromError(backOffErr); ok && (s.Code() == codes.InvalidArgument ||
-			s.Code() == codes.PermissionDenied ||
-			s.Code() == codes.NotFound ||
-			s.Code() == codes.Unimplemented) {
-			loginErr = backOffErr
-			return nil
-		}
-		return backOffErr
-	})
-	if err != nil {
-		return fmt.Errorf("login backoff cycle failed: %v", err)
-	}
-
-	if loginErr != nil {
-		return fmt.Errorf("login failed: %v", loginErr)
-	}
-
-	if loginResp.NeedsSSOLogin {
-
-		openURL(cmd, loginResp.VerificationURIComplete, loginResp.UserCode, noBrowser)
-
-		_, err = client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode, Hostname: hostName})
-		if err != nil {
-			return fmt.Errorf("waiting sso login failed with: %v", err)
-		}
+	if cmd.Flag(blockInboundFlag).Changed {
+		loginRequest.BlockInbound = &blockInbound
 	}
 
-	if _, err := client.Up(ctx, &proto.UpRequest{}); err != nil {
-		return fmt.Errorf("call service up method: %v", err)
+	if cmd.Flag(enableLazyConnectionFlag).Changed {
+		loginRequest.LazyConnectionEnabled = &lazyConnEnabled
 	}
-	cmd.Println("Connected")
-	return nil
+	return &loginRequest, nil
 }
 
 func validateNATExternalIPs(list []string) error {
diff --git a/client/firewall/iptables/manager_linux.go b/client/firewall/iptables/manager_linux.go
index 652ab1b3..81f7a912 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)
@@ -148,6 +147,10 @@ func (m *Manager) IsServerRouteSupported() bool {
 	return true
 }
 
+func (m *Manager) IsStateful() bool {
+	return true
+}
+
 func (m *Manager) AddNatRule(pair firewall.RouterPair) error {
 	m.mutex.Lock()
 	defer m.mutex.Unlock()
@@ -199,7 +202,7 @@ func (m *Manager) AllowNetbird() error {
 	_, err := m.AddPeerFiltering(
 		nil,
 		net.IP{0, 0, 0, 0},
-		"all",
+		firewall.ProtocolALL,
 		nil,
 		nil,
 		firewall.ActionAccept,
@@ -220,10 +223,16 @@ func (m *Manager) SetLogLevel(log.Level) {
 }
 
 func (m *Manager) EnableRouting() error {
+	if err := m.router.ipFwdState.RequestForwarding(); err != nil {
+		return fmt.Errorf("enable IP forwarding: %w", err)
+	}
 	return nil
 }
 
 func (m *Manager) DisableRouting() error {
+	if err := m.router.ipFwdState.ReleaseForwarding(); err != nil {
+		return fmt.Errorf("disable IP forwarding: %w", err)
+	}
 	return nil
 }
 
@@ -243,6 +252,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 869b0b35..1e44c7a4 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 {
+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 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 {
-			return rule[i+3]
+			sets = append(sets, rule[i+3])
 		}
 	}
-	return ""
+	return sets
 }
 
 func (r *router) createIpSet(setName string, sources []netip.Prefix) error {
@@ -231,15 +241,13 @@ 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
 }
 
 // AddNatRule inserts an iptables rule pair into the nat chain
 func (r *router) AddNatRule(pair firewall.RouterPair) error {
-	if err := r.ipFwdState.RequestForwarding(); err != nil {
-		return err
-	}
-
 	if r.legacyManagement {
 		log.Warnf("This peer is connected to a NetBird Management service with an older version. Allowing all traffic for %s", pair.Destination)
 		if err := r.addLegacyRouteRule(pair); err != nil {
@@ -266,16 +274,14 @@ func (r *router) AddNatRule(pair firewall.RouterPair) error {
 
 // RemoveNatRule removes an iptables rule pair from forwarding and nat chains
 func (r *router) RemoveNatRule(pair firewall.RouterPair) error {
-	if err := r.ipFwdState.ReleaseForwarding(); err != nil {
-		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 +319,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 +607,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 +644,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 +813,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 +837,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/manager/firewall.go b/client/firewall/manager/firewall.go
index 1d71051e..3b316482 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 "<invalid network>"
+}
+
+// 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
@@ -80,13 +116,14 @@ type Manager interface {
 	// IsServerRouteSupported returns true if the firewall supports server side routing operations
 	IsServerRouteSupported() bool
 
+	IsStateful() bool
+
 	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 +156,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 +193,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/routerpair.go b/client/firewall/manager/routerpair.go
index 8c94b7dd..079c051d 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 00000000..4c88f6ea
--- /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 a5809471..560f224f 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)
@@ -171,6 +170,10 @@ func (m *Manager) IsServerRouteSupported() bool {
 	return true
 }
 
+func (m *Manager) IsStateful() bool {
+	return true
+}
+
 func (m *Manager) AddNatRule(pair firewall.RouterPair) error {
 	m.mutex.Lock()
 	defer m.mutex.Unlock()
@@ -242,7 +245,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()
@@ -325,10 +328,16 @@ func (m *Manager) SetLogLevel(log.Level) {
 }
 
 func (m *Manager) EnableRouting() error {
+	if err := m.router.ipFwdState.RequestForwarding(); err != nil {
+		return fmt.Errorf("enable IP forwarding: %w", err)
+	}
 	return nil
 }
 
 func (m *Manager) DisableRouting() error {
+	if err := m.router.ipFwdState.ReleaseForwarding(); err != nil {
+		return fmt.Errorf("disable IP forwarding: %w", err)
+	}
 	return nil
 }
 
@@ -359,6 +368,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/router_linux.go b/client/firewall/nftables/router_linux.go
index aff86dd9..f8fed4d8 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 {
+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 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 {
-			return lookup.SetName
+			sets = append(sets, lookup.SetName)
 		}
 	}
-	return ""
+	return sets
 }
 
 func (r *router) deleteNftRule(rule *nftables.Rule, ruleKey string) error {
@@ -560,10 +573,6 @@ func (r *router) deleteNftRule(rule *nftables.Rule, ruleKey string) error {
 
 // AddNatRule appends a nftables rule pair to the nat chain
 func (r *router) AddNatRule(pair firewall.RouterPair) error {
-	if err := r.ipFwdState.RequestForwarding(); err != nil {
-		return err
-	}
-
 	if err := r.refreshRulesMap(); err != nil {
 		return fmt.Errorf(refreshRulesMapError, err)
 	}
@@ -586,7 +595,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 +604,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 +629,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 +662,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 +747,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 +764,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 +778,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 +793,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
@@ -974,20 +1002,18 @@ func (r *router) removeAcceptForwardRulesIptables(ipt *iptables.IPTables) error
 
 // RemoveNatRule removes the prerouting mark rule
 func (r *router) RemoveNatRule(pair firewall.RouterPair) error {
-	if err := r.ipFwdState.ReleaseForwarding(); err != nil {
-		log.Errorf("%v", err)
-	}
-
 	if err := r.refreshRulesMap(); err != nil {
 		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 +1021,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 +1032,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 +1056,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 +1330,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 +1485,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/uspfilter/allow_netbird.go b/client/firewall/uspfilter/allow_netbird.go
index 5fe698aa..ce04c82c 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 f63792fe..f261c472 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 0dff3acc..42a3e080 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,14 +32,16 @@ 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
 	ctx          context.Context
 	cancel       context.CancelFunc
-	ip           net.IP
+	ip           tcpip.Address
 	netstack     bool
 }
 
@@ -66,12 +71,11 @@ func New(iface common.IFaceMapper, logger *nblog.Logger, flowLogger nftypes.Flow
 		return nil, fmt.Errorf("failed to create NIC: %v", err)
 	}
 
-	ones, _ := iface.Address().Network.Mask.Size()
 	protoAddr := tcpip.ProtocolAddress{
 		Protocol: ipv4.ProtocolNumber,
 		AddressWithPrefix: tcpip.AddressWithPrefix{
-			Address:   tcpip.AddrFromSlice(iface.Address().IP.To4()),
-			PrefixLen: ones,
+			Address:   tcpip.AddrFromSlice(iface.Address().IP.AsSlice()),
+			PrefixLen: iface.Address().Network.Bits(),
 		},
 	}
 
@@ -111,7 +115,7 @@ func New(iface common.IFaceMapper, logger *nblog.Logger, flowLogger nftypes.Flow
 		ctx:          ctx,
 		cancel:       cancel,
 		netstack:     netstack,
-		ip:           iface.Address().IP,
+		ip:           tcpip.AddrFromSlice(iface.Address().IP.AsSlice()),
 	}
 
 	receiveWindow := defaultReceiveWindow
@@ -162,8 +166,39 @@ func (f *Forwarder) Stop() {
 }
 
 func (f *Forwarder) determineDialAddr(addr tcpip.Address) net.IP {
-	if f.netstack && f.ip.Equal(addr.AsSlice()) {
+	if f.netstack && f.ip.Equal(addr) {
 		return net.IPv4(127, 0, 0, 1)
 	}
 	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 a21ec2c8..08d77ed0 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 71cd457e..64e54e29 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 {
+
+	ctx, cancel := context.WithCancel(f.ctx)
+	defer cancel()
+
+	go func() {
+		<-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 {
+		if err := outConn.Close(); err != nil && !isClosedError(err) {
 			f.logger.Debug("forwarder: outConn close error: %v", err)
 		}
-		ep.Close()
 
-		f.sendTCPEvent(nftypes.TypeEnd, flowID, id, ep)
+		ep.Close()
 	}()
 
-	// Create context for managing the proxy goroutines
-	ctx, cancel := context.WithCancel(f.ctx)
-	defer cancel()
+	var wg sync.WaitGroup
+	wg.Add(2)
 
-	errChan := make(chan error, 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() {
-		_, err := io.Copy(outConn, inConn)
-		errChan <- err
+		bytesFromInToOut, errInToOut = io.Copy(outConn, inConn)
+		cancel()
+		wg.Done()
 	}()
 
 	go func() {
-		_, err := io.Copy(inConn, outConn)
-		errChan <- err
+
+		bytesFromOutToIn, errOutToIn = io.Copy(inConn, outConn)
+		cancel()
+		wg.Done()
 	}()
 
-	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)
+	wg.Wait()
+
+	if errInToOut != nil {
+		if !isClosedError(errInToOut) {
+			f.logger.Error("proxyTCP: copy error (in -> out) for %s: %v", epID(id), errInToOut)
+		}
+	}
+	if errOutToIn != nil {
+		if !isClosedError(errOutToIn) {
+			f.logger.Error("proxyTCP: copy error (out -> in) for %s: %v", epID(id), errOutToIn)
 		}
-		f.logger.Trace("forwarder: tearing down TCP connection %v", epID(id))
-		return
 	}
+
+	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 7ce85e2b..f237a313 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) for %s: %v", epID(id), outboundErr)
+	}
+	if inboundErr != nil && !isClosedError(inboundErr) {
+		f.logger.Error("proxyUDP: copy error (inbound->outbound) for %s: %v", epID(id), 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/localip.go b/client/firewall/uspfilter/localip.go
index f093f342..7f6b52c7 100644
--- a/client/firewall/uspfilter/localip.go
+++ b/client/firewall/uspfilter/localip.go
@@ -45,24 +45,26 @@ func (m *localIPManager) setBitmapBit(ip net.IP) {
 	m.ipv4Bitmap[high].bitmap[index] |= 1 << bit
 }
 
-func (m *localIPManager) setBitInBitmap(ip net.IP, bitmap *[256]*ipv4LowBitmap, ipv4Set map[string]struct{}, ipv4Addresses *[]string) {
-	if ipv4 := ip.To4(); ipv4 != nil {
-		high := uint16(ipv4[0])
-		low := (uint16(ipv4[1]) << 8) | (uint16(ipv4[2]) << 4) | uint16(ipv4[3])
+func (m *localIPManager) setBitInBitmap(ip netip.Addr, bitmap *[256]*ipv4LowBitmap, ipv4Set map[netip.Addr]struct{}, ipv4Addresses *[]netip.Addr) {
+	if !ip.Is4() {
+		return
+	}
+	ipv4 := ip.AsSlice()
 
-		if bitmap[high] == nil {
-			bitmap[high] = &ipv4LowBitmap{}
-		}
+	high := uint16(ipv4[0])
+	low := (uint16(ipv4[1]) << 8) | (uint16(ipv4[2]) << 4) | uint16(ipv4[3])
+
+	if bitmap[high] == nil {
+		bitmap[high] = &ipv4LowBitmap{}
+	}
 
-		index := low / 32
-		bit := low % 32
-		bitmap[high].bitmap[index] |= 1 << bit
+	index := low / 32
+	bit := low % 32
+	bitmap[high].bitmap[index] |= 1 << bit
 
-		ipStr := ipv4.String()
-		if _, exists := ipv4Set[ipStr]; !exists {
-			ipv4Set[ipStr] = struct{}{}
-			*ipv4Addresses = append(*ipv4Addresses, ipStr)
-		}
+	if _, exists := ipv4Set[ip]; !exists {
+		ipv4Set[ip] = struct{}{}
+		*ipv4Addresses = append(*ipv4Addresses, ip)
 	}
 }
 
@@ -79,12 +81,12 @@ func (m *localIPManager) checkBitmapBit(ip []byte) bool {
 	return (m.ipv4Bitmap[high].bitmap[index] & (1 << bit)) != 0
 }
 
-func (m *localIPManager) processIP(ip net.IP, bitmap *[256]*ipv4LowBitmap, ipv4Set map[string]struct{}, ipv4Addresses *[]string) error {
+func (m *localIPManager) processIP(ip netip.Addr, bitmap *[256]*ipv4LowBitmap, ipv4Set map[netip.Addr]struct{}, ipv4Addresses *[]netip.Addr) error {
 	m.setBitInBitmap(ip, bitmap, ipv4Set, ipv4Addresses)
 	return nil
 }
 
-func (m *localIPManager) processInterface(iface net.Interface, bitmap *[256]*ipv4LowBitmap, ipv4Set map[string]struct{}, ipv4Addresses *[]string) {
+func (m *localIPManager) processInterface(iface net.Interface, bitmap *[256]*ipv4LowBitmap, ipv4Set map[netip.Addr]struct{}, ipv4Addresses *[]netip.Addr) {
 	addrs, err := iface.Addrs()
 	if err != nil {
 		log.Debugf("get addresses for interface %s failed: %v", iface.Name, err)
@@ -102,7 +104,13 @@ func (m *localIPManager) processInterface(iface net.Interface, bitmap *[256]*ipv
 			continue
 		}
 
-		if err := m.processIP(ip, bitmap, ipv4Set, ipv4Addresses); err != nil {
+		addr, ok := netip.AddrFromSlice(ip)
+		if !ok {
+			log.Warnf("invalid IP address %s in interface %s", ip.String(), iface.Name)
+			continue
+		}
+
+		if err := m.processIP(addr.Unmap(), bitmap, ipv4Set, ipv4Addresses); err != nil {
 			log.Debugf("process IP failed: %v", err)
 		}
 	}
@@ -116,8 +124,8 @@ func (m *localIPManager) UpdateLocalIPs(iface common.IFaceMapper) (err error) {
 	}()
 
 	var newIPv4Bitmap [256]*ipv4LowBitmap
-	ipv4Set := make(map[string]struct{})
-	var ipv4Addresses []string
+	ipv4Set := make(map[netip.Addr]struct{})
+	var ipv4Addresses []netip.Addr
 
 	// 127.0.0.0/8
 	newIPv4Bitmap[127] = &ipv4LowBitmap{}
diff --git a/client/firewall/uspfilter/rule.go b/client/firewall/uspfilter/rule.go
index a23d2011..b765c72e 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/uspfilter.go b/client/firewall/uspfilter/uspfilter.go
index 466c6a18..c216bc30 100644
--- a/client/firewall/uspfilter/uspfilter.go
+++ b/client/firewall/uspfilter/uspfilter.go
@@ -39,8 +39,12 @@ const (
 	// EnvForceUserspaceRouter forces userspace routing even if native routing is available.
 	EnvForceUserspaceRouter = "NB_FORCE_USERSPACE_ROUTER"
 
-	// EnvEnableNetstackLocalForwarding enables forwarding of local traffic to the native stack when running netstack
-	// Leaving this on by default introduces a security risk as sockets on listening on localhost only will be accessible
+	// EnvEnableLocalForwarding enables forwarding of local traffic to the native stack for internal (non-NetBird) interfaces.
+	// Default off as it might be security risk because sockets listening on localhost only will become accessible.
+	EnvEnableLocalForwarding = "NB_ENABLE_LOCAL_FORWARDING"
+
+	// EnvEnableNetstackLocalForwarding is an alias for EnvEnableLocalForwarding.
+	// In netstack mode, it enables forwarding of local traffic to the native stack for all interfaces.
 	EnvEnableNetstackLocalForwarding = "NB_ENABLE_NETSTACK_LOCAL_FORWARDING"
 )
 
@@ -49,10 +53,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
@@ -71,7 +75,6 @@ type Manager struct {
 	// incomingRules is used for filtering and hooks
 	incomingRules  map[netip.Addr]RuleSet
 	routeRules     RouteRules
-	wgNetwork      *net.IPNet
 	decoders       sync.Pool
 	wgIface        common.IFaceMapper
 	nativeFirewall firewall.Manager
@@ -99,6 +102,8 @@ type Manager struct {
 	forwarder   atomic.Pointer[forwarder.Forwarder]
 	logger      *nblog.Logger
 	flowLogger  nftypes.FlowLogger
+
+	blockRule firewall.Rule
 }
 
 // decoder for packages
@@ -146,6 +151,11 @@ func parseCreateEnv() (bool, bool) {
 		if err != nil {
 			log.Warnf("failed to parse %s: %v", EnvEnableNetstackLocalForwarding, err)
 		}
+	} else if val := os.Getenv(EnvEnableLocalForwarding); val != "" {
+		enableLocalForwarding, err = strconv.ParseBool(val)
+		if err != nil {
+			log.Warnf("failed to parse %s: %v", EnvEnableLocalForwarding, err)
+		}
 	}
 
 	return disableConntrack, enableLocalForwarding
@@ -201,41 +211,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 {
@@ -273,7 +277,7 @@ func (m *Manager) determineRouting() error {
 
 		log.Info("userspace routing is forced")
 
-	case !m.netstack && m.nativeFirewall != nil && m.nativeFirewall.IsServerRouteSupported():
+	case !m.netstack && m.nativeFirewall != nil:
 		// if the OS supports routing natively, then we don't need to filter/route ourselves
 		// netstack mode won't support native routing as there is no interface
 
@@ -330,6 +334,10 @@ func (m *Manager) IsServerRouteSupported() bool {
 	return true
 }
 
+func (m *Manager) IsStateful() bool {
+	return m.stateful
+}
+
 func (m *Manager) AddNatRule(pair firewall.RouterPair) error {
 	if m.nativeRouter.Load() && m.nativeFirewall != nil {
 		return m.nativeFirewall.AddNatRule(pair)
@@ -413,10 +421,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 +447,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 +535,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)
@@ -546,9 +618,8 @@ func (m *Manager) processOutgoingHooks(packetData []byte, size int) bool {
 		return true
 	}
 
-	if m.stateful {
-		m.trackOutbound(d, srcIP, dstIP, size)
-	}
+	// for netflow we keep track even if the firewall is stateless
+	m.trackOutbound(d, srcIP, dstIP, size)
 
 	return false
 }
@@ -717,9 +788,10 @@ func (m *Manager) handleLocalTraffic(d *decoder, srcIP, dstIP netip.Addr, packet
 		return true
 	}
 
-	// if running in netstack mode we need to pass this to the forwarder
-	if m.netstack && m.localForwarding {
-		return m.handleNetstackLocalTraffic(packetData)
+	// If requested we pass local traffic to internal interfaces to the forwarder.
+	// netstack doesn't have an interface to forward packets to the native stack so we always need to use the forwarder.
+	if m.localForwarding && (m.netstack || dstIP != m.wgIface.Address().IP) {
+		return m.handleForwardedLocalTraffic(packetData)
 	}
 
 	// track inbound packets to get the correct direction and session id for flows
@@ -729,8 +801,7 @@ func (m *Manager) handleLocalTraffic(d *decoder, srcIP, dstIP netip.Addr, packet
 	return false
 }
 
-func (m *Manager) handleNetstackLocalTraffic(packetData []byte) bool {
-
+func (m *Manager) handleForwardedLocalTraffic(packetData []byte) bool {
 	fwd := m.forwarder.Load()
 	if fwd == nil {
 		m.logger.Trace("Dropping local packet (forwarder not initialized)")
@@ -764,7 +835,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 +862,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 +1063,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
 	}
 
@@ -1017,11 +1099,6 @@ func (m *Manager) ruleMatches(rule RouteRule, srcAddr, dstAddr netip.Addr, proto
 	return true
 }
 
-// SetNetwork of the wireguard interface to which filtering applied
-func (m *Manager) SetNetwork(network *net.IPNet) {
-	m.wgNetwork = network
-}
-
 // AddUDPPacketHook calls hook when UDP packet from given direction matched
 //
 // Hook function returns flag which indicates should be the matched package dropped or not
@@ -1091,7 +1168,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 +1208,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/iface/bind/udp_mux_universal.go b/client/iface/bind/udp_mux_universal.go
index 9fed02bb..5cc63495 100644
--- a/client/iface/bind/udp_mux_universal.go
+++ b/client/iface/bind/udp_mux_universal.go
@@ -164,7 +164,7 @@ func (u *udpConn) performFilterCheck(addr net.Addr) error {
 		return nil
 	}
 
-	if u.address.Network.Contains(a.AsSlice()) {
+	if u.address.Network.Contains(a) {
 		log.Warnf("Address %s is part of the NetBird network %s, refusing to write", addr, u.address)
 		return fmt.Errorf("address %s is part of the NetBird network %s, refusing to write", addr, u.address)
 	}
diff --git a/client/iface/configurer/kernel_unix.go b/client/iface/configurer/kernel_unix.go
index 6f09a63c..91991177 100644
--- a/client/iface/configurer/kernel_unix.go
+++ b/client/iface/configurer/kernel_unix.go
@@ -12,6 +12,8 @@ import (
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
 )
 
+var zeroKey wgtypes.Key
+
 type KernelConfigurer struct {
 	deviceName string
 }
@@ -201,14 +203,71 @@ func (c *KernelConfigurer) configure(config wgtypes.Config) error {
 func (c *KernelConfigurer) Close() {
 }
 
-func (c *KernelConfigurer) GetStats(peerKey string) (WGStats, error) {
-	peer, err := c.getPeer(c.deviceName, peerKey)
+func (c *KernelConfigurer) FullStats() (*Stats, error) {
+	wg, err := wgctrl.New()
+	if err != nil {
+		return nil, fmt.Errorf("wgctl: %w", err)
+	}
+	defer func() {
+		err = wg.Close()
+		if err != nil {
+			log.Errorf("Got error while closing wgctl: %v", err)
+		}
+	}()
+
+	wgDevice, err := wg.Device(c.deviceName)
+	if err != nil {
+		return nil, fmt.Errorf("get device %s: %w", c.deviceName, err)
+	}
+	fullStats := &Stats{
+		DeviceName: wgDevice.Name,
+		PublicKey:  wgDevice.PublicKey.String(),
+		ListenPort: wgDevice.ListenPort,
+		FWMark:     wgDevice.FirewallMark,
+		Peers:      []Peer{},
+	}
+
+	for _, p := range wgDevice.Peers {
+		peer := Peer{
+			PublicKey:     p.PublicKey.String(),
+			AllowedIPs:    p.AllowedIPs,
+			TxBytes:       p.TransmitBytes,
+			RxBytes:       p.ReceiveBytes,
+			LastHandshake: p.LastHandshakeTime,
+			PresharedKey:  p.PresharedKey != zeroKey,
+		}
+		if p.Endpoint != nil {
+			peer.Endpoint = *p.Endpoint
+		}
+		fullStats.Peers = append(fullStats.Peers, peer)
+	}
+	return fullStats, nil
+}
+
+func (c *KernelConfigurer) GetStats() (map[string]WGStats, error) {
+	stats := make(map[string]WGStats)
+	wg, err := wgctrl.New()
 	if err != nil {
-		return WGStats{}, fmt.Errorf("get wireguard stats: %w", err)
+		return nil, fmt.Errorf("wgctl: %w", err)
+	}
+	defer func() {
+		err = wg.Close()
+		if err != nil {
+			log.Errorf("Got error while closing wgctl: %v", err)
+		}
+	}()
+
+	wgDevice, err := wg.Device(c.deviceName)
+	if err != nil {
+		return nil, fmt.Errorf("get device %s: %w", c.deviceName, err)
+	}
+
+	for _, peer := range wgDevice.Peers {
+		stats[peer.PublicKey.String()] = WGStats{
+			LastHandshake: peer.LastHandshakeTime,
+			TxBytes:       peer.TransmitBytes,
+			RxBytes:       peer.ReceiveBytes,
+		}
 	}
-	return WGStats{
-		LastHandshake: peer.LastHandshakeTime,
-		TxBytes:       peer.TransmitBytes,
-		RxBytes:       peer.ReceiveBytes,
-	}, nil
+	return stats, nil
 }
diff --git a/client/iface/configurer/usp.go b/client/iface/configurer/usp.go
index e536f265..91478882 100644
--- a/client/iface/configurer/usp.go
+++ b/client/iface/configurer/usp.go
@@ -1,6 +1,7 @@
 package configurer
 
 import (
+	"encoding/base64"
 	"encoding/hex"
 	"fmt"
 	"net"
@@ -17,6 +18,20 @@ import (
 	nbnet "github.com/netbirdio/netbird/util/net"
 )
 
+const (
+	privateKey                  = "private_key"
+	ipcKeyLastHandshakeTimeSec  = "last_handshake_time_sec"
+	ipcKeyLastHandshakeTimeNsec = "last_handshake_time_nsec"
+	ipcKeyTxBytes               = "tx_bytes"
+	ipcKeyRxBytes               = "rx_bytes"
+	allowedIP                   = "allowed_ip"
+	endpoint                    = "endpoint"
+	fwmark                      = "fwmark"
+	listenPort                  = "listen_port"
+	publicKey                   = "public_key"
+	presharedKey                = "preshared_key"
+)
+
 var ErrAllowedIPNotFound = fmt.Errorf("allowed IP not found")
 
 type WGUSPConfigurer struct {
@@ -178,6 +193,15 @@ func (c *WGUSPConfigurer) RemoveAllowedIP(peerKey string, ip string) error {
 	return c.device.IpcSet(toWgUserspaceString(config))
 }
 
+func (c *WGUSPConfigurer) FullStats() (*Stats, error) {
+	ipcStr, err := c.device.IpcGet()
+	if err != nil {
+		return nil, fmt.Errorf("IpcGet failed: %w", err)
+	}
+
+	return parseStatus(c.deviceName, ipcStr)
+}
+
 // startUAPI starts the UAPI listener for managing the WireGuard interface via external tool
 func (t *WGUSPConfigurer) startUAPI() {
 	var err error
@@ -217,91 +241,75 @@ func (t *WGUSPConfigurer) Close() {
 	}
 }
 
-func (t *WGUSPConfigurer) GetStats(peerKey string) (WGStats, error) {
+func (t *WGUSPConfigurer) GetStats() (map[string]WGStats, error) {
 	ipc, err := t.device.IpcGet()
 	if err != nil {
-		return WGStats{}, fmt.Errorf("ipc get: %w", err)
-	}
-
-	stats, err := findPeerInfo(ipc, peerKey, []string{
-		"last_handshake_time_sec",
-		"last_handshake_time_nsec",
-		"tx_bytes",
-		"rx_bytes",
-	})
-	if err != nil {
-		return WGStats{}, fmt.Errorf("find peer info: %w", err)
-	}
-
-	sec, err := strconv.ParseInt(stats["last_handshake_time_sec"], 10, 64)
-	if err != nil {
-		return WGStats{}, fmt.Errorf("parse handshake sec: %w", err)
-	}
-	nsec, err := strconv.ParseInt(stats["last_handshake_time_nsec"], 10, 64)
-	if err != nil {
-		return WGStats{}, fmt.Errorf("parse handshake nsec: %w", err)
-	}
-	txBytes, err := strconv.ParseInt(stats["tx_bytes"], 10, 64)
-	if err != nil {
-		return WGStats{}, fmt.Errorf("parse tx_bytes: %w", err)
-	}
-	rxBytes, err := strconv.ParseInt(stats["rx_bytes"], 10, 64)
-	if err != nil {
-		return WGStats{}, fmt.Errorf("parse rx_bytes: %w", err)
+		return nil, fmt.Errorf("ipc get: %w", err)
 	}
 
-	return WGStats{
-		LastHandshake: time.Unix(sec, nsec),
-		TxBytes:       txBytes,
-		RxBytes:       rxBytes,
-	}, nil
+	return parseTransfers(ipc)
 }
 
-func findPeerInfo(ipcInput string, peerKey string, searchConfigKeys []string) (map[string]string, error) {
-	peerKeyParsed, err := wgtypes.ParseKey(peerKey)
-	if err != nil {
-		return nil, fmt.Errorf("parse key: %w", err)
-	}
-
-	hexKey := hex.EncodeToString(peerKeyParsed[:])
-
-	lines := strings.Split(ipcInput, "\n")
-
-	configFound := map[string]string{}
-	foundPeer := false
+func parseTransfers(ipc string) (map[string]WGStats, error) {
+	stats := make(map[string]WGStats)
+	var (
+		currentKey   string
+		currentStats WGStats
+		hasPeer      bool
+	)
+	lines := strings.Split(ipc, "\n")
 	for _, line := range lines {
 		line = strings.TrimSpace(line)
 
 		// If we're within the details of the found peer and encounter another public key,
 		// this means we're starting another peer's details. So, stop.
-		if strings.HasPrefix(line, "public_key=") && foundPeer {
-			break
+		if strings.HasPrefix(line, "public_key=") {
+			peerID := strings.TrimPrefix(line, "public_key=")
+			h, err := hex.DecodeString(peerID)
+			if err != nil {
+				return nil, fmt.Errorf("decode peerID: %w", err)
+			}
+			currentKey = base64.StdEncoding.EncodeToString(h)
+			currentStats = WGStats{} // Reset stats for the new peer
+			hasPeer = true
+			stats[currentKey] = currentStats
+			continue
 		}
 
-		// Identify the peer with the specific public key
-		if line == fmt.Sprintf("public_key=%s", hexKey) {
-			foundPeer = true
+		if !hasPeer {
+			continue
 		}
 
-		for _, key := range searchConfigKeys {
-			if foundPeer && strings.HasPrefix(line, key+"=") {
-				v := strings.SplitN(line, "=", 2)
-				configFound[v[0]] = v[1]
-			}
+		key := strings.SplitN(line, "=", 2)
+		if len(key) != 2 {
+			continue
 		}
-	}
-
-	// todo: use multierr
-	for _, key := range searchConfigKeys {
-		if _, ok := configFound[key]; !ok {
-			return configFound, fmt.Errorf("config key not found: %s", key)
+		switch key[0] {
+		case ipcKeyLastHandshakeTimeSec:
+			hs, err := toLastHandshake(key[1])
+			if err != nil {
+				return nil, err
+			}
+			currentStats.LastHandshake = hs
+			stats[currentKey] = currentStats
+		case ipcKeyRxBytes:
+			rxBytes, err := toBytes(key[1])
+			if err != nil {
+				return nil, fmt.Errorf("parse rx_bytes: %w", err)
+			}
+			currentStats.RxBytes = rxBytes
+			stats[currentKey] = currentStats
+		case ipcKeyTxBytes:
+			TxBytes, err := toBytes(key[1])
+			if err != nil {
+				return nil, fmt.Errorf("parse tx_bytes: %w", err)
+			}
+			currentStats.TxBytes = TxBytes
+			stats[currentKey] = currentStats
 		}
 	}
-	if !foundPeer {
-		return nil, fmt.Errorf("%w: %s", ErrPeerNotFound, peerKey)
-	}
 
-	return configFound, nil
+	return stats, nil
 }
 
 func toWgUserspaceString(wgCfg wgtypes.Config) string {
@@ -355,9 +363,154 @@ func toWgUserspaceString(wgCfg wgtypes.Config) string {
 	return sb.String()
 }
 
+func toLastHandshake(stringVar string) (time.Time, error) {
+	sec, err := strconv.ParseInt(stringVar, 10, 64)
+	if err != nil {
+		return time.Time{}, fmt.Errorf("parse handshake sec: %w", err)
+	}
+	return time.Unix(sec, 0), nil
+}
+
+func toBytes(s string) (int64, error) {
+	return strconv.ParseInt(s, 10, 64)
+}
+
 func getFwmark() int {
 	if nbnet.AdvancedRouting() {
 		return nbnet.ControlPlaneMark
 	}
 	return 0
 }
+
+func hexToWireguardKey(hexKey string) (wgtypes.Key, error) {
+	// Decode hex string to bytes
+	keyBytes, err := hex.DecodeString(hexKey)
+	if err != nil {
+		return wgtypes.Key{}, fmt.Errorf("failed to decode hex key: %w", err)
+	}
+
+	// Check if we have the right number of bytes (WireGuard keys are 32 bytes)
+	if len(keyBytes) != 32 {
+		return wgtypes.Key{}, fmt.Errorf("invalid key length: expected 32 bytes, got %d", len(keyBytes))
+	}
+
+	// Convert to wgtypes.Key
+	var key wgtypes.Key
+	copy(key[:], keyBytes)
+
+	return key, nil
+}
+
+func parseStatus(deviceName, ipcStr string) (*Stats, error) {
+	stats := &Stats{DeviceName: deviceName}
+	var currentPeer *Peer
+	for _, line := range strings.Split(strings.TrimSpace(ipcStr), "\n") {
+		if line == "" {
+			continue
+		}
+		parts := strings.SplitN(line, "=", 2)
+		if len(parts) != 2 {
+			continue
+		}
+		key := parts[0]
+		val := parts[1]
+
+		switch key {
+		case privateKey:
+			key, err := hexToWireguardKey(val)
+			if err != nil {
+				log.Errorf("failed to parse private key: %v", err)
+				continue
+			}
+			stats.PublicKey = key.PublicKey().String()
+		case publicKey:
+			// Save previous peer
+			if currentPeer != nil {
+				stats.Peers = append(stats.Peers, *currentPeer)
+			}
+			key, err := hexToWireguardKey(val)
+			if err != nil {
+				log.Errorf("failed to parse public key: %v", err)
+				continue
+			}
+			currentPeer = &Peer{
+				PublicKey: key.String(),
+			}
+		case listenPort:
+			if port, err := strconv.Atoi(val); err == nil {
+				stats.ListenPort = port
+			}
+		case fwmark:
+			if fwmark, err := strconv.Atoi(val); err == nil {
+				stats.FWMark = fwmark
+			}
+		case endpoint:
+			if currentPeer == nil {
+				continue
+			}
+
+			host, portStr, err := net.SplitHostPort(strings.Trim(val, "[]"))
+			if err != nil {
+				log.Errorf("failed to parse endpoint: %v", err)
+				continue
+			}
+			port, err := strconv.Atoi(portStr)
+			if err != nil {
+				log.Errorf("failed to parse endpoint port: %v", err)
+				continue
+			}
+			currentPeer.Endpoint = net.UDPAddr{
+				IP:   net.ParseIP(host),
+				Port: port,
+			}
+		case allowedIP:
+			if currentPeer == nil {
+				continue
+			}
+			_, ipnet, err := net.ParseCIDR(val)
+			if err == nil {
+				currentPeer.AllowedIPs = append(currentPeer.AllowedIPs, *ipnet)
+			}
+		case ipcKeyTxBytes:
+			if currentPeer == nil {
+				continue
+			}
+			rxBytes, err := toBytes(val)
+			if err != nil {
+				continue
+			}
+			currentPeer.TxBytes = rxBytes
+		case ipcKeyRxBytes:
+			if currentPeer == nil {
+				continue
+			}
+			rxBytes, err := toBytes(val)
+			if err != nil {
+				continue
+			}
+			currentPeer.RxBytes = rxBytes
+
+		case ipcKeyLastHandshakeTimeSec:
+			if currentPeer == nil {
+				continue
+			}
+
+			ts, err := toLastHandshake(val)
+			if err != nil {
+				continue
+			}
+			currentPeer.LastHandshake = ts
+		case presharedKey:
+			if currentPeer == nil {
+				continue
+			}
+			if val != "" {
+				currentPeer.PresharedKey = true
+			}
+		}
+	}
+	if currentPeer != nil {
+		stats.Peers = append(stats.Peers, *currentPeer)
+	}
+	return stats, nil
+}
diff --git a/client/iface/configurer/wgshow.go b/client/iface/configurer/wgshow.go
new file mode 100644
index 00000000..60426402
--- /dev/null
+++ b/client/iface/configurer/wgshow.go
@@ -0,0 +1,24 @@
+package configurer
+
+import (
+	"net"
+	"time"
+)
+
+type Peer struct {
+	PublicKey     string
+	Endpoint      net.UDPAddr
+	AllowedIPs    []net.IPNet
+	TxBytes       int64
+	RxBytes       int64
+	LastHandshake time.Time
+	PresharedKey  bool
+}
+
+type Stats struct {
+	DeviceName string
+	PublicKey  string
+	ListenPort int
+	FWMark     int
+	Peers      []Peer
+}
diff --git a/client/iface/device/device_filter.go b/client/iface/device/device_filter.go
index c9b7e244..5a1a0e96 100644
--- a/client/iface/device/device_filter.go
+++ b/client/iface/device/device_filter.go
@@ -1,7 +1,6 @@
 package device
 
 import (
-	"net"
 	"net/netip"
 	"sync"
 
@@ -24,9 +23,6 @@ type PacketFilter interface {
 
 	// RemovePacketHook removes hook by ID
 	RemovePacketHook(hookID string) error
-
-	// SetNetwork of the wireguard interface to which filtering applied
-	SetNetwork(*net.IPNet)
 }
 
 // FilteredDevice to override Read or Write of packets
diff --git a/client/iface/device/device_netstack.go b/client/iface/device/device_netstack.go
index d3c92235..d2f2c87a 100644
--- a/client/iface/device/device_netstack.go
+++ b/client/iface/device/device_netstack.go
@@ -51,7 +51,11 @@ func (t *TunNetstackDevice) Create() (WGConfigurer, error) {
 	log.Info("create nbnetstack tun interface")
 
 	// TODO: get from service listener runtime IP
-	dnsAddr := nbnet.GetLastIPFromNetwork(t.address.Network, 1)
+	dnsAddr, err := nbnet.GetLastIPFromNetwork(t.address.Network, 1)
+	if err != nil {
+		return nil, fmt.Errorf("last ip: %w", err)
+	}
+
 	log.Debugf("netstack using address: %s", t.address.IP)
 	t.nsTun = nbnetstack.NewNetStackTun(t.listenAddress, t.address.IP, dnsAddr, t.mtu)
 	log.Debugf("netstack using dns address: %s", dnsAddr)
diff --git a/client/iface/device/interface.go b/client/iface/device/interface.go
index 6971b694..31ebdf4b 100644
--- a/client/iface/device/interface.go
+++ b/client/iface/device/interface.go
@@ -16,5 +16,6 @@ type WGConfigurer interface {
 	AddAllowedIP(peerKey string, allowedIP string) error
 	RemoveAllowedIP(peerKey string, allowedIP string) error
 	Close()
-	GetStats(peerKey string) (configurer.WGStats, error)
+	GetStats() (map[string]configurer.WGStats, error)
+	FullStats() (*configurer.Stats, error)
 }
diff --git a/client/iface/device/wg_link_freebsd.go b/client/iface/device/wg_link_freebsd.go
index 9067790e..1b06e0e1 100644
--- a/client/iface/device/wg_link_freebsd.go
+++ b/client/iface/device/wg_link_freebsd.go
@@ -64,7 +64,15 @@ func (l *wgLink) assignAddr(address wgaddr.Address) error {
 	}
 
 	ip := address.IP.String()
-	mask := "0x" + address.Network.Mask.String()
+
+	// Convert prefix length to hex netmask
+	prefixLen := address.Network.Bits()
+	if !address.IP.Is4() {
+		return fmt.Errorf("IPv6 not supported for interface assignment")
+	}
+
+	maskBits := uint32(0xffffffff) << (32 - prefixLen)
+	mask := fmt.Sprintf("0x%08x", maskBits)
 
 	log.Infof("assign addr %s mask %s to %s interface", ip, mask, l.name)
 
diff --git a/client/iface/iface.go b/client/iface/iface.go
index 9d5262ae..f4394c47 100644
--- a/client/iface/iface.go
+++ b/client/iface/iface.go
@@ -185,7 +185,6 @@ func (w *WGIface) SetFilter(filter device.PacketFilter) error {
 	}
 
 	w.filter = filter
-	w.filter.SetNetwork(w.tun.WgAddress().Network)
 
 	w.tun.FilteredDevice().SetFilter(filter)
 	return nil
@@ -212,9 +211,13 @@ func (w *WGIface) GetWGDevice() *wgdevice.Device {
 	return w.tun.Device()
 }
 
-// GetStats returns the last handshake time, rx and tx bytes for the given peer
-func (w *WGIface) GetStats(peerKey string) (configurer.WGStats, error) {
-	return w.configurer.GetStats(peerKey)
+// GetStats returns the last handshake time, rx and tx bytes
+func (w *WGIface) GetStats() (map[string]configurer.WGStats, error) {
+	return w.configurer.GetStats()
+}
+
+func (w *WGIface) FullStats() (*configurer.Stats, error) {
+	return w.configurer.FullStats()
 }
 
 func (w *WGIface) waitUntilRemoved() error {
diff --git a/client/iface/mocks/filter.go b/client/iface/mocks/filter.go
index faac55d6..8cd2a123 100644
--- a/client/iface/mocks/filter.go
+++ b/client/iface/mocks/filter.go
@@ -5,7 +5,6 @@
 package mocks
 
 import (
-	net "net"
 	"net/netip"
 	reflect "reflect"
 
@@ -90,15 +89,3 @@ func (mr *MockPacketFilterMockRecorder) RemovePacketHook(arg0 interface{}) *gomo
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemovePacketHook", reflect.TypeOf((*MockPacketFilter)(nil).RemovePacketHook), arg0)
 }
-
-// SetNetwork mocks base method.
-func (m *MockPacketFilter) SetNetwork(arg0 *net.IPNet) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "SetNetwork", arg0)
-}
-
-// SetNetwork indicates an expected call of SetNetwork.
-func (mr *MockPacketFilterMockRecorder) SetNetwork(arg0 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetNetwork", reflect.TypeOf((*MockPacketFilter)(nil).SetNetwork), arg0)
-}
diff --git a/client/iface/netstack/tun.go b/client/iface/netstack/tun.go
index a271a195..aec9d4fa 100644
--- a/client/iface/netstack/tun.go
+++ b/client/iface/netstack/tun.go
@@ -1,8 +1,6 @@
 package netstack
 
 import (
-	"fmt"
-	"net"
 	"net/netip"
 	"os"
 	"strconv"
@@ -15,8 +13,8 @@ import (
 const EnvSkipProxy = "NB_NETSTACK_SKIP_PROXY"
 
 type NetStackTun struct { //nolint:revive
-	address       net.IP
-	dnsAddress    net.IP
+	address       netip.Addr
+	dnsAddress    netip.Addr
 	mtu           int
 	listenAddress string
 
@@ -24,7 +22,7 @@ type NetStackTun struct { //nolint:revive
 	tundev tun.Device
 }
 
-func NewNetStackTun(listenAddress string, address net.IP, dnsAddress net.IP, mtu int) *NetStackTun {
+func NewNetStackTun(listenAddress string, address netip.Addr, dnsAddress netip.Addr, mtu int) *NetStackTun {
 	return &NetStackTun{
 		address:       address,
 		dnsAddress:    dnsAddress,
@@ -34,19 +32,9 @@ func NewNetStackTun(listenAddress string, address net.IP, dnsAddress net.IP, mtu
 }
 
 func (t *NetStackTun) Create() (tun.Device, *netstack.Net, error) {
-	addr, ok := netip.AddrFromSlice(t.address)
-	if !ok {
-		return nil, nil, fmt.Errorf("convert address to netip.Addr: %v", t.address)
-	}
-
-	dnsAddr, ok := netip.AddrFromSlice(t.dnsAddress)
-	if !ok {
-		return nil, nil, fmt.Errorf("convert dns address to netip.Addr: %v", t.dnsAddress)
-	}
-
 	nsTunDev, tunNet, err := netstack.CreateNetTUN(
-		[]netip.Addr{addr.Unmap()},
-		[]netip.Addr{dnsAddr.Unmap()},
+		[]netip.Addr{t.address},
+		[]netip.Addr{t.dnsAddress},
 		t.mtu)
 	if err != nil {
 		return nil, nil, err
diff --git a/client/iface/wgaddr/address.go b/client/iface/wgaddr/address.go
index e5079258..078f8be9 100644
--- a/client/iface/wgaddr/address.go
+++ b/client/iface/wgaddr/address.go
@@ -2,28 +2,27 @@ package wgaddr
 
 import (
 	"fmt"
-	"net"
+	"net/netip"
 )
 
 // Address WireGuard parsed address
 type Address struct {
-	IP      net.IP
-	Network *net.IPNet
+	IP      netip.Addr
+	Network netip.Prefix
 }
 
 // ParseWGAddress parse a string ("1.2.3.4/24") address to WG Address
 func ParseWGAddress(address string) (Address, error) {
-	ip, network, err := net.ParseCIDR(address)
+	prefix, err := netip.ParsePrefix(address)
 	if err != nil {
 		return Address{}, err
 	}
 	return Address{
-		IP:      ip,
-		Network: network,
+		IP:      prefix.Addr().Unmap(),
+		Network: prefix.Masked(),
 	}, nil
 }
 
 func (addr Address) String() string {
-	maskSize, _ := addr.Network.Mask.Size()
-	return fmt.Sprintf("%s/%d", addr.IP.String(), maskSize)
+	return fmt.Sprintf("%s/%d", addr.IP.String(), addr.Network.Bits())
 }
diff --git a/client/installer.nsis b/client/installer.nsis
index 5219058a..fabd727f 100644
--- a/client/installer.nsis
+++ b/client/installer.nsis
@@ -24,6 +24,8 @@
 
 !define AUTOSTART_REG_KEY "Software\Microsoft\Windows\CurrentVersion\Run"
 
+!define NETBIRD_DATA_DIR "$COMMONPROGRAMDATA\Netbird"
+
 Unicode True
 
 ######################################################################
@@ -49,6 +51,10 @@ ShowInstDetails Show
 
 ######################################################################
 
+!include "MUI2.nsh"
+!include LogicLib.nsh
+!include "nsDialogs.nsh"
+
 !define MUI_ICON "${ICON}"
 !define MUI_UNICON "${ICON}"
 !define MUI_WELCOMEFINISHPAGE_BITMAP "${BANNER}"
@@ -58,9 +64,6 @@ ShowInstDetails Show
 !define MUI_FINISHPAGE_RUN_FUNCTION "LaunchLink"
 ######################################################################
 
-!include "MUI2.nsh"
-!include LogicLib.nsh
-
 !define MUI_ABORTWARNING
 !define MUI_UNABORTWARNING
 
@@ -70,13 +73,16 @@ ShowInstDetails Show
 
 !insertmacro MUI_PAGE_DIRECTORY
 
-; Custom page for autostart checkbox
 Page custom AutostartPage AutostartPageLeave
 
 !insertmacro MUI_PAGE_INSTFILES
 
 !insertmacro MUI_PAGE_FINISH
 
+!insertmacro MUI_UNPAGE_WELCOME
+
+UninstPage custom un.DeleteDataPage un.DeleteDataPageLeave
+
 !insertmacro MUI_UNPAGE_CONFIRM
 
 !insertmacro MUI_UNPAGE_INSTFILES
@@ -89,6 +95,10 @@ Page custom AutostartPage AutostartPageLeave
 Var AutostartCheckbox
 Var AutostartEnabled
 
+; Variables for uninstall data deletion option
+Var DeleteDataCheckbox
+Var DeleteDataEnabled
+
 ######################################################################
 
 ; Function to create the autostart options page
@@ -104,8 +114,8 @@ Function AutostartPage
 
   ${NSD_CreateCheckbox} 0 20u 100% 10u "Start ${APP_NAME} UI automatically when Windows starts"
   Pop $AutostartCheckbox
-  ${NSD_Check} $AutostartCheckbox ; Default to checked
-  StrCpy $AutostartEnabled "1" ; Default to enabled
+  ${NSD_Check} $AutostartCheckbox
+  StrCpy $AutostartEnabled "1"
 
   nsDialogs::Show
 FunctionEnd
@@ -115,6 +125,30 @@ Function AutostartPageLeave
   ${NSD_GetState} $AutostartCheckbox $AutostartEnabled
 FunctionEnd
 
+; Function to create the uninstall data deletion page
+Function un.DeleteDataPage
+  !insertmacro MUI_HEADER_TEXT "Uninstall Options" "Choose whether to delete ${APP_NAME} data."
+
+  nsDialogs::Create 1018
+  Pop $0
+
+  ${If} $0 == error
+    Abort
+  ${EndIf}
+
+  ${NSD_CreateCheckbox} 0 20u 100% 10u "Delete all ${APP_NAME} configuration and state data (${NETBIRD_DATA_DIR})"
+  Pop $DeleteDataCheckbox
+  ${NSD_Uncheck} $DeleteDataCheckbox
+  StrCpy $DeleteDataEnabled "0"
+
+  nsDialogs::Show
+FunctionEnd
+
+; Function to handle leaving the data deletion page
+Function un.DeleteDataPageLeave
+  ${NSD_GetState} $DeleteDataCheckbox $DeleteDataEnabled
+FunctionEnd
+
 Function GetAppFromCommand
 Exch $1
 Push $2
@@ -176,10 +210,10 @@ ${EndIf}
 FunctionEnd
 ######################################################################
 Section -MainProgram
-	${INSTALL_TYPE}
-	# SetOverwrite ifnewer
-	SetOutPath "$INSTDIR"
-	File /r "..\\dist\\netbird_windows_amd64\\"
+    ${INSTALL_TYPE}
+    # SetOverwrite ifnewer
+    SetOutPath "$INSTDIR"
+    File /r "..\\dist\\netbird_windows_amd64\\"
 SectionEnd
 ######################################################################
 
@@ -225,31 +259,58 @@ SectionEnd
 Section Uninstall
 ${INSTALL_TYPE}
 
+DetailPrint "Stopping Netbird service..."
 ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service stop'
+DetailPrint "Uninstalling Netbird service..."
 ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service uninstall'
 
-# kill ui client
+DetailPrint "Terminating Netbird UI process..."
 ExecWait `taskkill /im ${UI_APP_EXE}.exe /f`
 
 ; Remove autostart registry entry
+DetailPrint "Removing autostart registry entry if exists..."
 DeleteRegValue HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}"
 
+; Handle data deletion based on checkbox
+DetailPrint "Checking if user requested data deletion..."
+${If} $DeleteDataEnabled == "1"
+    DetailPrint "User opted to delete Netbird data. Removing ${NETBIRD_DATA_DIR}..."
+    ClearErrors
+    RMDir /r "${NETBIRD_DATA_DIR}"
+    IfErrors 0 +2 ; If no errors, jump over the message
+    DetailPrint "Error deleting Netbird data directory. It might be in use or already removed."
+    DetailPrint "Netbird data directory removal complete."
+${Else}
+    DetailPrint "User did not opt to delete Netbird data."
+${EndIf}
+
 # wait the service uninstall take unblock the executable
+DetailPrint "Waiting for service handle to be released..."
 Sleep 3000
+
+DetailPrint "Deleting application files..."
 Delete "$INSTDIR\${UI_APP_EXE}"
 Delete "$INSTDIR\${MAIN_APP_EXE}"
 Delete "$INSTDIR\wintun.dll"
 Delete "$INSTDIR\opengl32.dll"
+DetailPrint "Removing application directory..."
 RmDir /r "$INSTDIR"
 
+DetailPrint "Removing shortcuts..."
 SetShellVarContext all
 Delete "$DESKTOP\${APP_NAME}.lnk"
 Delete "$SMPROGRAMS\${APP_NAME}.lnk"
 
+DetailPrint "Removing registry keys..."
 DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}"
 DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}"
+DeleteRegKey ${REG_ROOT} "${UI_REG_APP_PATH}"
+
+DetailPrint "Removing application directory from PATH..."
 EnVar::SetHKLM
 EnVar::DeleteValue "path" "$INSTDIR"
+
+DetailPrint "Uninstallation finished."
 SectionEnd
 
 
diff --git a/client/internal/acl/id/id.go b/client/internal/acl/id/id.go
index 93f16b42..23451453 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 61fbb10c..c8bc9123 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,10 +54,15 @@ 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()
 
+	if d.firewall == nil {
+		log.Debug("firewall manager is not supported, skipping firewall rules")
+		return
+	}
+
 	start := time.Now()
 	defer func() {
 		total := 0
@@ -68,21 +74,9 @@ func (d *DefaultManager) ApplyFiltering(networkMap *mgmProto.NetworkMap) {
 			time.Since(start), total)
 	}()
 
-	if d.firewall == nil {
-		log.Debug("firewall manager is not supported, skipping firewall rules")
-		return
-	}
-
 	d.applyPeerACLs(networkMap)
 
-	// If we got empty rules list but management did not set the networkMap.FirewallRulesIsEmpty flag,
-	// then the mgmt server is older than the client, and we need to allow all traffic for routes
-	isLegacy := len(networkMap.RoutesFirewallRules) == 0 && !networkMap.RoutesFirewallRulesIsEmpty
-	if err := d.firewall.SetLegacyManagement(isLegacy); err != nil {
-		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 +170,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 +202,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 +216,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)
@@ -296,8 +284,10 @@ func (d *DefaultManager) protoRuleToFirewallRule(
 	case mgmProto.RuleDirection_IN:
 		rules, err = d.addInRules(r.PolicyID, ip, protocol, port, action, ipsetName)
 	case mgmProto.RuleDirection_OUT:
-		// TODO: Remove this soon. Outbound rules are obsolete.
-		// We only maintain this for return traffic (inbound dir) which is now handled by the stateful firewall already
+		if d.firewall.IsStateful() {
+			return "", nil, nil
+		}
+		// return traffic for outbound connections if firewall is stateless
 		rules, err = d.addOutRules(r.PolicyID, ip, protocol, port, action, ipsetName)
 	default:
 		return "", nil, fmt.Errorf("invalid direction, skipping firewall rule")
@@ -580,6 +570,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/auth/oauth.go b/client/internal/auth/oauth.go
index 001609f2..86df58fd 100644
--- a/client/internal/auth/oauth.go
+++ b/client/internal/auth/oauth.go
@@ -64,13 +64,8 @@ func (t TokenInfo) GetTokenToUse() string {
 // and if that also fails, the authentication process is deemed unsuccessful
 //
 // On Linux distros without desktop environment support, it only tries to initialize the Device Code Flow
-func NewOAuthFlow(ctx context.Context, config *internal.Config, isLinuxDesktopClient bool) (OAuthFlow, error) {
-	if runtime.GOOS == "linux" && !isLinuxDesktopClient {
-		return authenticateWithDeviceCodeFlow(ctx, config)
-	}
-
-	// On FreeBSD we currently do not support desktop environments and offer only Device Code Flow (#2384)
-	if runtime.GOOS == "freebsd" {
+func NewOAuthFlow(ctx context.Context, config *internal.Config, isUnixDesktopClient bool) (OAuthFlow, error) {
+	if (runtime.GOOS == "linux" || runtime.GOOS == "freebsd") && !isUnixDesktopClient {
 		return authenticateWithDeviceCodeFlow(ctx, config)
 	}
 
diff --git a/client/internal/auth/pkce_flow.go b/client/internal/auth/pkce_flow.go
index c5bd84cd..d955679a 100644
--- a/client/internal/auth/pkce_flow.go
+++ b/client/internal/auth/pkce_flow.go
@@ -101,7 +101,12 @@ func (p *PKCEAuthorizationFlow) RequestAuthInfo(ctx context.Context) (AuthFlowIn
 		oauth2.SetAuthURLParam("audience", p.providerConfig.Audience),
 	}
 	if !p.providerConfig.DisablePromptLogin {
-		params = append(params, oauth2.SetAuthURLParam("prompt", "login"))
+		if p.providerConfig.LoginFlag.IsPromptLogin() {
+			params = append(params, oauth2.SetAuthURLParam("prompt", "login"))
+		}
+		if p.providerConfig.LoginFlag.IsMaxAge0Login() {
+			params = append(params, oauth2.SetAuthURLParam("max_age", "0"))
+		}
 	}
 
 	authURL := p.oAuthConfig.AuthCodeURL(state, params...)
diff --git a/client/internal/config.go b/client/internal/config.go
index b2f96cbd..45a7620e 100644
--- a/client/internal/config.go
+++ b/client/internal/config.go
@@ -68,12 +68,14 @@ type ConfigInput struct {
 	DisableServerRoutes *bool
 	DisableDNS          *bool
 	DisableFirewall     *bool
-
-	BlockLANAccess *bool
+	BlockLANAccess      *bool
+	BlockInbound        *bool
 
 	DisableNotifications *bool
 
 	DNSLabels domain.List
+
+	LazyConnectionEnabled *bool
 }
 
 // Config Configuration type
@@ -96,8 +98,8 @@ type Config struct {
 	DisableServerRoutes bool
 	DisableDNS          bool
 	DisableFirewall     bool
-
-	BlockLANAccess bool
+	BlockLANAccess      bool
+	BlockInbound        bool
 
 	DisableNotifications *bool
 
@@ -138,6 +140,8 @@ type Config struct {
 	ClientCertKeyPath string
 
 	ClientCertKeyPair *tls.Certificate `json:"-"`
+
+	LazyConnectionEnabled bool
 }
 
 // ReadConfig read config file and return with Config. If it is not exists create a new with default values
@@ -479,6 +483,16 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) {
 		updated = true
 	}
 
+	if input.BlockInbound != nil && *input.BlockInbound != config.BlockInbound {
+		if *input.BlockInbound {
+			log.Infof("blocking inbound connections")
+		} else {
+			log.Infof("allowing inbound connections")
+		}
+		config.BlockInbound = *input.BlockInbound
+		updated = true
+	}
+
 	if input.DisableNotifications != nil && input.DisableNotifications != config.DisableNotifications {
 		if *input.DisableNotifications {
 			log.Infof("disabling notifications")
@@ -524,6 +538,12 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) {
 		updated = true
 	}
 
+	if input.LazyConnectionEnabled != nil && *input.LazyConnectionEnabled != config.LazyConnectionEnabled {
+		log.Infof("switching lazy connection to %t", *input.LazyConnectionEnabled)
+		config.LazyConnectionEnabled = *input.LazyConnectionEnabled
+		updated = true
+	}
+
 	return updated, nil
 }
 
diff --git a/client/internal/conn_mgr.go b/client/internal/conn_mgr.go
new file mode 100644
index 00000000..f7b1f6a0
--- /dev/null
+++ b/client/internal/conn_mgr.go
@@ -0,0 +1,303 @@
+package internal
+
+import (
+	"context"
+	"os"
+	"strconv"
+	"sync"
+	"time"
+
+	log "github.com/sirupsen/logrus"
+
+	"github.com/netbirdio/netbird/client/internal/lazyconn"
+	"github.com/netbirdio/netbird/client/internal/lazyconn/manager"
+	"github.com/netbirdio/netbird/client/internal/peer"
+	"github.com/netbirdio/netbird/client/internal/peer/dispatcher"
+	"github.com/netbirdio/netbird/client/internal/peerstore"
+)
+
+// ConnMgr coordinates both lazy connections (established on-demand) and permanent peer connections.
+//
+// The connection manager is responsible for:
+// - Managing lazy connections via the lazyConnManager
+// - Maintaining a list of excluded peers that should always have permanent connections
+// - Handling connection establishment based on peer signaling
+//
+// The implementation is not thread-safe; it is protected by engine.syncMsgMux.
+type ConnMgr struct {
+	peerStore      *peerstore.Store
+	statusRecorder *peer.Status
+	iface          lazyconn.WGIface
+	dispatcher     *dispatcher.ConnectionDispatcher
+	enabledLocally bool
+
+	lazyConnMgr *manager.Manager
+
+	wg        sync.WaitGroup
+	ctx       context.Context
+	ctxCancel context.CancelFunc
+}
+
+func NewConnMgr(engineConfig *EngineConfig, statusRecorder *peer.Status, peerStore *peerstore.Store, iface lazyconn.WGIface, dispatcher *dispatcher.ConnectionDispatcher) *ConnMgr {
+	e := &ConnMgr{
+		peerStore:      peerStore,
+		statusRecorder: statusRecorder,
+		iface:          iface,
+		dispatcher:     dispatcher,
+	}
+	if engineConfig.LazyConnectionEnabled || lazyconn.IsLazyConnEnabledByEnv() {
+		e.enabledLocally = true
+	}
+	return e
+}
+
+// Start initializes the connection manager and starts the lazy connection manager if enabled by env var or cmd line option.
+func (e *ConnMgr) Start(ctx context.Context) {
+	if e.lazyConnMgr != nil {
+		log.Errorf("lazy connection manager is already started")
+		return
+	}
+
+	if !e.enabledLocally {
+		log.Infof("lazy connection manager is disabled")
+		return
+	}
+
+	e.initLazyManager(ctx)
+	e.statusRecorder.UpdateLazyConnection(true)
+}
+
+// UpdatedRemoteFeatureFlag is called when the remote feature flag is updated.
+// If enabled, it initializes the lazy connection manager and start it. Do not need to call Start() again.
+// If disabled, then it closes the lazy connection manager and open the connections to all peers.
+func (e *ConnMgr) UpdatedRemoteFeatureFlag(ctx context.Context, enabled bool) error {
+	// do not disable lazy connection manager if it was enabled by env var
+	if e.enabledLocally {
+		return nil
+	}
+
+	if enabled {
+		// if the lazy connection manager is already started, do not start it again
+		if e.lazyConnMgr != nil {
+			return nil
+		}
+
+		log.Infof("lazy connection manager is enabled by management feature flag")
+		e.initLazyManager(ctx)
+		e.statusRecorder.UpdateLazyConnection(true)
+		return e.addPeersToLazyConnManager(ctx)
+	} else {
+		if e.lazyConnMgr == nil {
+			return nil
+		}
+		log.Infof("lazy connection manager is disabled by management feature flag")
+		e.closeManager(ctx)
+		e.statusRecorder.UpdateLazyConnection(false)
+		return nil
+	}
+}
+
+// SetExcludeList sets the list of peer IDs that should always have permanent connections.
+func (e *ConnMgr) SetExcludeList(peerIDs map[string]bool) {
+	if e.lazyConnMgr == nil {
+		return
+	}
+
+	excludedPeers := make([]lazyconn.PeerConfig, 0, len(peerIDs))
+
+	for peerID := range peerIDs {
+		var peerConn *peer.Conn
+		var exists bool
+		if peerConn, exists = e.peerStore.PeerConn(peerID); !exists {
+			log.Warnf("failed to find peer conn for peerID: %s", peerID)
+			continue
+		}
+
+		lazyPeerCfg := lazyconn.PeerConfig{
+			PublicKey:  peerID,
+			AllowedIPs: peerConn.WgConfig().AllowedIps,
+			PeerConnID: peerConn.ConnID(),
+			Log:        peerConn.Log,
+		}
+		excludedPeers = append(excludedPeers, lazyPeerCfg)
+	}
+
+	added := e.lazyConnMgr.ExcludePeer(e.ctx, excludedPeers)
+	for _, peerID := range added {
+		var peerConn *peer.Conn
+		var exists bool
+		if peerConn, exists = e.peerStore.PeerConn(peerID); !exists {
+			// if the peer not exist in the store, it means that the engine will call the AddPeerConn in next step
+			continue
+		}
+
+		peerConn.Log.Infof("peer has been added to lazy connection exclude list, opening permanent connection")
+		if err := peerConn.Open(e.ctx); err != nil {
+			peerConn.Log.Errorf("failed to open connection: %v", err)
+		}
+	}
+}
+
+func (e *ConnMgr) AddPeerConn(ctx context.Context, peerKey string, conn *peer.Conn) (exists bool) {
+	if success := e.peerStore.AddPeerConn(peerKey, conn); !success {
+		return true
+	}
+
+	if !e.isStartedWithLazyMgr() {
+		if err := conn.Open(ctx); err != nil {
+			conn.Log.Errorf("failed to open connection: %v", err)
+		}
+		return
+	}
+
+	if !lazyconn.IsSupported(conn.AgentVersionString()) {
+		conn.Log.Warnf("peer does not support lazy connection (%s), open permanent connection", conn.AgentVersionString())
+		if err := conn.Open(ctx); err != nil {
+			conn.Log.Errorf("failed to open connection: %v", err)
+		}
+		return
+	}
+
+	lazyPeerCfg := lazyconn.PeerConfig{
+		PublicKey:  peerKey,
+		AllowedIPs: conn.WgConfig().AllowedIps,
+		PeerConnID: conn.ConnID(),
+		Log:        conn.Log,
+	}
+	excluded, err := e.lazyConnMgr.AddPeer(lazyPeerCfg)
+	if err != nil {
+		conn.Log.Errorf("failed to add peer to lazyconn manager: %v", err)
+		if err := conn.Open(ctx); err != nil {
+			conn.Log.Errorf("failed to open connection: %v", err)
+		}
+		return
+	}
+
+	if excluded {
+		conn.Log.Infof("peer is on lazy conn manager exclude list, opening connection")
+		if err := conn.Open(ctx); err != nil {
+			conn.Log.Errorf("failed to open connection: %v", err)
+		}
+		return
+	}
+
+	conn.Log.Infof("peer added to lazy conn manager")
+	return
+}
+
+func (e *ConnMgr) RemovePeerConn(peerKey string) {
+	conn, ok := e.peerStore.Remove(peerKey)
+	if !ok {
+		return
+	}
+	defer conn.Close()
+
+	if !e.isStartedWithLazyMgr() {
+		return
+	}
+
+	e.lazyConnMgr.RemovePeer(peerKey)
+	conn.Log.Infof("removed peer from lazy conn manager")
+}
+
+func (e *ConnMgr) OnSignalMsg(ctx context.Context, peerKey string) (*peer.Conn, bool) {
+	conn, ok := e.peerStore.PeerConn(peerKey)
+	if !ok {
+		return nil, false
+	}
+
+	if !e.isStartedWithLazyMgr() {
+		return conn, true
+	}
+
+	if found := e.lazyConnMgr.ActivatePeer(ctx, peerKey); found {
+		conn.Log.Infof("activated peer from inactive state")
+		if err := conn.Open(e.ctx); err != nil {
+			conn.Log.Errorf("failed to open connection: %v", err)
+		}
+	}
+	return conn, true
+}
+
+func (e *ConnMgr) Close() {
+	if !e.isStartedWithLazyMgr() {
+		return
+	}
+
+	e.ctxCancel()
+	e.wg.Wait()
+	e.lazyConnMgr = nil
+}
+
+func (e *ConnMgr) initLazyManager(parentCtx context.Context) {
+	cfg := manager.Config{
+		InactivityThreshold: inactivityThresholdEnv(),
+	}
+	e.lazyConnMgr = manager.NewManager(cfg, e.peerStore, e.iface, e.dispatcher)
+
+	ctx, cancel := context.WithCancel(parentCtx)
+	e.ctx = ctx
+	e.ctxCancel = cancel
+
+	e.wg.Add(1)
+	go func() {
+		defer e.wg.Done()
+		e.lazyConnMgr.Start(ctx)
+	}()
+}
+
+func (e *ConnMgr) addPeersToLazyConnManager(ctx context.Context) error {
+	peers := e.peerStore.PeersPubKey()
+	lazyPeerCfgs := make([]lazyconn.PeerConfig, 0, len(peers))
+	for _, peerID := range peers {
+		var peerConn *peer.Conn
+		var exists bool
+		if peerConn, exists = e.peerStore.PeerConn(peerID); !exists {
+			log.Warnf("failed to find peer conn for peerID: %s", peerID)
+			continue
+		}
+
+		lazyPeerCfg := lazyconn.PeerConfig{
+			PublicKey:  peerID,
+			AllowedIPs: peerConn.WgConfig().AllowedIps,
+			PeerConnID: peerConn.ConnID(),
+			Log:        peerConn.Log,
+		}
+		lazyPeerCfgs = append(lazyPeerCfgs, lazyPeerCfg)
+	}
+
+	return e.lazyConnMgr.AddActivePeers(ctx, lazyPeerCfgs)
+}
+
+func (e *ConnMgr) closeManager(ctx context.Context) {
+	if e.lazyConnMgr == nil {
+		return
+	}
+
+	e.ctxCancel()
+	e.wg.Wait()
+	e.lazyConnMgr = nil
+
+	for _, peerID := range e.peerStore.PeersPubKey() {
+		e.peerStore.PeerConnOpen(ctx, peerID)
+	}
+}
+
+func (e *ConnMgr) isStartedWithLazyMgr() bool {
+	return e.lazyConnMgr != nil && e.ctxCancel != nil
+}
+
+func inactivityThresholdEnv() *time.Duration {
+	envValue := os.Getenv(lazyconn.EnvInactivityThreshold)
+	if envValue == "" {
+		return nil
+	}
+
+	parsedMinutes, err := strconv.Atoi(envValue)
+	if err != nil || parsedMinutes <= 0 {
+		return nil
+	}
+
+	d := time.Duration(parsedMinutes) * time.Minute
+	return &d
+}
diff --git a/client/internal/connect.go b/client/internal/connect.go
index 832d58dc..1cfef77f 100644
--- a/client/internal/connect.go
+++ b/client/internal/connect.go
@@ -436,11 +436,13 @@ func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.Pe
 		DNSRouteInterval:     config.DNSRouteInterval,
 
 		DisableClientRoutes: config.DisableClientRoutes,
-		DisableServerRoutes: config.DisableServerRoutes,
+		DisableServerRoutes: config.DisableServerRoutes || config.BlockInbound,
 		DisableDNS:          config.DisableDNS,
 		DisableFirewall:     config.DisableFirewall,
+		BlockLANAccess:      config.BlockLANAccess,
+		BlockInbound:        config.BlockInbound,
 
-		BlockLANAccess: config.BlockLANAccess,
+		LazyConnectionEnabled: config.LazyConnectionEnabled,
 	}
 
 	if config.PreSharedKey != "" {
@@ -481,7 +483,7 @@ func connectToSignal(ctx context.Context, wtConfig *mgmProto.NetbirdConfig, ourP
 	return signalClient, nil
 }
 
-// loginToManagement creates Management Services client, establishes a connection, logs-in and gets a global Netbird config (signal, turn, stun hosts, etc)
+// loginToManagement creates Management ServiceDependencies client, establishes a connection, logs-in and gets a global Netbird config (signal, turn, stun hosts, etc)
 func loginToManagement(ctx context.Context, client mgm.Client, pubSSHKey []byte, config *Config) (*mgmProto.LoginResponse, error) {
 
 	serverPublicKey, err := client.GetServerPublicKey()
diff --git a/client/internal/debug/debug.go b/client/internal/debug/debug.go
index e07f981f..dfed47f0 100644
--- a/client/internal/debug/debug.go
+++ b/client/internal/debug/debug.go
@@ -4,6 +4,7 @@ import (
 	"archive/zip"
 	"bufio"
 	"bytes"
+	"compress/gzip"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -269,11 +270,21 @@ func (g *BundleGenerator) createArchive() error {
 		log.Errorf("Failed to add corrupted state files to debug bundle: %v", err)
 	}
 
-	if g.logFile != "console" {
+	if err := g.addWgShow(); err != nil {
+		log.Errorf("Failed to add wg show output: %v", err)
+	}
+
+	if g.logFile != "console" && g.logFile != "" {
 		if err := g.addLogfile(); err != nil {
-			return fmt.Errorf("add log file: %w", err)
+			log.Errorf("Failed to add log file to debug bundle: %v", err)
+			if err := g.trySystemdLogFallback(); err != nil {
+				log.Errorf("Failed to add systemd logs as fallback: %v", err)
+			}
 		}
+	} else if err := g.trySystemdLogFallback(); err != nil {
+		log.Errorf("Failed to add systemd logs: %v", err)
 	}
+
 	return nil
 }
 
@@ -365,17 +376,34 @@ func (g *BundleGenerator) addCommonConfigFields(configContent *strings.Builder)
 	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("ServerSSHAllowed: %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("DisableServerRoutes: %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))
+	configContent.WriteString(fmt.Sprintf("BlockInbound: %v\n", g.internalConfig.BlockInbound))
+
+	if g.internalConfig.DisableNotifications != nil {
+		configContent.WriteString(fmt.Sprintf("DisableNotifications: %v\n", *g.internalConfig.DisableNotifications))
+	}
+
+	configContent.WriteString(fmt.Sprintf("DNSLabels: %v\n", g.internalConfig.DNSLabels))
+
+	configContent.WriteString(fmt.Sprintf("DisableAutoConnect: %v\n", g.internalConfig.DisableAutoConnect))
+
+	configContent.WriteString(fmt.Sprintf("DNSRouteInterval: %s\n", g.internalConfig.DNSRouteInterval))
+
+	if g.internalConfig.ClientCertPath != "" {
+		configContent.WriteString(fmt.Sprintf("ClientCertPath: %s\n", g.internalConfig.ClientCertPath))
+	}
+	if g.internalConfig.ClientCertKeyPath != "" {
+		configContent.WriteString(fmt.Sprintf("ClientCertKeyPath: %s\n", g.internalConfig.ClientCertKeyPath))
+	}
+
+	configContent.WriteString(fmt.Sprintf("LazyConnectionEnabled: %v\n", g.internalConfig.LazyConnectionEnabled))
 }
 
 func (g *BundleGenerator) addProf() (err error) {
@@ -533,6 +561,33 @@ func (g *BundleGenerator) addLogfile() error {
 		return fmt.Errorf("add client log file to zip: %w", err)
 	}
 
+	// add latest rotated log file
+	pattern := filepath.Join(logDir, "client-*.log.gz")
+	files, err := filepath.Glob(pattern)
+	if err != nil {
+		log.Warnf("failed to glob rotated logs: %v", err)
+	} else if len(files) > 0 {
+		// pick the file with the latest ModTime
+		sort.Slice(files, func(i, j int) bool {
+			fi, err := os.Stat(files[i])
+			if err != nil {
+				log.Warnf("failed to stat rotated log %s: %v", files[i], err)
+				return false
+			}
+			fj, err := os.Stat(files[j])
+			if err != nil {
+				log.Warnf("failed to stat rotated log %s: %v", files[j], err)
+				return false
+			}
+			return fi.ModTime().Before(fj.ModTime())
+		})
+		latest := files[len(files)-1]
+		name := filepath.Base(latest)
+		if err := g.addSingleLogFileGz(latest, name); err != nil {
+			log.Warnf("failed to add rotated log %s: %v", name, err)
+		}
+	}
+
 	stdErrLogPath := filepath.Join(logDir, errorLogFile)
 	stdoutLogPath := filepath.Join(logDir, stdoutLogFile)
 	if runtime.GOOS == "darwin" {
@@ -563,16 +618,13 @@ func (g *BundleGenerator) addSingleLogfile(logPath, targetName string) error {
 		}
 	}()
 
-	var logReader io.Reader
+	var logReader io.Reader = logFile
 	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)
 	}
@@ -580,6 +632,44 @@ func (g *BundleGenerator) addSingleLogfile(logPath, targetName string) error {
 	return nil
 }
 
+// addSingleLogFileGz adds a single gzipped log file to the archive
+func (g *BundleGenerator) addSingleLogFileGz(logPath, targetName string) error {
+	f, err := os.Open(logPath)
+	if err != nil {
+		return fmt.Errorf("open gz log file %s: %w", targetName, err)
+	}
+	defer f.Close()
+
+	gzr, err := gzip.NewReader(f)
+	if err != nil {
+		return fmt.Errorf("create gzip reader: %w", err)
+	}
+	defer gzr.Close()
+
+	var logReader io.Reader = gzr
+	if g.anonymize {
+		var pw *io.PipeWriter
+		logReader, pw = io.Pipe()
+		go anonymizeLog(gzr, pw, g.anonymizer)
+	}
+
+	var buf bytes.Buffer
+	gw := gzip.NewWriter(&buf)
+	if _, err := io.Copy(gw, logReader); err != nil {
+		return fmt.Errorf("re-gzip: %w", err)
+	}
+
+	if err := gw.Close(); err != nil {
+		return fmt.Errorf("close gzip writer: %w", err)
+	}
+
+	if err := g.addFileToZip(&buf, targetName); err != nil {
+		return fmt.Errorf("add anonymized gz: %w", err)
+	}
+
+	return nil
+}
+
 func (g *BundleGenerator) addFileToZip(reader io.Reader, filename string) error {
 	header := &zip.FileHeader{
 		Name:     filename,
diff --git a/client/internal/debug/debug_linux.go b/client/internal/debug/debug_linux.go
index 291531fe..4626cd9a 100644
--- a/client/internal/debug/debug_linux.go
+++ b/client/internal/debug/debug_linux.go
@@ -4,17 +4,104 @@ package debug
 
 import (
 	"bytes"
+	"context"
 	"encoding/binary"
+	"errors"
 	"fmt"
+	"os"
 	"os/exec"
 	"sort"
 	"strings"
+	"time"
 
 	"github.com/google/nftables"
 	"github.com/google/nftables/expr"
 	log "github.com/sirupsen/logrus"
 )
 
+const (
+	maxLogEntries = 100000
+	maxLogAge     = 7 * 24 * time.Hour // Last 7 days
+)
+
+// trySystemdLogFallback attempts to get logs from systemd journal as fallback
+func (g *BundleGenerator) trySystemdLogFallback() error {
+	log.Debug("Attempting to collect systemd journal logs")
+
+	serviceName := getServiceName()
+	journalLogs, err := getSystemdLogs(serviceName)
+	if err != nil {
+		return fmt.Errorf("get systemd logs for %s: %w", serviceName, err)
+	}
+
+	if strings.Contains(journalLogs, "No recent log entries found") {
+		log.Debug("No recent log entries found in systemd journal")
+		return nil
+	}
+
+	if g.anonymize {
+		journalLogs = g.anonymizer.AnonymizeString(journalLogs)
+	}
+
+	logReader := strings.NewReader(journalLogs)
+	fileName := fmt.Sprintf("systemd-%s.log", serviceName)
+	if err := g.addFileToZip(logReader, fileName); err != nil {
+		return fmt.Errorf("add systemd logs to bundle: %w", err)
+	}
+
+	log.Infof("Added systemd journal logs for %s to debug bundle", serviceName)
+	return nil
+}
+
+// getServiceName gets the service name from environment or defaults to netbird
+func getServiceName() string {
+	if unitName := os.Getenv("SYSTEMD_UNIT"); unitName != "" {
+		log.Debugf("Detected SYSTEMD_UNIT environment variable: %s", unitName)
+		return unitName
+	}
+
+	return "netbird"
+}
+
+// getSystemdLogs retrieves logs from systemd journal for a specific service using journalctl
+func getSystemdLogs(serviceName string) (string, error) {
+	args := []string{
+		"-u", fmt.Sprintf("%s.service", serviceName),
+		"--since", fmt.Sprintf("-%s", maxLogAge.String()),
+		"--lines", fmt.Sprintf("%d", maxLogEntries),
+		"--no-pager",
+		"--output", "short-iso",
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer cancel()
+
+	cmd := exec.CommandContext(ctx, "journalctl", args...)
+	var stdout, stderr bytes.Buffer
+	cmd.Stdout = &stdout
+	cmd.Stderr = &stderr
+
+	if err := cmd.Run(); err != nil {
+		if errors.Is(ctx.Err(), context.DeadlineExceeded) {
+			return "", fmt.Errorf("journalctl command timed out after 30 seconds")
+		}
+		if strings.Contains(err.Error(), "executable file not found") {
+			return "", fmt.Errorf("journalctl command not found: %w", err)
+		}
+		return "", fmt.Errorf("execute journalctl: %w (stderr: %s)", err, stderr.String())
+	}
+
+	logs := stdout.String()
+	if strings.TrimSpace(logs) == "" {
+		return "No recent log entries found in systemd journal", nil
+	}
+
+	header := fmt.Sprintf("=== Systemd Journal Logs for %s.service (last %d entries, max %s) ===\n",
+		serviceName, maxLogEntries, maxLogAge.String())
+
+	return header + logs, nil
+}
+
 // addFirewallRules collects and adds firewall rules to the archive
 func (g *BundleGenerator) addFirewallRules() error {
 	log.Info("Collecting firewall rules")
@@ -59,6 +146,16 @@ func collectIPTablesRules() (string, error) {
 		builder.WriteString("\n")
 	}
 
+	// 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")
 
 	tables := []string{"filter", "nat", "mangle", "raw", "security"}
@@ -78,6 +175,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")
@@ -449,7 +568,7 @@ func formatExpr(exp expr.Any) string {
 	case *expr.Fib:
 		return formatFib(e)
 	case *expr.Target:
-		return fmt.Sprintf("jump %s", e.Name) // Properly format jump targets
+		return fmt.Sprintf("jump %s", e.Name)
 	case *expr.Immediate:
 		if e.Register == 1 {
 			return formatImmediateData(e.Data)
diff --git a/client/internal/debug/debug_nonlinux.go b/client/internal/debug/debug_nonlinux.go
index ef93620a..b0ff5561 100644
--- a/client/internal/debug/debug_nonlinux.go
+++ b/client/internal/debug/debug_nonlinux.go
@@ -6,3 +6,9 @@ package debug
 func (g *BundleGenerator) addFirewallRules() error {
 	return nil
 }
+
+func (g *BundleGenerator) trySystemdLogFallback() error {
+	// Systemd is only available on Linux
+	// TODO: Add BSD support
+	return nil
+}
diff --git a/client/internal/debug/wgshow.go b/client/internal/debug/wgshow.go
new file mode 100644
index 00000000..e4b4c236
--- /dev/null
+++ b/client/internal/debug/wgshow.go
@@ -0,0 +1,66 @@
+package debug
+
+import (
+	"bytes"
+	"fmt"
+	"strings"
+	"time"
+
+	"github.com/netbirdio/netbird/client/iface/configurer"
+)
+
+type WGIface interface {
+	FullStats() (*configurer.Stats, error)
+}
+
+func (g *BundleGenerator) addWgShow() error {
+	result, err := g.statusRecorder.PeersStatus()
+	if err != nil {
+		return err
+	}
+
+	output := g.toWGShowFormat(result)
+	reader := bytes.NewReader([]byte(output))
+
+	if err := g.addFileToZip(reader, "wgshow.txt"); err != nil {
+		return fmt.Errorf("add wg show to zip: %w", err)
+	}
+	return nil
+}
+
+func (g *BundleGenerator) toWGShowFormat(s *configurer.Stats) string {
+	var sb strings.Builder
+
+	sb.WriteString(fmt.Sprintf("interface: %s\n", s.DeviceName))
+	sb.WriteString(fmt.Sprintf("  public key: %s\n", s.PublicKey))
+	sb.WriteString(fmt.Sprintf("  listen port: %d\n", s.ListenPort))
+	if s.FWMark != 0 {
+		sb.WriteString(fmt.Sprintf("  fwmark: %#x\n", s.FWMark))
+	}
+
+	for _, peer := range s.Peers {
+		sb.WriteString(fmt.Sprintf("\npeer: %s\n", peer.PublicKey))
+		if peer.Endpoint.IP != nil {
+			if g.anonymize {
+				anonEndpoint := g.anonymizer.AnonymizeUDPAddr(peer.Endpoint)
+				sb.WriteString(fmt.Sprintf("  endpoint: %s\n", anonEndpoint.String()))
+			} else {
+				sb.WriteString(fmt.Sprintf("  endpoint: %s\n", peer.Endpoint.String()))
+			}
+		}
+		if len(peer.AllowedIPs) > 0 {
+			var ipStrings []string
+			for _, ipnet := range peer.AllowedIPs {
+				ipStrings = append(ipStrings, ipnet.String())
+			}
+			sb.WriteString(fmt.Sprintf("  allowed ips: %s\n", strings.Join(ipStrings, ", ")))
+		}
+		sb.WriteString(fmt.Sprintf("  latest handshake: %s\n", peer.LastHandshake.Format(time.RFC1123)))
+		sb.WriteString(fmt.Sprintf("  transfer: %d B received, %d B sent\n", peer.RxBytes, peer.TxBytes))
+		if peer.PresharedKey {
+			sb.WriteString("  preshared key: (hidden)\n")
+		}
+	}
+
+	return sb.String()
+}
diff --git a/client/internal/dns.go b/client/internal/dns.go
index 8a73f50f..5e604bec 100644
--- a/client/internal/dns.go
+++ b/client/internal/dns.go
@@ -2,7 +2,7 @@ package internal
 
 import (
 	"fmt"
-	"net"
+	"net/netip"
 	"slices"
 	"strings"
 
@@ -12,13 +12,14 @@ import (
 	nbdns "github.com/netbirdio/netbird/dns"
 )
 
-func createPTRRecord(aRecord nbdns.SimpleRecord, ipNet *net.IPNet) (nbdns.SimpleRecord, bool) {
-	ip := net.ParseIP(aRecord.RData)
-	if ip == nil || ip.To4() == nil {
+func createPTRRecord(aRecord nbdns.SimpleRecord, prefix netip.Prefix) (nbdns.SimpleRecord, bool) {
+	ip, err := netip.ParseAddr(aRecord.RData)
+	if err != nil {
+		log.Warnf("failed to parse IP address %s: %v", aRecord.RData, err)
 		return nbdns.SimpleRecord{}, false
 	}
 
-	if !ipNet.Contains(ip) {
+	if !prefix.Contains(ip) {
 		return nbdns.SimpleRecord{}, false
 	}
 
@@ -36,16 +37,19 @@ func createPTRRecord(aRecord nbdns.SimpleRecord, ipNet *net.IPNet) (nbdns.Simple
 }
 
 // generateReverseZoneName creates the reverse DNS zone name for a given network
-func generateReverseZoneName(ipNet *net.IPNet) (string, error) {
-	networkIP := ipNet.IP.Mask(ipNet.Mask)
-	maskOnes, _ := ipNet.Mask.Size()
+func generateReverseZoneName(network netip.Prefix) (string, error) {
+	networkIP := network.Masked().Addr()
+
+	if !networkIP.Is4() {
+		return "", fmt.Errorf("reverse DNS is only supported for IPv4 networks, got: %s", networkIP)
+	}
 
 	// round up to nearest byte
-	octetsToUse := (maskOnes + 7) / 8
+	octetsToUse := (network.Bits() + 7) / 8
 
 	octets := strings.Split(networkIP.String(), ".")
 	if octetsToUse > len(octets) {
-		return "", fmt.Errorf("invalid network mask size for reverse DNS: %d", maskOnes)
+		return "", fmt.Errorf("invalid network mask size for reverse DNS: %d", network.Bits())
 	}
 
 	reverseOctets := make([]string, octetsToUse)
@@ -68,7 +72,7 @@ func zoneExists(config *nbdns.Config, zoneName string) bool {
 }
 
 // collectPTRRecords gathers all PTR records for the given network from A records
-func collectPTRRecords(config *nbdns.Config, ipNet *net.IPNet) []nbdns.SimpleRecord {
+func collectPTRRecords(config *nbdns.Config, prefix netip.Prefix) []nbdns.SimpleRecord {
 	var records []nbdns.SimpleRecord
 
 	for _, zone := range config.CustomZones {
@@ -77,7 +81,7 @@ func collectPTRRecords(config *nbdns.Config, ipNet *net.IPNet) []nbdns.SimpleRec
 				continue
 			}
 
-			if ptrRecord, ok := createPTRRecord(record, ipNet); ok {
+			if ptrRecord, ok := createPTRRecord(record, prefix); ok {
 				records = append(records, ptrRecord)
 			}
 		}
@@ -87,8 +91,8 @@ func collectPTRRecords(config *nbdns.Config, ipNet *net.IPNet) []nbdns.SimpleRec
 }
 
 // addReverseZone adds a reverse DNS zone to the configuration for the given network
-func addReverseZone(config *nbdns.Config, ipNet *net.IPNet) {
-	zoneName, err := generateReverseZoneName(ipNet)
+func addReverseZone(config *nbdns.Config, network netip.Prefix) {
+	zoneName, err := generateReverseZoneName(network)
 	if err != nil {
 		log.Warn(err)
 		return
@@ -99,7 +103,7 @@ func addReverseZone(config *nbdns.Config, ipNet *net.IPNet) {
 		return
 	}
 
-	records := collectPTRRecords(config, ipNet)
+	records := collectPTRRecords(config, network)
 
 	reverseZone := nbdns.CustomZone{
 		Domain:  zoneName,
diff --git a/client/internal/dns/handler_chain.go b/client/internal/dns/handler_chain.go
index 6baf9ed9..22caaa76 100644
--- a/client/internal/dns/handler_chain.go
+++ b/client/internal/dns/handler_chain.go
@@ -1,6 +1,7 @@
 package dns
 
 import (
+	"fmt"
 	"slices"
 	"strings"
 	"sync"
@@ -148,61 +149,42 @@ func (c *HandlerChain) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
 	}
 
 	qname := strings.ToLower(r.Question[0].Name)
-	log.Tracef("handling DNS request for domain=%s", qname)
 
 	c.mu.RLock()
 	handlers := slices.Clone(c.handlers)
 	c.mu.RUnlock()
 
 	if log.IsLevelEnabled(log.TraceLevel) {
-		log.Tracef("current handlers (%d):", len(handlers))
+		var b strings.Builder
+		b.WriteString(fmt.Sprintf("DNS request domain=%s, handlers (%d):\n", qname, len(handlers)))
 		for _, h := range handlers {
-			log.Tracef("  - pattern: domain=%s original: domain=%s wildcard=%v match_subdomain=%v priority=%d",
-				h.Pattern, h.OrigPattern, h.IsWildcard, h.MatchSubdomains, h.Priority)
+			b.WriteString(fmt.Sprintf("  - pattern: domain=%s original: domain=%s wildcard=%v match_subdomain=%v priority=%d\n",
+				h.Pattern, h.OrigPattern, h.IsWildcard, h.MatchSubdomains, h.Priority))
 		}
+		log.Trace(strings.TrimSuffix(b.String(), "\n"))
 	}
 
 	// Try handlers in priority order
 	for _, entry := range handlers {
-		var matched bool
-		switch {
-		case entry.Pattern == ".":
-			matched = true
-		case entry.IsWildcard:
-			parts := strings.Split(strings.TrimSuffix(qname, entry.Pattern), ".")
-			matched = len(parts) >= 2 && strings.HasSuffix(qname, entry.Pattern)
-		default:
-			// For non-wildcard patterns:
-			// If handler wants subdomain matching, allow suffix match
-			// Otherwise require exact match
-			if entry.MatchSubdomains {
-				matched = strings.EqualFold(qname, entry.Pattern) || strings.HasSuffix(qname, "."+entry.Pattern)
-			} else {
-				matched = strings.EqualFold(qname, entry.Pattern)
-			}
-		}
-
-		if !matched {
-			log.Tracef("trying domain match: request: domain=%s pattern: domain=%s wildcard=%v match_subdomain=%v priority=%d matched=false",
-				qname, entry.OrigPattern, entry.MatchSubdomains, entry.IsWildcard, entry.Priority)
-			continue
-		}
+		matched := c.isHandlerMatch(qname, entry)
 
-		log.Tracef("handler matched: request: domain=%s pattern: domain=%s wildcard=%v match_subdomain=%v priority=%d",
-			qname, entry.OrigPattern, entry.IsWildcard, entry.MatchSubdomains, entry.Priority)
+		if matched {
+			log.Tracef("handler matched: domain=%s -> pattern=%s wildcard=%v match_subdomain=%v priority=%d",
+				qname, entry.OrigPattern, entry.IsWildcard, entry.MatchSubdomains, entry.Priority)
 
-		chainWriter := &ResponseWriterChain{
-			ResponseWriter: w,
-			origPattern:    entry.OrigPattern,
-		}
-		entry.Handler.ServeDNS(chainWriter, r)
+			chainWriter := &ResponseWriterChain{
+				ResponseWriter: w,
+				origPattern:    entry.OrigPattern,
+			}
+			entry.Handler.ServeDNS(chainWriter, r)
 
-		// If handler wants to continue, try next handler
-		if chainWriter.shouldContinue {
-			log.Tracef("handler requested continue to next handler")
-			continue
+			// If handler wants to continue, try next handler
+			if chainWriter.shouldContinue {
+				log.Tracef("handler requested continue to next handler for domain=%s", qname)
+				continue
+			}
+			return
 		}
-		return
 	}
 
 	// No handler matched or all handlers passed
@@ -213,3 +195,22 @@ func (c *HandlerChain) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
 		log.Errorf("failed to write DNS response: %v", err)
 	}
 }
+
+func (c *HandlerChain) isHandlerMatch(qname string, entry HandlerEntry) bool {
+	switch {
+	case entry.Pattern == ".":
+		return true
+	case entry.IsWildcard:
+		parts := strings.Split(strings.TrimSuffix(qname, entry.Pattern), ".")
+		return len(parts) >= 2 && strings.HasSuffix(qname, entry.Pattern)
+	default:
+		// For non-wildcard patterns:
+		// If handler wants subdomain matching, allow suffix match
+		// Otherwise require exact match
+		if entry.MatchSubdomains {
+			return strings.EqualFold(qname, entry.Pattern) || strings.HasSuffix(qname, "."+entry.Pattern)
+		} else {
+			return strings.EqualFold(qname, entry.Pattern)
+		}
+	}
+}
diff --git a/client/internal/dns/host_windows.go b/client/internal/dns/host_windows.go
index cfba2950..f8939328 100644
--- a/client/internal/dns/host_windows.go
+++ b/client/internal/dns/host_windows.go
@@ -1,11 +1,14 @@
 package dns
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"io"
+	"os/exec"
 	"strings"
 	"syscall"
+	"time"
 
 	"github.com/hashicorp/go-multierror"
 	log "github.com/sirupsen/logrus"
@@ -41,6 +44,20 @@ const (
 	interfaceConfigNameServerKey = "NameServer"
 	interfaceConfigSearchListKey = "SearchList"
 
+	// Network interface DNS registration settings
+	disableDynamicUpdateKey           = "DisableDynamicUpdate"
+	registrationEnabledKey            = "RegistrationEnabled"
+	maxNumberOfAddressesToRegisterKey = "MaxNumberOfAddressesToRegister"
+
+	// NetBIOS/WINS settings
+	netbtInterfacePath = `SYSTEM\CurrentControlSet\Services\NetBT\Parameters\Interfaces`
+	netbiosOptionsKey  = "NetbiosOptions"
+
+	// NetBIOS option values: 0 = from DHCP, 1 = enabled, 2 = disabled
+	netbiosFromDHCP = 0
+	netbiosEnabled  = 1
+	netbiosDisabled = 2
+
 	// RP_FORCE: Reapply all policies even if no policy change was detected
 	rpForce = 0x1
 )
@@ -67,16 +84,85 @@ func newHostManager(wgInterface WGIface) (*registryConfigurator, error) {
 		log.Infof("detected GPO DNS policy configuration, using policy store")
 	}
 
-	return &registryConfigurator{
+	configurator := &registryConfigurator{
 		guid: guid,
 		gpo:  useGPO,
-	}, nil
+	}
+
+	if err := configurator.configureInterface(); err != nil {
+		log.Errorf("failed to configure interface settings: %v", err)
+	}
+
+	return configurator, nil
 }
 
 func (r *registryConfigurator) supportCustomPort() bool {
 	return false
 }
 
+func (r *registryConfigurator) configureInterface() error {
+	var merr *multierror.Error
+
+	if err := r.disableDNSRegistrationForInterface(); err != nil {
+		merr = multierror.Append(merr, fmt.Errorf("disable DNS registration: %w", err))
+	}
+
+	if err := r.disableWINSForInterface(); err != nil {
+		merr = multierror.Append(merr, fmt.Errorf("disable WINS: %w", err))
+	}
+
+	return nberrors.FormatErrorOrNil(merr)
+}
+
+func (r *registryConfigurator) disableDNSRegistrationForInterface() error {
+	regKey, err := r.getInterfaceRegistryKey()
+	if err != nil {
+		return fmt.Errorf("get interface registry key: %w", err)
+	}
+	defer closer(regKey)
+
+	var merr *multierror.Error
+
+	if err := regKey.SetDWordValue(disableDynamicUpdateKey, 1); err != nil {
+		merr = multierror.Append(merr, fmt.Errorf("set %s: %w", disableDynamicUpdateKey, err))
+	}
+
+	if err := regKey.SetDWordValue(registrationEnabledKey, 0); err != nil {
+		merr = multierror.Append(merr, fmt.Errorf("set %s: %w", registrationEnabledKey, err))
+	}
+
+	if err := regKey.SetDWordValue(maxNumberOfAddressesToRegisterKey, 0); err != nil {
+		merr = multierror.Append(merr, fmt.Errorf("set %s: %w", maxNumberOfAddressesToRegisterKey, err))
+	}
+
+	if merr == nil || len(merr.Errors) == 0 {
+		log.Infof("disabled DNS registration for interface %s", r.guid)
+	}
+
+	return nberrors.FormatErrorOrNil(merr)
+}
+
+func (r *registryConfigurator) disableWINSForInterface() error {
+	netbtKeyPath := fmt.Sprintf(`%s\Tcpip_%s`, netbtInterfacePath, r.guid)
+
+	regKey, err := registry.OpenKey(registry.LOCAL_MACHINE, netbtKeyPath, registry.SET_VALUE)
+	if err != nil {
+		regKey, _, err = registry.CreateKey(registry.LOCAL_MACHINE, netbtKeyPath, registry.SET_VALUE)
+		if err != nil {
+			return fmt.Errorf("create NetBT interface key %s: %w", netbtKeyPath, err)
+		}
+	}
+	defer closer(regKey)
+
+	// NetbiosOptions: 2 = disabled
+	if err := regKey.SetDWordValue(netbiosOptionsKey, netbiosDisabled); err != nil {
+		return fmt.Errorf("set %s: %w", netbiosOptionsKey, err)
+	}
+
+	log.Infof("disabled WINS/NetBIOS for interface %s", r.guid)
+	return nil
+}
+
 func (r *registryConfigurator) applyDNSConfig(config HostDNSConfig, stateManager *statemanager.Manager) error {
 	if config.RouteAll {
 		if err := r.addDNSSetupForAll(config.ServerIP); err != nil {
@@ -119,9 +205,7 @@ func (r *registryConfigurator) applyDNSConfig(config HostDNSConfig, stateManager
 		return fmt.Errorf("update search domains: %w", err)
 	}
 
-	if err := r.flushDNSCache(); err != nil {
-		log.Errorf("failed to flush DNS cache: %v", err)
-	}
+	go r.flushDNSCache()
 
 	return nil
 }
@@ -191,7 +275,25 @@ func (r *registryConfigurator) string() string {
 	return "registry"
 }
 
-func (r *registryConfigurator) flushDNSCache() error {
+func (r *registryConfigurator) registerDNS() {
+	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
+	defer cancel()
+
+	// nolint:misspell
+	cmd := exec.CommandContext(ctx, "ipconfig", "/registerdns")
+	out, err := cmd.CombinedOutput()
+
+	if err != nil {
+		log.Errorf("failed to register DNS: %v, output: %s", err, out)
+		return
+	}
+
+	log.Info("registered DNS names")
+}
+
+func (r *registryConfigurator) flushDNSCache() {
+	r.registerDNS()
+
 	// dnsFlushResolverCacheFn.Call() may panic if the func is not found
 	defer func() {
 		if rec := recover(); rec != nil {
@@ -202,13 +304,14 @@ func (r *registryConfigurator) flushDNSCache() error {
 	ret, _, err := dnsFlushResolverCacheFn.Call()
 	if ret == 0 {
 		if err != nil && !errors.Is(err, syscall.Errno(0)) {
-			return fmt.Errorf("DnsFlushResolverCache failed: %w", err)
+			log.Errorf("DnsFlushResolverCache failed: %v", err)
+			return
 		}
-		return fmt.Errorf("DnsFlushResolverCache failed")
+		log.Errorf("DnsFlushResolverCache failed")
+		return
 	}
 
 	log.Info("flushed DNS cache")
-	return nil
 }
 
 func (r *registryConfigurator) updateSearchDomains(domains []string) error {
@@ -263,9 +366,7 @@ func (r *registryConfigurator) restoreHostDNS() error {
 		return fmt.Errorf("remove interface registry key: %w", err)
 	}
 
-	if err := r.flushDNSCache(); err != nil {
-		log.Errorf("failed to flush DNS cache: %v", err)
-	}
+	go r.flushDNSCache()
 
 	return nil
 }
diff --git a/client/internal/dns/local.go b/client/internal/dns/local.go
deleted file mode 100644
index 76e18e3c..00000000
--- a/client/internal/dns/local.go
+++ /dev/null
@@ -1,130 +0,0 @@
-package dns
-
-import (
-	"fmt"
-	"strings"
-	"sync"
-
-	"github.com/miekg/dns"
-	log "github.com/sirupsen/logrus"
-
-	nbdns "github.com/netbirdio/netbird/dns"
-)
-
-type registrationMap map[string]struct{}
-
-type localResolver struct {
-	registeredMap registrationMap
-	records       sync.Map // key: string (domain_class_type), value: []dns.RR
-}
-
-func (d *localResolver) MatchSubdomains() bool {
-	return true
-}
-
-func (d *localResolver) stop() {
-}
-
-// String returns a string representation of the local resolver
-func (d *localResolver) String() string {
-	return fmt.Sprintf("local resolver [%d records]", len(d.registeredMap))
-}
-
-// ID returns the unique handler ID
-func (d *localResolver) id() handlerID {
-	return "local-resolver"
-}
-
-// ServeDNS handles a DNS request
-func (d *localResolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
-	if len(r.Question) > 0 {
-		log.Tracef("received local question: domain=%s type=%v class=%v", r.Question[0].Name, r.Question[0].Qtype, r.Question[0].Qclass)
-	}
-
-	replyMessage := &dns.Msg{}
-	replyMessage.SetReply(r)
-	replyMessage.RecursionAvailable = true
-
-	// lookup all records matching the question
-	records := d.lookupRecords(r)
-	if len(records) > 0 {
-		replyMessage.Rcode = dns.RcodeSuccess
-		replyMessage.Answer = append(replyMessage.Answer, records...)
-	} else {
-		replyMessage.Rcode = dns.RcodeNameError
-	}
-
-	err := w.WriteMsg(replyMessage)
-	if err != nil {
-		log.Debugf("got an error while writing the local resolver response, error: %v", err)
-	}
-}
-
-// lookupRecords fetches *all* DNS records matching the first question in r.
-func (d *localResolver) lookupRecords(r *dns.Msg) []dns.RR {
-	if len(r.Question) == 0 {
-		return nil
-	}
-	question := r.Question[0]
-	question.Name = strings.ToLower(question.Name)
-	key := buildRecordKey(question.Name, question.Qclass, question.Qtype)
-
-	value, found := d.records.Load(key)
-	if !found {
-		// alternatively check if we have a cname
-		if question.Qtype != dns.TypeCNAME {
-			r.Question[0].Qtype = dns.TypeCNAME
-			return d.lookupRecords(r)
-		}
-
-		return nil
-	}
-
-	records, ok := value.([]dns.RR)
-	if !ok {
-		log.Errorf("failed to cast records to []dns.RR, records: %v", value)
-		return nil
-	}
-
-	// if there's more than one record, rotate them (round-robin)
-	if len(records) > 1 {
-		first := records[0]
-		records = append(records[1:], first)
-		d.records.Store(key, records)
-	}
-
-	return records
-}
-
-// registerRecord stores a new record by appending it to any existing list
-func (d *localResolver) registerRecord(record nbdns.SimpleRecord) (string, error) {
-	rr, err := dns.NewRR(record.String())
-	if err != nil {
-		return "", fmt.Errorf("register record: %w", err)
-	}
-
-	rr.Header().Rdlength = record.Len()
-	header := rr.Header()
-	key := buildRecordKey(header.Name, header.Class, header.Rrtype)
-
-	// load any existing slice of records, then append
-	existing, _ := d.records.LoadOrStore(key, []dns.RR{})
-	records := existing.([]dns.RR)
-	records = append(records, rr)
-
-	// store updated slice
-	d.records.Store(key, records)
-	return key, nil
-}
-
-// deleteRecord removes *all* records under the recordKey.
-func (d *localResolver) deleteRecord(recordKey string) {
-	d.records.Delete(dns.Fqdn(recordKey))
-}
-
-// buildRecordKey consistently generates a key: name_class_type
-func buildRecordKey(name string, class, qType uint16) string {
-	return fmt.Sprintf("%s_%d_%d", dns.Fqdn(name), class, qType)
-}
-
-func (d *localResolver) probeAvailability() {}
diff --git a/client/internal/dns/local/local.go b/client/internal/dns/local/local.go
new file mode 100644
index 00000000..de3d8514
--- /dev/null
+++ b/client/internal/dns/local/local.go
@@ -0,0 +1,149 @@
+package local
+
+import (
+	"fmt"
+	"slices"
+	"strings"
+	"sync"
+
+	"github.com/miekg/dns"
+	log "github.com/sirupsen/logrus"
+	"golang.org/x/exp/maps"
+
+	"github.com/netbirdio/netbird/client/internal/dns/types"
+	nbdns "github.com/netbirdio/netbird/dns"
+)
+
+type Resolver struct {
+	mu      sync.RWMutex
+	records map[dns.Question][]dns.RR
+}
+
+func NewResolver() *Resolver {
+	return &Resolver{
+		records: make(map[dns.Question][]dns.RR),
+	}
+}
+
+func (d *Resolver) MatchSubdomains() bool {
+	return true
+}
+
+// String returns a string representation of the local resolver
+func (d *Resolver) String() string {
+	return fmt.Sprintf("local resolver [%d records]", len(d.records))
+}
+
+func (d *Resolver) Stop() {}
+
+// ID returns the unique handler ID
+func (d *Resolver) ID() types.HandlerID {
+	return "local-resolver"
+}
+
+func (d *Resolver) ProbeAvailability() {}
+
+// ServeDNS handles a DNS request
+func (d *Resolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
+	if len(r.Question) == 0 {
+		log.Debugf("received local resolver request with no question")
+		return
+	}
+	question := r.Question[0]
+	question.Name = strings.ToLower(dns.Fqdn(question.Name))
+
+	log.Tracef("received local question: domain=%s type=%v class=%v", r.Question[0].Name, question.Qtype, question.Qclass)
+
+	replyMessage := &dns.Msg{}
+	replyMessage.SetReply(r)
+	replyMessage.RecursionAvailable = true
+
+	// lookup all records matching the question
+	records := d.lookupRecords(question)
+	if len(records) > 0 {
+		replyMessage.Rcode = dns.RcodeSuccess
+		replyMessage.Answer = append(replyMessage.Answer, records...)
+	} else {
+		// TODO: return success if we have a different record type for the same name, relevant for search domains
+		replyMessage.Rcode = dns.RcodeNameError
+	}
+
+	if err := w.WriteMsg(replyMessage); err != nil {
+		log.Warnf("failed to write the local resolver response: %v", err)
+	}
+}
+
+// lookupRecords fetches *all* DNS records matching the first question in r.
+func (d *Resolver) lookupRecords(question dns.Question) []dns.RR {
+	d.mu.RLock()
+	records, found := d.records[question]
+
+	if !found {
+		d.mu.RUnlock()
+		// alternatively check if we have a cname
+		if question.Qtype != dns.TypeCNAME {
+			question.Qtype = dns.TypeCNAME
+			return d.lookupRecords(question)
+		}
+		return nil
+	}
+
+	recordsCopy := slices.Clone(records)
+	d.mu.RUnlock()
+
+	// if there's more than one record, rotate them (round-robin)
+	if len(recordsCopy) > 1 {
+		d.mu.Lock()
+		records = d.records[question]
+		if len(records) > 1 {
+			first := records[0]
+			records = append(records[1:], first)
+			d.records[question] = records
+		}
+		d.mu.Unlock()
+	}
+
+	return recordsCopy
+}
+
+func (d *Resolver) Update(update []nbdns.SimpleRecord) {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+
+	maps.Clear(d.records)
+
+	for _, rec := range update {
+		if err := d.registerRecord(rec); err != nil {
+			log.Warnf("failed to register the record (%s): %v", rec, err)
+			continue
+		}
+	}
+}
+
+// RegisterRecord stores a new record by appending it to any existing list
+func (d *Resolver) RegisterRecord(record nbdns.SimpleRecord) error {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+
+	return d.registerRecord(record)
+}
+
+// registerRecord performs the registration with the lock already held
+func (d *Resolver) registerRecord(record nbdns.SimpleRecord) error {
+	rr, err := dns.NewRR(record.String())
+	if err != nil {
+		return fmt.Errorf("register record: %w", err)
+	}
+
+	rr.Header().Rdlength = record.Len()
+	header := rr.Header()
+	q := dns.Question{
+		Name:   strings.ToLower(dns.Fqdn(header.Name)),
+		Qtype:  header.Rrtype,
+		Qclass: header.Class,
+	}
+
+	d.records[q] = append(d.records[q], rr)
+
+	return nil
+}
diff --git a/client/internal/dns/server.go b/client/internal/dns/server.go
index 65b90e5f..7b845235 100644
--- a/client/internal/dns/server.go
+++ b/client/internal/dns/server.go
@@ -15,6 +15,8 @@ import (
 	"golang.org/x/exp/maps"
 
 	"github.com/netbirdio/netbird/client/iface/netstack"
+	"github.com/netbirdio/netbird/client/internal/dns/local"
+	"github.com/netbirdio/netbird/client/internal/dns/types"
 	"github.com/netbirdio/netbird/client/internal/listener"
 	"github.com/netbirdio/netbird/client/internal/peer"
 	"github.com/netbirdio/netbird/client/internal/statemanager"
@@ -46,8 +48,6 @@ type Server interface {
 	ProbeAvailability()
 }
 
-type handlerID string
-
 type nsGroupsByDomain struct {
 	domain string
 	groups []*nbdns.NameServerGroup
@@ -61,7 +61,7 @@ type DefaultServer struct {
 	mux                sync.Mutex
 	service            service
 	dnsMuxMap          registeredHandlerMap
-	localResolver      *localResolver
+	localResolver      *local.Resolver
 	wgInterface        WGIface
 	hostManager        hostManager
 	updateSerial       uint64
@@ -84,9 +84,9 @@ type DefaultServer struct {
 
 type handlerWithStop interface {
 	dns.Handler
-	stop()
-	probeAvailability()
-	id() handlerID
+	Stop()
+	ProbeAvailability()
+	ID() types.HandlerID
 }
 
 type handlerWrapper struct {
@@ -95,7 +95,7 @@ type handlerWrapper struct {
 	priority int
 }
 
-type registeredHandlerMap map[handlerID]handlerWrapper
+type registeredHandlerMap map[types.HandlerID]handlerWrapper
 
 // NewDefaultServer returns a new dns server
 func NewDefaultServer(
@@ -171,16 +171,14 @@ func newDefaultServer(
 	handlerChain := NewHandlerChain()
 	ctx, stop := context.WithCancel(ctx)
 	defaultServer := &DefaultServer{
-		ctx:          ctx,
-		ctxCancel:    stop,
-		disableSys:   disableSys,
-		service:      dnsService,
-		handlerChain: handlerChain,
-		extraDomains: make(map[domain.Domain]int),
-		dnsMuxMap:    make(registeredHandlerMap),
-		localResolver: &localResolver{
-			registeredMap: make(registrationMap),
-		},
+		ctx:            ctx,
+		ctxCancel:      stop,
+		disableSys:     disableSys,
+		service:        dnsService,
+		handlerChain:   handlerChain,
+		extraDomains:   make(map[domain.Domain]int),
+		dnsMuxMap:      make(registeredHandlerMap),
+		localResolver:  local.NewResolver(),
 		wgInterface:    wgInterface,
 		statusRecorder: statusRecorder,
 		stateManager:   stateManager,
@@ -403,7 +401,7 @@ func (s *DefaultServer) ProbeAvailability() {
 		wg.Add(1)
 		go func(mux handlerWithStop) {
 			defer wg.Done()
-			mux.probeAvailability()
+			mux.ProbeAvailability()
 		}(mux.handler)
 	}
 	wg.Wait()
@@ -420,7 +418,7 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error {
 		s.service.Stop()
 	}
 
-	localMuxUpdates, localRecordsByDomain, err := s.buildLocalHandlerUpdate(update.CustomZones)
+	localMuxUpdates, localRecords, err := s.buildLocalHandlerUpdate(update.CustomZones)
 	if err != nil {
 		return fmt.Errorf("local handler updater: %w", err)
 	}
@@ -434,7 +432,7 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error {
 	s.updateMux(muxUpdates)
 
 	// register local records
-	s.updateLocalResolver(localRecordsByDomain)
+	s.localResolver.Update(localRecords)
 
 	s.currentConfig = dnsConfigToHostDNSConfig(update, s.service.RuntimeIP(), s.service.RuntimePort())
 
@@ -491,7 +489,7 @@ func (s *DefaultServer) applyHostConfig() {
 		}
 	}
 
-	log.Debugf("extra match domains: %v", s.extraDomains)
+	log.Debugf("extra match domains: %v", maps.Keys(s.extraDomains))
 
 	if err := s.hostManager.applyDNSConfig(config, s.stateManager); err != nil {
 		log.Errorf("failed to apply DNS host manager update: %v", err)
@@ -516,11 +514,9 @@ func (s *DefaultServer) handleErrNoGroupaAll(err error) {
 	)
 }
 
-func (s *DefaultServer) buildLocalHandlerUpdate(
-	customZones []nbdns.CustomZone,
-) ([]handlerWrapper, map[string][]nbdns.SimpleRecord, error) {
+func (s *DefaultServer) buildLocalHandlerUpdate(customZones []nbdns.CustomZone) ([]handlerWrapper, []nbdns.SimpleRecord, error) {
 	var muxUpdates []handlerWrapper
-	localRecords := make(map[string][]nbdns.SimpleRecord)
+	var localRecords []nbdns.SimpleRecord
 
 	for _, customZone := range customZones {
 		if len(customZone.Records) == 0 {
@@ -534,17 +530,13 @@ func (s *DefaultServer) buildLocalHandlerUpdate(
 			priority: PriorityMatchDomain,
 		})
 
-		// group all records under this domain
 		for _, record := range customZone.Records {
-			var class uint16 = dns.ClassINET
 			if record.Class != nbdns.DefaultClass {
 				log.Warnf("received an invalid class type: %s", record.Class)
 				continue
 			}
-
-			key := buildRecordKey(record.Name, class, uint16(record.Type))
-
-			localRecords[key] = append(localRecords[key], record)
+			// zone records contain the fqdn, so we can just flatten them
+			localRecords = append(localRecords, record)
 		}
 	}
 
@@ -627,7 +619,7 @@ func (s *DefaultServer) createHandlersForDomainGroup(domainGroup nsGroupsByDomai
 		}
 
 		if len(handler.upstreamServers) == 0 {
-			handler.stop()
+			handler.Stop()
 			log.Errorf("received a nameserver group with an invalid nameserver list")
 			continue
 		}
@@ -656,7 +648,7 @@ func (s *DefaultServer) updateMux(muxUpdates []handlerWrapper) {
 	// this will introduce a short period of time when the server is not able to handle DNS requests
 	for _, existing := range s.dnsMuxMap {
 		s.deregisterHandler([]string{existing.domain}, existing.priority)
-		existing.handler.stop()
+		existing.handler.Stop()
 	}
 
 	muxUpdateMap := make(registeredHandlerMap)
@@ -667,7 +659,7 @@ func (s *DefaultServer) updateMux(muxUpdates []handlerWrapper) {
 			containsRootUpdate = true
 		}
 		s.registerHandler([]string{update.domain}, update.handler, update.priority)
-		muxUpdateMap[update.handler.id()] = update
+		muxUpdateMap[update.handler.ID()] = update
 	}
 
 	// If there's no root update and we had a root handler, restore it
@@ -683,33 +675,6 @@ func (s *DefaultServer) updateMux(muxUpdates []handlerWrapper) {
 	s.dnsMuxMap = muxUpdateMap
 }
 
-func (s *DefaultServer) updateLocalResolver(update map[string][]nbdns.SimpleRecord) {
-	// remove old records that are no longer present
-	for key := range s.localResolver.registeredMap {
-		_, found := update[key]
-		if !found {
-			s.localResolver.deleteRecord(key)
-		}
-	}
-
-	updatedMap := make(registrationMap)
-	for _, recs := range update {
-		for _, rec := range recs {
-			// convert the record to a dns.RR and register
-			key, err := s.localResolver.registerRecord(rec)
-			if err != nil {
-				log.Warnf("got an error while registering the record (%s), error: %v",
-					rec.String(), err)
-				continue
-			}
-
-			updatedMap[key] = struct{}{}
-		}
-	}
-
-	s.localResolver.registeredMap = updatedMap
-}
-
 func getNSHostPort(ns nbdns.NameServer) string {
 	return fmt.Sprintf("%s:%d", ns.IP.String(), ns.Port)
 }
diff --git a/client/internal/dns/service_memory.go b/client/internal/dns/service_memory.go
index 34c56375..226202cf 100644
--- a/client/internal/dns/service_memory.go
+++ b/client/internal/dns/service_memory.go
@@ -24,11 +24,15 @@ type ServiceViaMemory struct {
 }
 
 func NewServiceViaMemory(wgIface WGIface) *ServiceViaMemory {
+	lastIP, err := nbnet.GetLastIPFromNetwork(wgIface.Address().Network, 1)
+	if err != nil {
+		log.Errorf("get last ip from network: %v", err)
+	}
 	s := &ServiceViaMemory{
 		wgInterface: wgIface,
 		dnsMux:      dns.NewServeMux(),
 
-		runtimeIP:   nbnet.GetLastIPFromNetwork(wgIface.Address().Network, 1).String(),
+		runtimeIP:   lastIP.String(),
 		runtimePort: defaultPort,
 	}
 	return s
@@ -91,7 +95,7 @@ func (s *ServiceViaMemory) filterDNSTraffic() (string, error) {
 	}
 
 	firstLayerDecoder := layers.LayerTypeIPv4
-	if s.wgInterface.Address().Network.IP.To4() == nil {
+	if s.wgInterface.Address().IP.Is6() {
 		firstLayerDecoder = layers.LayerTypeIPv6
 	}
 
diff --git a/client/internal/dns/systemd_linux.go b/client/internal/dns/systemd_linux.go
index 53c5c58a..9040ed78 100644
--- a/client/internal/dns/systemd_linux.go
+++ b/client/internal/dns/systemd_linux.go
@@ -30,9 +30,12 @@ const (
 	systemdDbusSetDNSMethodSuffix          = systemdDbusLinkInterface + ".SetDNS"
 	systemdDbusSetDefaultRouteMethodSuffix = systemdDbusLinkInterface + ".SetDefaultRoute"
 	systemdDbusSetDomainsMethodSuffix      = systemdDbusLinkInterface + ".SetDomains"
+	systemdDbusSetDNSSECMethodSuffix       = systemdDbusLinkInterface + ".SetDNSSEC"
 	systemdDbusResolvConfModeForeign       = "foreign"
 
 	dbusErrorUnknownObject = "org.freedesktop.DBus.Error.UnknownObject"
+
+	dnsSecDisabled = "no"
 )
 
 type systemdDbusConfigurator struct {
@@ -95,9 +98,13 @@ func (s *systemdDbusConfigurator) applyDNSConfig(config HostDNSConfig, stateMana
 		Family:  unix.AF_INET,
 		Address: ipAs4[:],
 	}
-	err = s.callLinkMethod(systemdDbusSetDNSMethodSuffix, []systemdDbusDNSInput{defaultLinkInput})
-	if err != nil {
-		return fmt.Errorf("setting the interface DNS server %s:%d failed with error: %w", config.ServerIP, config.ServerPort, err)
+	if err = s.callLinkMethod(systemdDbusSetDNSMethodSuffix, []systemdDbusDNSInput{defaultLinkInput}); err != nil {
+		return fmt.Errorf("set interface DNS server %s:%d: %w", config.ServerIP, config.ServerPort, err)
+	}
+
+	// We don't support dnssec. On some machines this is default on so we explicitly set it to off
+	if err = s.callLinkMethod(systemdDbusSetDNSSECMethodSuffix, dnsSecDisabled); err != nil {
+		log.Warnf("failed to set DNSSEC to 'no': %v", err)
 	}
 
 	var (
diff --git a/client/internal/dns/types/types.go b/client/internal/dns/types/types.go
new file mode 100644
index 00000000..5a8be03b
--- /dev/null
+++ b/client/internal/dns/types/types.go
@@ -0,0 +1,3 @@
+package types
+
+type HandlerID string
diff --git a/client/internal/dns/upstream.go b/client/internal/dns/upstream.go
index fa69d493..2fbfb3b9 100644
--- a/client/internal/dns/upstream.go
+++ b/client/internal/dns/upstream.go
@@ -19,6 +19,7 @@ import (
 	log "github.com/sirupsen/logrus"
 
 	"github.com/netbirdio/netbird/client/iface"
+	"github.com/netbirdio/netbird/client/internal/dns/types"
 	"github.com/netbirdio/netbird/client/internal/peer"
 	"github.com/netbirdio/netbird/client/proto"
 )
@@ -81,21 +82,21 @@ func (u *upstreamResolverBase) String() string {
 }
 
 // ID returns the unique handler ID
-func (u *upstreamResolverBase) id() handlerID {
+func (u *upstreamResolverBase) ID() types.HandlerID {
 	servers := slices.Clone(u.upstreamServers)
 	slices.Sort(servers)
 
 	hash := sha256.New()
 	hash.Write([]byte(u.domain + ":"))
 	hash.Write([]byte(strings.Join(servers, ",")))
-	return handlerID("upstream-" + hex.EncodeToString(hash.Sum(nil)[:8]))
+	return types.HandlerID("upstream-" + hex.EncodeToString(hash.Sum(nil)[:8]))
 }
 
 func (u *upstreamResolverBase) MatchSubdomains() bool {
 	return true
 }
 
-func (u *upstreamResolverBase) stop() {
+func (u *upstreamResolverBase) Stop() {
 	log.Debugf("stopping serving DNS for upstreams %s", u.upstreamServers)
 	u.cancel()
 }
@@ -198,9 +199,9 @@ func (u *upstreamResolverBase) checkUpstreamFails(err error) {
 	)
 }
 
-// probeAvailability tests all upstream servers simultaneously and
+// ProbeAvailability tests all upstream servers simultaneously and
 // disables the resolver if none work
-func (u *upstreamResolverBase) probeAvailability() {
+func (u *upstreamResolverBase) ProbeAvailability() {
 	u.mutex.Lock()
 	defer u.mutex.Unlock()
 
diff --git a/client/internal/dns/upstream_android.go b/client/internal/dns/upstream_android.go
index 06ffcba1..52d2ba58 100644
--- a/client/internal/dns/upstream_android.go
+++ b/client/internal/dns/upstream_android.go
@@ -3,6 +3,7 @@ package dns
 import (
 	"context"
 	"net"
+	"net/netip"
 	"syscall"
 	"time"
 
@@ -23,8 +24,8 @@ type upstreamResolver struct {
 func newUpstreamResolver(
 	ctx context.Context,
 	_ string,
-	_ net.IP,
-	_ *net.IPNet,
+	_ netip.Addr,
+	_ netip.Prefix,
 	statusRecorder *peer.Status,
 	hostsDNSHolder *hostsDNSHolder,
 	domain string,
diff --git a/client/internal/dns/upstream_general.go b/client/internal/dns/upstream_general.go
index 9bb5feab..1bc06a7c 100644
--- a/client/internal/dns/upstream_general.go
+++ b/client/internal/dns/upstream_general.go
@@ -4,7 +4,7 @@ package dns
 
 import (
 	"context"
-	"net"
+	"net/netip"
 	"time"
 
 	"github.com/miekg/dns"
@@ -19,8 +19,8 @@ type upstreamResolver struct {
 func newUpstreamResolver(
 	ctx context.Context,
 	_ string,
-	_ net.IP,
-	_ *net.IPNet,
+	_ netip.Addr,
+	_ netip.Prefix,
 	statusRecorder *peer.Status,
 	_ *hostsDNSHolder,
 	domain string,
diff --git a/client/internal/dns/upstream_ios.go b/client/internal/dns/upstream_ios.go
index ca5b3113..648cab17 100644
--- a/client/internal/dns/upstream_ios.go
+++ b/client/internal/dns/upstream_ios.go
@@ -6,6 +6,7 @@ import (
 	"context"
 	"fmt"
 	"net"
+	"net/netip"
 	"syscall"
 	"time"
 
@@ -18,16 +19,16 @@ import (
 
 type upstreamResolverIOS struct {
 	*upstreamResolverBase
-	lIP           net.IP
-	lNet          *net.IPNet
+	lIP           netip.Addr
+	lNet          netip.Prefix
 	interfaceName string
 }
 
 func newUpstreamResolver(
 	ctx context.Context,
 	interfaceName string,
-	ip net.IP,
-	net *net.IPNet,
+	ip netip.Addr,
+	net netip.Prefix,
 	statusRecorder *peer.Status,
 	_ *hostsDNSHolder,
 	domain string,
@@ -58,8 +59,11 @@ func (u *upstreamResolverIOS) exchange(ctx context.Context, upstream string, r *
 	}
 	client.DialTimeout = timeout
 
-	upstreamIP := net.ParseIP(upstreamHost)
-	if u.lNet.Contains(upstreamIP) || net.IP.IsPrivate(upstreamIP) {
+	upstreamIP, err := netip.ParseAddr(upstreamHost)
+	if err != nil {
+		log.Warnf("failed to parse upstream host %s: %s", upstreamHost, err)
+	}
+	if u.lNet.Contains(upstreamIP) || upstreamIP.IsPrivate() {
 		log.Debugf("using private client to query upstream: %s", upstream)
 		client, err = GetClientPrivate(u.lIP, u.interfaceName, timeout)
 		if err != nil {
@@ -73,7 +77,7 @@ func (u *upstreamResolverIOS) exchange(ctx context.Context, upstream string, r *
 
 // GetClientPrivate returns a new DNS client bound to the local IP address of the Netbird interface
 // This method is needed for iOS
-func GetClientPrivate(ip net.IP, interfaceName string, dialTimeout time.Duration) (*dns.Client, error) {
+func GetClientPrivate(ip netip.Addr, interfaceName string, dialTimeout time.Duration) (*dns.Client, error) {
 	index, err := getInterfaceIndex(interfaceName)
 	if err != nil {
 		log.Debugf("unable to get interface index for %s: %s", interfaceName, err)
@@ -82,7 +86,7 @@ func GetClientPrivate(ip net.IP, interfaceName string, dialTimeout time.Duration
 
 	dialer := &net.Dialer{
 		LocalAddr: &net.UDPAddr{
-			IP:   ip,
+			IP:   ip.AsSlice(),
 			Port: 0, // Let the OS pick a free port
 		},
 		Timeout: dialTimeout,
diff --git a/client/internal/dns/wgiface.go b/client/internal/dns/wgiface.go
index c6c1752e..28e9cebf 100644
--- a/client/internal/dns/wgiface.go
+++ b/client/internal/dns/wgiface.go
@@ -5,7 +5,6 @@ package dns
 import (
 	"net"
 
-	"github.com/netbirdio/netbird/client/iface/configurer"
 	"github.com/netbirdio/netbird/client/iface/device"
 	"github.com/netbirdio/netbird/client/iface/wgaddr"
 )
@@ -18,5 +17,4 @@ type WGIface interface {
 	IsUserspaceBind() bool
 	GetFilter() device.PacketFilter
 	GetDevice() *device.FilteredDevice
-	GetStats(peerKey string) (configurer.WGStats, error)
 }
diff --git a/client/internal/dns/wgiface_windows.go b/client/internal/dns/wgiface_windows.go
index 74e5c75a..d1374fd5 100644
--- a/client/internal/dns/wgiface_windows.go
+++ b/client/internal/dns/wgiface_windows.go
@@ -1,7 +1,6 @@
 package dns
 
 import (
-	"github.com/netbirdio/netbird/client/iface/configurer"
 	"github.com/netbirdio/netbird/client/iface/device"
 	"github.com/netbirdio/netbird/client/iface/wgaddr"
 )
@@ -13,6 +12,5 @@ type WGIface interface {
 	IsUserspaceBind() bool
 	GetFilter() device.PacketFilter
 	GetDevice() *device.FilteredDevice
-	GetStats(peerKey string) (configurer.WGStats, error)
 	GetInterfaceGUIDString() (string, error)
 }
diff --git a/client/internal/dnsfwd/forwarder.go b/client/internal/dnsfwd/forwarder.go
index 2d69ce85..45b47963 100644
--- a/client/internal/dnsfwd/forwarder.go
+++ b/client/internal/dnsfwd/forwarder.go
@@ -3,6 +3,7 @@ package dnsfwd
 import (
 	"context"
 	"errors"
+	"fmt"
 	"math"
 	"net"
 	"net/netip"
@@ -10,11 +11,16 @@ import (
 	"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"
@@ -23,79 +29,116 @@ const upstreamTimeout = 15 * time.Second
 type DNSForwarder struct {
 	listenAddress  string
 	ttl            uint32
-	domains        []string
 	statusRecorder *peer.Status
 
 	dnsServer *dns.Server
 	mux       *dns.ServeMux
+	tcpServer *dns.Server
+	tcpMux    *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 {
-	log.Infof("listen DNS forwarder on address=%s", f.listenAddress)
-	mux := dns.NewServeMux()
+func (f *DNSForwarder) Listen(entries []*ForwarderEntry) error {
+	log.Infof("starting DNS forwarder on address=%s", f.listenAddress)
 
-	dnsServer := &dns.Server{
+	// UDP server
+	mux := dns.NewServeMux()
+	f.mux = mux
+	f.dnsServer = &dns.Server{
 		Addr:    f.listenAddress,
 		Net:     "udp",
 		Handler: mux,
 	}
-	f.dnsServer = dnsServer
-	f.mux = mux
+	// TCP server
+	tcpMux := dns.NewServeMux()
+	f.tcpMux = tcpMux
+	f.tcpServer = &dns.Server{
+		Addr:    f.listenAddress,
+		Net:     "tcp",
+		Handler: tcpMux,
+	}
 
-	f.UpdateDomains(domains, resIds)
+	f.UpdateDomains(entries)
 
-	return dnsServer.ListenAndServe()
-}
+	errCh := make(chan error, 2)
+
+	go func() {
+		log.Infof("DNS UDP listener running on %s", f.listenAddress)
+		errCh <- f.dnsServer.ListenAndServe()
+	}()
+	go func() {
+		log.Infof("DNS TCP listener running on %s", f.listenAddress)
+		errCh <- f.tcpServer.ListenAndServe()
+	}()
 
-func (f *DNSForwarder) UpdateDomains(domains []string, resIds map[string]string) {
-	log.Debugf("Updating domains from %v to %v", f.domains, domains)
+	// return the first error we get (e.g. bind failure or shutdown)
+	return <-errCh
+}
+func (f *DNSForwarder) UpdateDomains(entries []*ForwarderEntry) {
+	f.mutex.Lock()
+	defer f.mutex.Unlock()
 
-	for _, d := range f.domains {
-		f.mux.HandleRemove(d)
+	if f.mux == nil {
+		log.Debug("DNS mux is nil, skipping domain update")
+		f.fwdEntries = entries
+		return
 	}
-	f.resId.Clear()
 
-	newDomains := filterDomains(domains)
-	for _, d := range newDomains {
-		f.mux.HandleFunc(d, f.handleDNSQuery)
+	oldDomains := filterDomains(f.fwdEntries)
+	for _, d := range oldDomains {
+		f.mux.HandleRemove(d.PunycodeString())
+		f.tcpMux.HandleRemove(d.PunycodeString())
 	}
 
-	for domain, resId := range resIds {
-		if domain != "" {
-			f.resId.Store(domain, resId)
-		}
+	newDomains := filterDomains(entries)
+	for _, d := range newDomains {
+		f.mux.HandleFunc(d.PunycodeString(), f.handleDNSQueryUDP)
+		f.tcpMux.HandleFunc(d.PunycodeString(), f.handleDNSQueryTCP)
 	}
 
-	f.domains = newDomains
+	f.fwdEntries = entries
+	log.Debugf("Updated domains from %v to %v", oldDomains, newDomains)
 }
 
 func (f *DNSForwarder) Close(ctx context.Context) error {
-	if f.dnsServer == nil {
-		return nil
+	var result *multierror.Error
+
+	if f.dnsServer != nil {
+		if err := f.dnsServer.ShutdownContext(ctx); err != nil {
+			result = multierror.Append(result, fmt.Errorf("UDP shutdown: %w", err))
+		}
 	}
-	return f.dnsServer.ShutdownContext(ctx)
+	if f.tcpServer != nil {
+		if err := f.tcpServer.ShutdownContext(ctx); err != nil {
+			result = multierror.Append(result, fmt.Errorf("TCP shutdown: %w", err))
+		}
+	}
+
+	return nberrors.FormatErrorOrNil(result)
 }
 
-func (f *DNSForwarder) handleDNSQuery(w dns.ResponseWriter, query *dns.Msg) {
+func (f *DNSForwarder) handleDNSQuery(w dns.ResponseWriter, query *dns.Msg) *dns.Msg {
 	if len(query.Question) == 0 {
-		return
+		return nil
 	}
+	question := query.Question[0]
 	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.Name, question.Qtype, question.Qclass)
 
-	question := query.Question[0]
-	domain := question.Name
+	domain := strings.ToLower(question.Name)
 
 	resp := query.SetReply(query)
 	var network string
@@ -111,41 +154,96 @@ func (f *DNSForwarder) handleDNSQuery(w dns.ResponseWriter, query *dns.Msg) {
 		if err := w.WriteMsg(resp); err != nil {
 			log.Errorf("failed to write DNS response: %v", err)
 		}
-		return
+		return nil
 	}
 
 	ctx, cancel := context.WithTimeout(context.Background(), upstreamTimeout)
 	defer cancel()
 	ips, err := net.DefaultResolver.LookupNetIP(ctx, network, domain)
 	if err != nil {
-		f.handleDNSError(w, resp, domain, err)
+		f.handleDNSError(w, query, resp, domain, err)
+		return nil
+	}
+
+	f.updateInternalState(domain, ips)
+	f.addIPsToResponse(resp, domain, ips)
+
+	return resp
+}
+
+func (f *DNSForwarder) handleDNSQueryUDP(w dns.ResponseWriter, query *dns.Msg) {
+
+	resp := f.handleDNSQuery(w, query)
+	if resp == nil {
 		return
 	}
 
-	resId := f.getResIdForDomain(strings.TrimSuffix(domain, "."))
-	if resId != "" {
+	opt := query.IsEdns0()
+	maxSize := dns.MinMsgSize
+	if opt != nil {
+		// client advertised a larger EDNS0 buffer
+		maxSize = int(opt.UDPSize())
+	}
+
+	// if our response is too big, truncate and set the TC bit
+	if resp.Len() > maxSize {
+		resp.Truncate(maxSize)
+	}
+
+	if err := w.WriteMsg(resp); err != nil {
+		log.Errorf("failed to write DNS response: %v", err)
+	}
+}
+
+func (f *DNSForwarder) handleDNSQueryTCP(w dns.ResponseWriter, query *dns.Msg) {
+	resp := f.handleDNSQuery(w, query)
+	if resp == nil {
+		return
+	}
+
+	if err := w.WriteMsg(resp); err != nil {
+		log.Errorf("failed to write DNS response: %v", err)
+	}
+}
+
+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 ipWithSuffix string
+			var prefix netip.Prefix
 			if ip.Is4() {
-				ipWithSuffix = ip.String() + "/32"
-				log.Tracef("resolved domain=%s to IPv4=%s", domain, ipWithSuffix)
+				prefix = netip.PrefixFrom(ip, 32)
 			} else {
-				ipWithSuffix = ip.String() + "/128"
-				log.Tracef("resolved domain=%s to IPv6=%s", domain, ipWithSuffix)
+				prefix = netip.PrefixFrom(ip, 128)
 			}
-			f.statusRecorder.AddResolvedIPLookupEntry(ipWithSuffix, resId)
+			prefixes = append(prefixes, prefix)
+			f.statusRecorder.AddResolvedIPLookupEntry(prefix, mostSpecificResId)
 		}
 	}
 
-	f.addIPsToResponse(resp, domain, ips)
+	if f.firewall != nil {
+		f.updateFirewall(matchingEntries, prefixes)
+	}
+}
 
-	if err := w.WriteMsg(resp); err != nil {
-		log.Errorf("failed to write DNS response: %v", err)
+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) {
+func (f *DNSForwarder) handleDNSError(w dns.ResponseWriter, query, resp *dns.Msg, domain string, err error) {
 	var dnsErr *net.DNSError
 
 	switch {
@@ -157,7 +255,7 @@ func (f *DNSForwarder) handleDNSError(w dns.ResponseWriter, resp *dns.Msg, domai
 		}
 
 		if dnsErr.Server != "" {
-			log.Warnf("failed to resolve query for domain=%s server=%s: %v", domain, dnsErr.Server, err)
+			log.Warnf("failed to resolve query for type=%s domain=%s server=%s: %v", dns.TypeToString[query.Question[0].Qtype], domain, dnsErr.Server, err)
 		} else {
 			log.Warnf(errResolveFailed, domain, err)
 		}
@@ -204,45 +302,53 @@ func (f *DNSForwarder) addIPsToResponse(resp *dns.Msg, domain string, ips []neti
 	}
 }
 
-func (f *DNSForwarder) getResIdForDomain(domain string) string {
-	var selectedResId string
+// 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()
 
-	f.resId.Range(func(key, value interface{}) bool {
+	for _, entry := range f.fwdEntries {
 		var score int
-		pattern := key.(string)
+		pattern := entry.Domain.PunycodeString()
 
 		switch {
 		case strings.HasPrefix(pattern, "*."):
 			baseDomain := strings.TrimPrefix(pattern, "*.")
-			if domain == baseDomain || strings.HasSuffix(domain, "."+baseDomain) {
+
+			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:
-			return true
+			continue
 		}
 
 		if score > bestScore {
 			bestScore = score
-			selectedResId = value.(string)
+			selectedResId = entry.ResID
 		}
-		return true
-	})
+	}
 
-	return selectedResId
+	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/manager.go b/client/internal/dnsfwd/manager.go
index a51ae7ab..91abce82 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,11 +21,19 @@ 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
 
 	fwRules      []firewall.Rule
+	tcpRules     []firewall.Rule
 	dnsForwarder *DNSForwarder
 }
 
@@ -34,7 +44,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 +54,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 +65,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 +91,47 @@ 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
+	}
+	m.fwRules = dnsRules
+
+	tcpRules, err := m.firewall.AddPeerFiltering(nil, net.IP{0, 0, 0, 0}, firewall.ProtocolTCP, 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.tcpRules = tcpRules
 
 	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))
+		}
+	}
+	for _, rule := range m.tcpRules {
+		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
+	m.tcpRules = nil
 	return nberrors.FormatErrorOrNil(mErr)
 }
diff --git a/client/internal/engine.go b/client/internal/engine.go
index c377c12e..e4700774 100644
--- a/client/internal/engine.go
+++ b/client/internal/engine.go
@@ -38,6 +38,7 @@ import (
 	nftypes "github.com/netbirdio/netbird/client/internal/netflow/types"
 	"github.com/netbirdio/netbird/client/internal/networkmonitor"
 	"github.com/netbirdio/netbird/client/internal/peer"
+	"github.com/netbirdio/netbird/client/internal/peer/dispatcher"
 	"github.com/netbirdio/netbird/client/internal/peer/guard"
 	icemaker "github.com/netbirdio/netbird/client/internal/peer/ice"
 	"github.com/netbirdio/netbird/client/internal/peerstore"
@@ -120,8 +121,10 @@ type EngineConfig struct {
 	DisableServerRoutes bool
 	DisableDNS          bool
 	DisableFirewall     bool
+	BlockLANAccess      bool
+	BlockInbound        bool
 
-	BlockLANAccess bool
+	LazyConnectionEnabled bool
 }
 
 // Engine is a mechanism responsible for reacting on Signal and Management stream events and managing connections to the remote peers.
@@ -134,6 +137,8 @@ type Engine struct {
 	// peerConns is a map that holds all the peers that are known to this peer
 	peerStore *peerstore.Store
 
+	connMgr *ConnMgr
+
 	beforePeerHook nbnet.AddHookFunc
 	afterPeerHook  nbnet.RemoveHookFunc
 
@@ -170,7 +175,8 @@ type Engine struct {
 	sshServerFunc func(hostKeyPEM []byte, addr string) (nbssh.Server, error)
 	sshServer     nbssh.Server
 
-	statusRecorder *peer.Status
+	statusRecorder     *peer.Status
+	peerConnDispatcher *dispatcher.ConnectionDispatcher
 
 	firewall          firewallManager.Manager
 	routeManager      routemanager.Manager
@@ -235,6 +241,8 @@ func NewEngine(
 		checks:         checks,
 		connSemaphore:  semaphoregroup.NewSemaphoreGroup(connInitLimit),
 	}
+
+	path := statemanager.GetDefaultStatePath()
 	if runtime.GOOS == "ios" {
 		if !fileExists(mobileDep.StateFilePath) {
 			err := createFile(mobileDep.StateFilePath)
@@ -244,11 +252,9 @@ func NewEngine(
 			}
 		}
 
-		engine.stateManager = statemanager.New(mobileDep.StateFilePath)
-	}
-	if path := statemanager.GetDefaultStatePath(); path != "" {
-		engine.stateManager = statemanager.New(path)
+		path = mobileDep.StateFilePath
 	}
+	engine.stateManager = statemanager.New(path)
 
 	return engine
 }
@@ -262,6 +268,10 @@ func (e *Engine) Stop() error {
 	e.syncMsgMux.Lock()
 	defer e.syncMsgMux.Unlock()
 
+	if e.connMgr != nil {
+		e.connMgr.Close()
+	}
+
 	// stopping network monitor first to avoid starting the engine again
 	if e.networkMonitor != nil {
 		e.networkMonitor.Stop()
@@ -297,8 +307,7 @@ func (e *Engine) Stop() error {
 	e.statusRecorder.UpdateDNSStates([]peer.NSGroupState{})
 	e.statusRecorder.UpdateRelayStates([]relay.ProbeResult{})
 
-	err := e.removeAllPeers()
-	if err != nil {
+	if err := e.removeAllPeers(); err != nil {
 		return fmt.Errorf("failed to remove all peers: %s", err)
 	}
 
@@ -350,6 +359,7 @@ func (e *Engine) Start() error {
 		return fmt.Errorf("new wg interface: %w", err)
 	}
 	e.wgInterface = wgIface
+	e.statusRecorder.SetWgIface(wgIface)
 
 	// start flow manager right after interface creation
 	publicKey := e.config.WgPrivateKey.PublicKey()
@@ -371,7 +381,6 @@ func (e *Engine) Start() error {
 			return fmt.Errorf("run rosenpass manager: %w", err)
 		}
 	}
-
 	e.stateManager.Start()
 
 	initialRoutes, dnsServer, err := e.newDnsServer()
@@ -405,8 +414,7 @@ func (e *Engine) Start() error {
 
 	e.routeManager.SetRouteChangeListener(e.mobileDep.NetworkChangeListener)
 
-	err = e.wgInterfaceCreate()
-	if err != nil {
+	if err = e.wgInterfaceCreate(); err != nil {
 		log.Errorf("failed creating tunnel interface %s: [%s]", e.config.WgIfaceName, err.Error())
 		e.close()
 		return fmt.Errorf("create wg interface: %w", err)
@@ -423,7 +431,8 @@ func (e *Engine) Start() error {
 		return fmt.Errorf("up wg interface: %w", err)
 	}
 
-	if e.firewall != nil {
+	// if inbound conns are blocked there is no need to create the ACL manager
+	if e.firewall != nil && !e.config.BlockInbound {
 		e.acl = acl.NewDefaultManager(e.firewall)
 	}
 
@@ -442,6 +451,11 @@ func (e *Engine) Start() error {
 		NATExternalIPs:       e.parseNATExternalIPMappings(),
 	}
 
+	e.peerConnDispatcher = dispatcher.NewConnectionDispatcher()
+
+	e.connMgr = NewConnMgr(e.config, e.statusRecorder, e.peerStore, wgIface, e.peerConnDispatcher)
+	e.connMgr.Start(e.ctx)
+
 	e.srWatcher = guard.NewSRWatcher(e.signal, e.relayManager, e.mobileDep.IFaceDiscover, iceCfg)
 	e.srWatcher.Start()
 
@@ -450,7 +464,6 @@ func (e *Engine) Start() error {
 
 	// starting network monitor at the very last to avoid disruptions
 	e.startNetworkMonitor()
-
 	return nil
 }
 
@@ -475,11 +488,9 @@ func (e *Engine) createFirewall() error {
 }
 
 func (e *Engine) initFirewall() error {
-	if e.firewall.IsServerRouteSupported() {
-		if err := e.routeManager.EnableServerRouter(e.firewall); err != nil {
-			e.close()
-			return fmt.Errorf("enable server router: %w", err)
-		}
+	if err := e.routeManager.EnableServerRouter(e.firewall); err != nil {
+		e.close()
+		return fmt.Errorf("enable server router: %w", err)
 	}
 
 	if e.config.BlockLANAccess {
@@ -513,6 +524,11 @@ func (e *Engine) initFirewall() error {
 }
 
 func (e *Engine) blockLanAccess() {
+	if e.config.BlockInbound {
+		// no need to set up extra deny rules if inbound is already blocked in general
+		return
+	}
+
 	var merr *multierror.Error
 
 	// TODO: keep this updated
@@ -527,7 +543,7 @@ func (e *Engine) blockLanAccess() {
 		if _, err := e.firewall.AddRouteFiltering(
 			nil,
 			[]netip.Prefix{v4},
-			network,
+			firewallManager.Network{Prefix: network},
 			firewallManager.ProtocolALL,
 			nil,
 			nil,
@@ -550,6 +566,16 @@ func (e *Engine) modifyPeers(peersUpdate []*mgmProto.RemotePeerConfig) error {
 	var modified []*mgmProto.RemotePeerConfig
 	for _, p := range peersUpdate {
 		peerPubKey := p.GetWgPubKey()
+		currentPeer, ok := e.peerStore.PeerConn(peerPubKey)
+		if !ok {
+			continue
+		}
+
+		if currentPeer.AgentVersionString() != p.AgentVersion {
+			modified = append(modified, p)
+			continue
+		}
+
 		allowedIPs, ok := e.peerStore.AllowedIPs(peerPubKey)
 		if !ok {
 			continue
@@ -559,8 +585,7 @@ func (e *Engine) modifyPeers(peersUpdate []*mgmProto.RemotePeerConfig) error {
 			continue
 		}
 
-		err := e.statusRecorder.UpdatePeerFQDN(peerPubKey, p.GetFqdn())
-		if err != nil {
+		if err := e.statusRecorder.UpdatePeerFQDN(peerPubKey, p.GetFqdn()); err != nil {
 			log.Warnf("error updating peer's %s fqdn in the status recorder, got error: %v", peerPubKey, err)
 		}
 	}
@@ -621,16 +646,11 @@ func (e *Engine) removePeer(peerKey string) error {
 		e.sshServer.RemoveAuthorizedKey(peerKey)
 	}
 
-	defer func() {
-		err := e.statusRecorder.RemovePeer(peerKey)
-		if err != nil {
-			log.Warnf("received error when removing peer %s from status recorder: %v", peerKey, err)
-		}
-	}()
+	e.connMgr.RemovePeerConn(peerKey)
 
-	conn, exists := e.peerStore.Remove(peerKey)
-	if exists {
-		conn.Close()
+	err := e.statusRecorder.RemovePeer(peerKey)
+	if err != nil {
+		log.Warnf("received error when removing peer %s from status recorder: %v", peerKey, err)
 	}
 	return nil
 }
@@ -780,56 +800,58 @@ func isNil(server nbssh.Server) bool {
 }
 
 func (e *Engine) updateSSH(sshConf *mgmProto.SSHConfig) error {
+	if e.config.BlockInbound {
+		log.Infof("SSH server is disabled because inbound connections are blocked")
+		return nil
+	}
 
 	if !e.config.ServerSSHAllowed {
-		log.Warnf("running SSH server is not permitted")
+		log.Info("SSH server is not enabled")
 		return nil
-	} else {
+	}
 
-		if sshConf.GetSshEnabled() {
-			if runtime.GOOS == "windows" {
-				log.Warnf("running SSH server on %s is not supported", runtime.GOOS)
-				return nil
+	if sshConf.GetSshEnabled() {
+		if runtime.GOOS == "windows" {
+			log.Warnf("running SSH server on %s is not supported", runtime.GOOS)
+			return nil
+		}
+		// start SSH server if it wasn't running
+		if isNil(e.sshServer) {
+			listenAddr := fmt.Sprintf("%s:%d", e.wgInterface.Address().IP.String(), nbssh.DefaultSSHPort)
+			if nbnetstack.IsEnabled() {
+				listenAddr = fmt.Sprintf("127.0.0.1:%d", nbssh.DefaultSSHPort)
 			}
-			// start SSH server if it wasn't running
-			if isNil(e.sshServer) {
-				listenAddr := fmt.Sprintf("%s:%d", e.wgInterface.Address().IP.String(), nbssh.DefaultSSHPort)
-				if nbnetstack.IsEnabled() {
-					listenAddr = fmt.Sprintf("127.0.0.1:%d", nbssh.DefaultSSHPort)
-				}
-				// nil sshServer means it has not yet been started
-				var err error
-				e.sshServer, err = e.sshServerFunc(e.config.SSHKey, listenAddr)
+			// nil sshServer means it has not yet been started
+			var err error
+			e.sshServer, err = e.sshServerFunc(e.config.SSHKey, listenAddr)
 
-				if err != nil {
-					return fmt.Errorf("create ssh server: %w", err)
-				}
-				go func() {
-					// blocking
-					err = e.sshServer.Start()
-					if err != nil {
-						// will throw error when we stop it even if it is a graceful stop
-						log.Debugf("stopped SSH server with error %v", err)
-					}
-					e.syncMsgMux.Lock()
-					defer e.syncMsgMux.Unlock()
-					e.sshServer = nil
-					log.Infof("stopped SSH server")
-				}()
-			} else {
-				log.Debugf("SSH server is already running")
-			}
-		} else if !isNil(e.sshServer) {
-			// Disable SSH server request, so stop it if it was running
-			err := e.sshServer.Stop()
 			if err != nil {
-				log.Warnf("failed to stop SSH server %v", err)
+				return fmt.Errorf("create ssh server: %w", err)
 			}
-			e.sshServer = nil
+			go func() {
+				// blocking
+				err = e.sshServer.Start()
+				if err != nil {
+					// will throw error when we stop it even if it is a graceful stop
+					log.Debugf("stopped SSH server with error %v", err)
+				}
+				e.syncMsgMux.Lock()
+				defer e.syncMsgMux.Unlock()
+				e.sshServer = nil
+				log.Infof("stopped SSH server")
+			}()
+		} else {
+			log.Debugf("SSH server is already running")
 		}
-		return nil
-
+	} else if !isNil(e.sshServer) {
+		// Disable SSH server request, so stop it if it was running
+		err := e.sshServer.Stop()
+		if err != nil {
+			log.Warnf("failed to stop SSH server %v", err)
+		}
+		e.sshServer = nil
 	}
+	return nil
 }
 
 func (e *Engine) updateConfig(conf *mgmProto.PeerConfig) error {
@@ -952,31 +974,53 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
 		return nil
 	}
 
+	if err := e.connMgr.UpdatedRemoteFeatureFlag(e.ctx, networkMap.GetPeerConfig().GetLazyConnectionEnabled()); err != nil {
+		log.Errorf("failed to update lazy connection feature flag: %v", err)
+	}
+
 	if e.firewall != nil {
 		if localipfw, ok := e.firewall.(localIpUpdater); ok {
 			if err := localipfw.UpdateLocalIPs(); err != nil {
 				log.Errorf("failed to update local IPs: %v", err)
 			}
 		}
+
+		// If we got empty rules list but management did not set the networkMap.FirewallRulesIsEmpty flag,
+		// then the mgmt server is older than the client, and we need to allow all traffic for routes.
+		// This needs to be toggled before applying routes.
+		isLegacy := len(networkMap.RoutesFirewallRules) == 0 && !networkMap.RoutesFirewallRulesIsEmpty
+		if err := e.firewall.SetLegacyManagement(isLegacy); err != nil {
+			log.Errorf("failed to set legacy management flag: %v", err)
+		}
+	}
+
+	protoDNSConfig := networkMap.GetDNSConfig()
+	if protoDNSConfig == nil {
+		protoDNSConfig = &mgmProto.DNSConfig{}
+	}
+
+	if err := e.dnsServer.UpdateDNSServer(serial, toDNSConfig(protoDNSConfig, e.wgInterface.Address().Network)); err != nil {
+		log.Errorf("failed to update dns server, err: %v", err)
 	}
 
-	// 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)
+		log.Errorf("failed to update routes: %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 {
+	forwardingRules, err := e.updateForwardRules(networkMap.GetForwardingRules())
+	if err != nil {
 		log.Errorf("failed to update forward rules, err: %v", err)
 	}
 
@@ -1022,14 +1066,9 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
 		}
 	}
 
-	protoDNSConfig := networkMap.GetDNSConfig()
-	if protoDNSConfig == nil {
-		protoDNSConfig = &mgmProto.DNSConfig{}
-	}
-
-	if err := e.dnsServer.UpdateDNSServer(serial, toDNSConfig(protoDNSConfig, e.wgInterface.Address().Network)); err != nil {
-		log.Errorf("failed to update dns server, err: %v", err)
-	}
+	// must set the exclude list after the peers are added. Without it the manager can not figure out the peers parameters from the store
+	excludedLazyPeers := e.toExcludedLazyPeers(routes, forwardingRules, networkMap.GetRemotePeers())
+	e.connMgr.SetExcludeList(excludedLazyPeers)
 
 	e.networkSerial = serial
 
@@ -1065,7 +1104,7 @@ func toRoutes(protoRoutes []*mgmProto.Route) []*route.Route {
 
 		convertedRoute := &route.Route{
 			ID:          route.ID(protoRoute.ID),
-			Network:     prefix,
+			Network:     prefix.Masked(),
 			Domains:     domain.FromPunycodeList(protoRoute.Domains),
 			NetID:       route.NetID(protoRoute.NetID),
 			NetworkType: route.NetworkType(protoRoute.NetworkType),
@@ -1079,32 +1118,27 @@ 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 {
+func toDNSConfig(protoDNSConfig *mgmProto.DNSConfig, network netip.Prefix) nbdns.Config {
 	dnsUpdate := nbdns.Config{
 		ServiceEnable:    protoDNSConfig.GetServiceEnable(),
 		CustomZones:      make([]nbdns.CustomZone, 0),
@@ -1160,7 +1194,7 @@ func (e *Engine) updateOfflinePeers(offlinePeers []*mgmProto.RemotePeerConfig) {
 			IP:               strings.Join(offlinePeer.GetAllowedIps(), ","),
 			PubKey:           offlinePeer.GetWgPubKey(),
 			FQDN:             offlinePeer.GetFqdn(),
-			ConnStatus:       peer.StatusDisconnected,
+			ConnStatus:       peer.StatusIdle,
 			ConnStatusUpdate: time.Now(),
 			Mux:              new(sync.RWMutex),
 		}
@@ -1196,12 +1230,17 @@ func (e *Engine) addNewPeer(peerConfig *mgmProto.RemotePeerConfig) error {
 		peerIPs = append(peerIPs, allowedNetIP)
 	}
 
-	conn, err := e.createPeerConn(peerKey, peerIPs)
+	conn, err := e.createPeerConn(peerKey, peerIPs, peerConfig.AgentVersion)
 	if err != nil {
 		return fmt.Errorf("create peer connection: %w", err)
 	}
 
-	if ok := e.peerStore.AddPeerConn(peerKey, conn); !ok {
+	err = e.statusRecorder.AddPeer(peerKey, peerConfig.Fqdn, peerIPs[0].Addr().String())
+	if err != nil {
+		log.Warnf("error adding peer %s to status recorder, got error: %v", peerKey, err)
+	}
+
+	if exists := e.connMgr.AddPeerConn(e.ctx, peerKey, conn); exists {
 		conn.Close()
 		return fmt.Errorf("peer already exists: %s", peerKey)
 	}
@@ -1210,17 +1249,10 @@ func (e *Engine) addNewPeer(peerConfig *mgmProto.RemotePeerConfig) error {
 		conn.AddBeforeAddPeerHook(e.beforePeerHook)
 		conn.AddAfterRemovePeerHook(e.afterPeerHook)
 	}
-
-	err = e.statusRecorder.AddPeer(peerKey, peerConfig.Fqdn)
-	if err != nil {
-		log.Warnf("error adding peer %s to status recorder, got error: %v", peerKey, err)
-	}
-
-	conn.Open()
 	return nil
 }
 
-func (e *Engine) createPeerConn(pubKey string, allowedIPs []netip.Prefix) (*peer.Conn, error) {
+func (e *Engine) createPeerConn(pubKey string, allowedIPs []netip.Prefix, agentVersion string) (*peer.Conn, error) {
 	log.Debugf("creating peer connection %s", pubKey)
 
 	wgConfig := peer.WgConfig{
@@ -1234,11 +1266,12 @@ func (e *Engine) createPeerConn(pubKey string, allowedIPs []netip.Prefix) (*peer
 	// 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,
+		Key:          pubKey,
+		LocalKey:     e.config.WgPrivateKey.PublicKey().String(),
+		AgentVersion: agentVersion,
+		Timeout:      timeout,
+		WgConfig:     wgConfig,
+		LocalWgPort:  e.config.WgPort,
 		RosenpassConfig: peer.RosenpassConfig{
 			PubKey:         e.getRosenpassPubKey(),
 			Addr:           e.getRosenpassAddr(),
@@ -1254,7 +1287,16 @@ func (e *Engine) createPeerConn(pubKey string, allowedIPs []netip.Prefix) (*peer
 		},
 	}
 
-	peerConn, err := peer.NewConn(e.ctx, config, e.statusRecorder, e.signaler, e.mobileDep.IFaceDiscover, e.relayManager, e.srWatcher, e.connSemaphore)
+	serviceDependencies := peer.ServiceDependencies{
+		StatusRecorder:     e.statusRecorder,
+		Signaler:           e.signaler,
+		IFaceDiscover:      e.mobileDep.IFaceDiscover,
+		RelayManager:       e.relayManager,
+		SrWatcher:          e.srWatcher,
+		Semaphore:          e.connSemaphore,
+		PeerConnDispatcher: e.peerConnDispatcher,
+	}
+	peerConn, err := peer.NewConn(config, serviceDependencies)
 	if err != nil {
 		return nil, err
 	}
@@ -1275,7 +1317,7 @@ func (e *Engine) receiveSignalEvents() {
 			e.syncMsgMux.Lock()
 			defer e.syncMsgMux.Unlock()
 
-			conn, ok := e.peerStore.PeerConn(msg.Key)
+			conn, ok := e.connMgr.OnSignalMsg(e.ctx, msg.Key)
 			if !ok {
 				return fmt.Errorf("wrongly addressed message %s", msg.Key)
 			}
@@ -1411,6 +1453,7 @@ func (e *Engine) close() {
 			log.Errorf("failed closing Netbird interface %s %v", e.config.WgIfaceName, err)
 		}
 		e.wgInterface = nil
+		e.statusRecorder.SetWgIface(nil)
 	}
 
 	if !isNil(e.sshServer) {
@@ -1583,13 +1626,39 @@ func (e *Engine) getRosenpassAddr() string {
 // RunHealthProbes executes health checks for Signal, Management, Relay and WireGuard services
 // and updates the status recorder with the latest states.
 func (e *Engine) RunHealthProbes() bool {
+	e.syncMsgMux.Lock()
+
 	signalHealthy := e.signal.IsHealthy()
 	log.Debugf("signal health check: healthy=%t", signalHealthy)
 
 	managementHealthy := e.mgmClient.IsHealthy()
 	log.Debugf("management health check: healthy=%t", managementHealthy)
 
-	results := append(e.probeSTUNs(), e.probeTURNs()...)
+	stuns := slices.Clone(e.STUNs)
+	turns := slices.Clone(e.TURNs)
+
+	if e.wgInterface != nil {
+		stats, err := e.wgInterface.GetStats()
+		if err != nil {
+			log.Warnf("failed to get wireguard stats: %v", err)
+			e.syncMsgMux.Unlock()
+			return false
+		}
+		for _, key := range e.peerStore.PeersPubKey() {
+			// wgStats could be zero value, in which case we just reset the stats
+			wgStats, ok := stats[key]
+			if !ok {
+				continue
+			}
+			if err := e.statusRecorder.UpdateWireGuardPeerState(key, wgStats); err != nil {
+				log.Debugf("failed to update wg stats for peer %s: %s", key, err)
+			}
+		}
+	}
+
+	e.syncMsgMux.Unlock()
+
+	results := e.probeICE(stuns, turns)
 	e.statusRecorder.UpdateRelayStates(results)
 
 	relayHealthy := true
@@ -1601,37 +1670,16 @@ func (e *Engine) RunHealthProbes() bool {
 	}
 	log.Debugf("relay health check: healthy=%t", relayHealthy)
 
-	for _, key := range e.peerStore.PeersPubKey() {
-		wgStats, err := e.wgInterface.GetStats(key)
-		if err != nil {
-			log.Debugf("failed to get wg stats for peer %s: %s", key, err)
-			continue
-		}
-		// wgStats could be zero value, in which case we just reset the stats
-		if err := e.statusRecorder.UpdateWireGuardPeerState(key, wgStats); err != nil {
-			log.Debugf("failed to update wg stats for peer %s: %s", key, err)
-		}
-	}
-
 	allHealthy := signalHealthy && managementHealthy && relayHealthy
 	log.Debugf("all health checks completed: healthy=%t", allHealthy)
 	return allHealthy
 }
 
-func (e *Engine) probeSTUNs() []relay.ProbeResult {
-	e.syncMsgMux.Lock()
-	stuns := slices.Clone(e.STUNs)
-	e.syncMsgMux.Unlock()
-
-	return relay.ProbeAll(e.ctx, relay.ProbeSTUN, stuns)
-}
-
-func (e *Engine) probeTURNs() []relay.ProbeResult {
-	e.syncMsgMux.Lock()
-	turns := slices.Clone(e.TURNs)
-	e.syncMsgMux.Unlock()
-
-	return relay.ProbeAll(e.ctx, relay.ProbeTURN, turns)
+func (e *Engine) probeICE(stuns, turns []*stun.URI) []relay.ProbeResult {
+	return append(
+		relay.ProbeAll(e.ctx, relay.ProbeSTUN, stuns),
+		relay.ProbeAll(e.ctx, relay.ProbeSTUN, turns)...,
+	)
 }
 
 // restartEngine restarts the engine by cancelling the client context
@@ -1743,15 +1791,22 @@ func (e *Engine) GetLatestNetworkMap() (*mgmProto.NetworkMap, error) {
 }
 
 // GetWgAddr returns the wireguard address
-func (e *Engine) GetWgAddr() net.IP {
+func (e *Engine) GetWgAddr() netip.Addr {
 	if e.wgInterface == nil {
-		return nil
+		return netip.Addr{}
 	}
 	return e.wgInterface.Address().IP
 }
 
 // updateDNSForwarder start or stop the DNS forwarder based on the domains and the feature flag
-func (e *Engine) updateDNSForwarder(enabled bool, domains []string, resIds map[string]string) {
+func (e *Engine) updateDNSForwarder(
+	enabled bool,
+	fwdEntries []*dnsfwd.ForwarderEntry,
+) {
+	if e.config.DisableServerRoutes {
+		return
+	}
+
 	if !enabled {
 		if e.dnsForwardMgr == nil {
 			return
@@ -1762,18 +1817,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")
@@ -1807,29 +1862,24 @@ func (e *Engine) Address() (netip.Addr, error) {
 		return netip.Addr{}, errors.New("wireguard interface not initialized")
 	}
 
-	addr := e.wgInterface.Address()
-	ip, ok := netip.AddrFromSlice(addr.IP)
-	if !ok {
-		return netip.Addr{}, errors.New("failed to convert address to netip.Addr")
-	}
-	return ip.Unmap(), nil
+	return e.wgInterface.Address().IP, nil
 }
 
-func (e *Engine) updateForwardRules(rules []*mgmProto.ForwardingRule) error {
+func (e *Engine) updateForwardRules(rules []*mgmProto.ForwardingRule) ([]firewallManager.ForwardRule, error) {
 	if e.firewall == nil {
 		log.Warn("firewall is disabled, not updating forwarding rules")
-		return nil
+		return nil, nil
 	}
 
 	if len(rules) == 0 {
 		if e.ingressGatewayMgr == nil {
-			return nil
+			return nil, nil
 		}
 
 		err := e.ingressGatewayMgr.Close()
 		e.ingressGatewayMgr = nil
 		e.statusRecorder.SetIngressGwMgr(nil)
-		return err
+		return nil, err
 	}
 
 	if e.ingressGatewayMgr == nil {
@@ -1880,7 +1930,35 @@ func (e *Engine) updateForwardRules(rules []*mgmProto.ForwardingRule) error {
 		log.Errorf("failed to update forwarding rules: %v", err)
 	}
 
-	return nberrors.FormatErrorOrNil(merr)
+	return forwardingRules, nberrors.FormatErrorOrNil(merr)
+}
+
+func (e *Engine) toExcludedLazyPeers(routes []*route.Route, rules []firewallManager.ForwardRule, peers []*mgmProto.RemotePeerConfig) map[string]bool {
+	excludedPeers := make(map[string]bool)
+	for _, r := range routes {
+		if r.Peer == "" {
+			continue
+		}
+		if !excludedPeers[r.Peer] {
+			log.Infof("exclude router peer from lazy connection: %s", r.Peer)
+			excludedPeers[r.Peer] = true
+		}
+	}
+
+	for _, r := range rules {
+		ip := r.TranslatedAddress
+		for _, p := range peers {
+			for _, allowedIP := range p.GetAllowedIps() {
+				if allowedIP != ip.String() {
+					continue
+				}
+				log.Infof("exclude forwarder peer from lazy connection: %s", p.GetWgPubKey())
+				excludedPeers[p.GetWgPubKey()] = true
+			}
+		}
+	}
+
+	return excludedPeers
 }
 
 // isChecksEqual checks if two slices of checks are equal.
diff --git a/client/internal/iface_common.go b/client/internal/iface_common.go
index ffeffaf4..95bf146f 100644
--- a/client/internal/iface_common.go
+++ b/client/internal/iface_common.go
@@ -35,6 +35,7 @@ type wgIfaceBase interface {
 	GetFilter() device.PacketFilter
 	GetDevice() *device.FilteredDevice
 	GetWGDevice() *wgdevice.Device
-	GetStats(peerKey string) (configurer.WGStats, error)
+	GetStats() (map[string]configurer.WGStats, error)
 	GetNet() *netstack.Net
+	FullStats() (*configurer.Stats, error)
 }
diff --git a/client/internal/lazyconn/activity/listen_ip.go b/client/internal/lazyconn/activity/listen_ip.go
new file mode 100644
index 00000000..aff73c5d
--- /dev/null
+++ b/client/internal/lazyconn/activity/listen_ip.go
@@ -0,0 +1,9 @@
+//go:build !linux || android
+
+package activity
+
+import "net"
+
+var (
+	listenIP = net.IP{127, 0, 0, 1}
+)
diff --git a/client/internal/lazyconn/activity/listen_ip_linux.go b/client/internal/lazyconn/activity/listen_ip_linux.go
new file mode 100644
index 00000000..98beb963
--- /dev/null
+++ b/client/internal/lazyconn/activity/listen_ip_linux.go
@@ -0,0 +1,10 @@
+//go:build !android
+
+package activity
+
+import "net"
+
+var (
+	// use this ip to avoid eBPF proxy congestion
+	listenIP = net.IP{127, 0, 1, 1}
+)
diff --git a/client/internal/lazyconn/activity/listener.go b/client/internal/lazyconn/activity/listener.go
new file mode 100644
index 00000000..1ef48416
--- /dev/null
+++ b/client/internal/lazyconn/activity/listener.go
@@ -0,0 +1,106 @@
+package activity
+
+import (
+	"fmt"
+	"net"
+	"sync"
+	"sync/atomic"
+
+	log "github.com/sirupsen/logrus"
+
+	"github.com/netbirdio/netbird/client/internal/lazyconn"
+)
+
+// Listener it is not a thread safe implementation, do not call Close before ReadPackets. It will cause blocking
+type Listener struct {
+	wgIface  lazyconn.WGIface
+	peerCfg  lazyconn.PeerConfig
+	conn     *net.UDPConn
+	endpoint *net.UDPAddr
+	done     sync.Mutex
+
+	isClosed atomic.Bool // use to avoid error log when closing the listener
+}
+
+func NewListener(wgIface lazyconn.WGIface, cfg lazyconn.PeerConfig) (*Listener, error) {
+	d := &Listener{
+		wgIface: wgIface,
+		peerCfg: cfg,
+	}
+
+	conn, err := d.newConn()
+	if err != nil {
+		return nil, fmt.Errorf("failed to creating activity listener: %v", err)
+	}
+	d.conn = conn
+	d.endpoint = conn.LocalAddr().(*net.UDPAddr)
+
+	if err := d.createEndpoint(); err != nil {
+		return nil, err
+	}
+	d.done.Lock()
+	cfg.Log.Infof("created activity listener: %s", conn.LocalAddr().(*net.UDPAddr).String())
+	return d, nil
+}
+
+func (d *Listener) ReadPackets() {
+	for {
+		n, remoteAddr, err := d.conn.ReadFromUDP(make([]byte, 1))
+		if err != nil {
+			if d.isClosed.Load() {
+				d.peerCfg.Log.Debugf("exit from activity listener")
+			} else {
+				d.peerCfg.Log.Errorf("failed to read from activity listener: %s", err)
+			}
+			break
+		}
+
+		if n < 1 {
+			d.peerCfg.Log.Warnf("received %d bytes from %s, too short", n, remoteAddr)
+			continue
+		}
+		break
+	}
+
+	if err := d.removeEndpoint(); err != nil {
+		d.peerCfg.Log.Errorf("failed to remove endpoint: %s", err)
+	}
+
+	_ = d.conn.Close() // do not care err because some cases it will return "use of closed network connection"
+	d.done.Unlock()
+}
+
+func (d *Listener) Close() {
+	d.peerCfg.Log.Infof("closing listener: %s", d.conn.LocalAddr().String())
+	d.isClosed.Store(true)
+
+	if err := d.conn.Close(); err != nil {
+		d.peerCfg.Log.Errorf("failed to close UDP listener: %s", err)
+	}
+	d.done.Lock()
+}
+
+func (d *Listener) removeEndpoint() error {
+	d.peerCfg.Log.Debugf("removing lazy endpoint: %s", d.endpoint.String())
+	return d.wgIface.RemovePeer(d.peerCfg.PublicKey)
+}
+
+func (d *Listener) createEndpoint() error {
+	d.peerCfg.Log.Debugf("creating lazy endpoint: %s", d.endpoint.String())
+	return d.wgIface.UpdatePeer(d.peerCfg.PublicKey, d.peerCfg.AllowedIPs, 0, d.endpoint, nil)
+}
+
+func (d *Listener) newConn() (*net.UDPConn, error) {
+	addr := &net.UDPAddr{
+		Port: 0,
+		IP:   listenIP,
+	}
+
+	conn, err := net.ListenUDP("udp", addr)
+	if err != nil {
+		log.Errorf("failed to create activity listener on %s: %s", addr, err)
+		return nil, err
+	}
+
+	return conn, nil
+}
diff --git a/client/internal/lazyconn/activity/manager.go b/client/internal/lazyconn/activity/manager.go
new file mode 100644
index 00000000..e18b9646
--- /dev/null
+++ b/client/internal/lazyconn/activity/manager.go
@@ -0,0 +1,95 @@
+package activity
+
+import (
+	"sync"
+
+	log "github.com/sirupsen/logrus"
+
+	"github.com/netbirdio/netbird/client/internal/lazyconn"
+	peerid "github.com/netbirdio/netbird/client/internal/peer/id"
+)
+
+type Manager struct {
+	OnActivityChan chan peerid.ConnID
+
+	wgIface lazyconn.WGIface
+
+	peers map[peerid.ConnID]*Listener
+	done  chan struct{}
+
+	mu sync.Mutex
+}
+
+func NewManager(wgIface lazyconn.WGIface) *Manager {
+	m := &Manager{
+		OnActivityChan: make(chan peerid.ConnID, 1),
+		wgIface:        wgIface,
+		peers:          make(map[peerid.ConnID]*Listener),
+		done:           make(chan struct{}),
+	}
+	return m
+}
+
+func (m *Manager) MonitorPeerActivity(peerCfg lazyconn.PeerConfig) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	if _, ok := m.peers[peerCfg.PeerConnID]; ok {
+		log.Warnf("activity listener already exists for: %s", peerCfg.PublicKey)
+		return nil
+	}
+
+	listener, err := NewListener(m.wgIface, peerCfg)
+	if err != nil {
+		return err
+	}
+	m.peers[peerCfg.PeerConnID] = listener
+
+	go m.waitForTraffic(listener, peerCfg.PeerConnID)
+	return nil
+}
+
+func (m *Manager) RemovePeer(log *log.Entry, peerConnID peerid.ConnID) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	listener, ok := m.peers[peerConnID]
+	if !ok {
+		return
+	}
+	log.Debugf("removing activity listener")
+	delete(m.peers, peerConnID)
+	listener.Close()
+}
+
+func (m *Manager) Close() {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	close(m.done)
+	for peerID, listener := range m.peers {
+		delete(m.peers, peerID)
+		listener.Close()
+	}
+}
+
+func (m *Manager) waitForTraffic(listener *Listener, peerConnID peerid.ConnID) {
+	listener.ReadPackets()
+
+	m.mu.Lock()
+	if _, ok := m.peers[peerConnID]; !ok {
+		m.mu.Unlock()
+		return
+	}
+	delete(m.peers, peerConnID)
+	m.mu.Unlock()
+
+	m.notify(peerConnID)
+}
+
+func (m *Manager) notify(peerConnID peerid.ConnID) {
+	select {
+	case <-m.done:
+	case m.OnActivityChan <- peerConnID:
+	}
+}
diff --git a/client/internal/lazyconn/doc.go b/client/internal/lazyconn/doc.go
new file mode 100644
index 00000000..156520bd
--- /dev/null
+++ b/client/internal/lazyconn/doc.go
@@ -0,0 +1,32 @@
+/*
+Package lazyconn provides mechanisms for managing lazy connections, which activate on demand to optimize resource usage and establish connections efficiently.
+
+## Overview
+
+The package includes a `Manager` component responsible for:
+- Managing lazy connections activated on-demand
+- Managing inactivity monitors for lazy connections (based on peer disconnection events)
+- Maintaining a list of excluded peers that should always have permanent connections
+- Handling remote peer connection initiatives based on peer signaling
+
+## Thread-Safe Operations
+
+The `Manager` ensures thread safety across multiple operations, categorized by caller:
+
+- **Engine (single goroutine)**:
+  - `AddPeer`: Adds a peer to the connection manager.
+  - `RemovePeer`: Removes a peer from the connection manager.
+  - `ActivatePeer`: Activates a lazy connection for a peer. This come from Signal client
+  - `ExcludePeer`: Marks peers for a permanent connection. Like router peers and other peers that should always have a connection.
+
+- **Connection Dispatcher (any peer routine)**:
+  - `onPeerConnected`: Suspend the inactivity monitor for an active peer connection.
+  - `onPeerDisconnected`: Starts the inactivity monitor for a disconnected peer.
+
+- **Activity Manager**:
+  - `onPeerActivity`: Run peer.Open(context).
+
+- **Inactivity Monitor**:
+  - `onPeerInactivityTimedOut`: Close peer connection and restart activity monitor.
+*/
+package lazyconn
diff --git a/client/internal/lazyconn/env.go b/client/internal/lazyconn/env.go
new file mode 100644
index 00000000..649d1cd6
--- /dev/null
+++ b/client/internal/lazyconn/env.go
@@ -0,0 +1,26 @@
+package lazyconn
+
+import (
+	"os"
+	"strconv"
+
+	log "github.com/sirupsen/logrus"
+)
+
+const (
+	EnvEnableLazyConn      = "NB_ENABLE_EXPERIMENTAL_LAZY_CONN"
+	EnvInactivityThreshold = "NB_LAZY_CONN_INACTIVITY_THRESHOLD"
+)
+
+func IsLazyConnEnabledByEnv() bool {
+	val := os.Getenv(EnvEnableLazyConn)
+	if val == "" {
+		return false
+	}
+	enabled, err := strconv.ParseBool(val)
+	if err != nil {
+		log.Warnf("failed to parse %s: %v", EnvEnableLazyConn, err)
+		return false
+	}
+	return enabled
+}
diff --git a/client/internal/lazyconn/inactivity/inactivity.go b/client/internal/lazyconn/inactivity/inactivity.go
new file mode 100644
index 00000000..a30c1846
--- /dev/null
+++ b/client/internal/lazyconn/inactivity/inactivity.go
@@ -0,0 +1,70 @@
+package inactivity
+
+import (
+	"context"
+	"time"
+
+	peer "github.com/netbirdio/netbird/client/internal/peer/id"
+)
+
+const (
+	DefaultInactivityThreshold = 60 * time.Minute // idle after 1 hour inactivity
+	MinimumInactivityThreshold = 3 * time.Minute
+)
+
+type Monitor struct {
+	id                  peer.ConnID
+	timer               *time.Timer
+	cancel              context.CancelFunc
+	inactivityThreshold time.Duration
+}
+
+func NewInactivityMonitor(peerID peer.ConnID, threshold time.Duration) *Monitor {
+	i := &Monitor{
+		id:                  peerID,
+		timer:               time.NewTimer(0),
+		inactivityThreshold: threshold,
+	}
+	i.timer.Stop()
+	return i
+}
+
+func (i *Monitor) Start(ctx context.Context, timeoutChan chan peer.ConnID) {
+	i.timer.Reset(i.inactivityThreshold)
+	defer i.timer.Stop()
+
+	ctx, i.cancel = context.WithCancel(ctx)
+	defer func() {
+		defer i.cancel()
+		select {
+		case <-i.timer.C:
+		default:
+		}
+	}()
+
+	select {
+	case <-i.timer.C:
+		select {
+		case timeoutChan <- i.id:
+		case <-ctx.Done():
+			return
+		}
+	case <-ctx.Done():
+		return
+	}
+}
+
+func (i *Monitor) Stop() {
+	if i.cancel == nil {
+		return
+	}
+	i.cancel()
+}
+
+func (i *Monitor) PauseTimer() {
+	i.timer.Stop()
+}
+
+func (i *Monitor) ResetTimer() {
+	i.timer.Reset(i.inactivityThreshold)
+}
diff --git a/client/internal/lazyconn/manager/manager.go b/client/internal/lazyconn/manager/manager.go
new file mode 100644
index 00000000..af12a73e
--- /dev/null
+++ b/client/internal/lazyconn/manager/manager.go
@@ -0,0 +1,404 @@
+package manager
+
+import (
+	"context"
+	"sync"
+	"time"
+
+	log "github.com/sirupsen/logrus"
+
+	"github.com/netbirdio/netbird/client/internal/lazyconn"
+	"github.com/netbirdio/netbird/client/internal/lazyconn/activity"
+	"github.com/netbirdio/netbird/client/internal/lazyconn/inactivity"
+	"github.com/netbirdio/netbird/client/internal/peer/dispatcher"
+	peerid "github.com/netbirdio/netbird/client/internal/peer/id"
+	"github.com/netbirdio/netbird/client/internal/peerstore"
+)
+
+const (
+	watcherActivity watcherType = iota
+	watcherInactivity
+)
+
+type watcherType int
+
+type managedPeer struct {
+	peerCfg         *lazyconn.PeerConfig
+	expectedWatcher watcherType
+}
+
+type Config struct {
+	InactivityThreshold *time.Duration
+}
+
+// Manager manages lazy connections
+// It is responsible for:
+// - Managing lazy connections activated on-demand
+// - Managing inactivity monitors for lazy connections (based on peer disconnection events)
+// - Maintaining a list of excluded peers that should always have permanent connections
+// - Handling connection establishment based on peer signaling
+type Manager struct {
+	peerStore           *peerstore.Store
+	connStateDispatcher *dispatcher.ConnectionDispatcher
+	inactivityThreshold time.Duration
+
+	connStateListener    *dispatcher.ConnectionListener
+	managedPeers         map[string]*lazyconn.PeerConfig
+	managedPeersByConnID map[peerid.ConnID]*managedPeer
+	excludes             map[string]lazyconn.PeerConfig
+	managedPeersMu       sync.Mutex
+
+	activityManager    *activity.Manager
+	inactivityMonitors map[peerid.ConnID]*inactivity.Monitor
+
+	cancel     context.CancelFunc
+	onInactive chan peerid.ConnID
+}
+
+func NewManager(config Config, peerStore *peerstore.Store, wgIface lazyconn.WGIface, connStateDispatcher *dispatcher.ConnectionDispatcher) *Manager {
+	log.Infof("setup lazy connection service")
+	m := &Manager{
+		peerStore:            peerStore,
+		connStateDispatcher:  connStateDispatcher,
+		inactivityThreshold:  inactivity.DefaultInactivityThreshold,
+		managedPeers:         make(map[string]*lazyconn.PeerConfig),
+		managedPeersByConnID: make(map[peerid.ConnID]*managedPeer),
+		excludes:             make(map[string]lazyconn.PeerConfig),
+		activityManager:      activity.NewManager(wgIface),
+		inactivityMonitors:   make(map[peerid.ConnID]*inactivity.Monitor),
+		onInactive:           make(chan peerid.ConnID),
+	}
+
+	if config.InactivityThreshold != nil {
+		if *config.InactivityThreshold >= inactivity.MinimumInactivityThreshold {
+			m.inactivityThreshold = *config.InactivityThreshold
+		} else {
+			log.Warnf("inactivity threshold is too low, using %v", m.inactivityThreshold)
+		}
+	}
+
+	m.connStateListener = &dispatcher.ConnectionListener{
+		OnConnected:    m.onPeerConnected,
+		OnDisconnected: m.onPeerDisconnected,
+	}
+
+	connStateDispatcher.AddListener(m.connStateListener)
+
+	return m
+}
+
+// Start starts the manager and listens for peer activity and inactivity events
+func (m *Manager) Start(ctx context.Context) {
+	defer m.close()
+
+	ctx, m.cancel = context.WithCancel(ctx)
+	for {
+		select {
+		case <-ctx.Done():
+			return
+		case peerConnID := <-m.activityManager.OnActivityChan:
+			m.onPeerActivity(ctx, peerConnID)
+		case peerConnID := <-m.onInactive:
+			m.onPeerInactivityTimedOut(peerConnID)
+		}
+	}
+}
+
+// ExcludePeer marks peers for a permanent connection
+// It removes peers from the managed list if they are added to the exclude list
+// Adds them back to the managed list and start the inactivity listener if they are removed from the exclude list. In
+// this case, we suppose that the connection status is connected or connecting.
+// If the peer is not exists yet in the managed list then the responsibility is the upper layer to call the AddPeer function
+func (m *Manager) ExcludePeer(ctx context.Context, peerConfigs []lazyconn.PeerConfig) []string {
+	m.managedPeersMu.Lock()
+	defer m.managedPeersMu.Unlock()
+
+	added := make([]string, 0)
+	excludes := make(map[string]lazyconn.PeerConfig, len(peerConfigs))
+
+	for _, peerCfg := range peerConfigs {
+		log.Infof("update excluded lazy connection list with peer: %s", peerCfg.PublicKey)
+		excludes[peerCfg.PublicKey] = peerCfg
+	}
+
+	// if a peer is newly added to the exclude list, remove from the managed peers list
+	for pubKey, peerCfg := range excludes {
+		if _, wasExcluded := m.excludes[pubKey]; wasExcluded {
+			continue
+		}
+
+		added = append(added, pubKey)
+		peerCfg.Log.Infof("peer newly added to lazy connection exclude list")
+		m.removePeer(pubKey)
+	}
+
+	// if a peer has been removed from exclude list then it should be added to the managed peers
+	for pubKey, peerCfg := range m.excludes {
+		if _, stillExcluded := excludes[pubKey]; stillExcluded {
+			continue
+		}
+
+		peerCfg.Log.Infof("peer removed from lazy connection exclude list")
+
+		if err := m.addActivePeer(ctx, peerCfg); err != nil {
+			log.Errorf("failed to add peer to lazy connection manager: %s", err)
+			continue
+		}
+	}
+
+	m.excludes = excludes
+	return added
+}
+
+func (m *Manager) AddPeer(peerCfg lazyconn.PeerConfig) (bool, error) {
+	m.managedPeersMu.Lock()
+	defer m.managedPeersMu.Unlock()
+
+	peerCfg.Log.Debugf("adding peer to lazy connection manager")
+
+	_, exists := m.excludes[peerCfg.PublicKey]
+	if exists {
+		return true, nil
+	}
+
+	if _, ok := m.managedPeers[peerCfg.PublicKey]; ok {
+		peerCfg.Log.Warnf("peer already managed")
+		return false, nil
+	}
+
+	if err := m.activityManager.MonitorPeerActivity(peerCfg); err != nil {
+		return false, err
+	}
+
+	im := inactivity.NewInactivityMonitor(peerCfg.PeerConnID, m.inactivityThreshold)
+	m.inactivityMonitors[peerCfg.PeerConnID] = im
+
+	m.managedPeers[peerCfg.PublicKey] = &peerCfg
+	m.managedPeersByConnID[peerCfg.PeerConnID] = &managedPeer{
+		peerCfg:         &peerCfg,
+		expectedWatcher: watcherActivity,
+	}
+	return false, nil
+}
+
+// AddActivePeers adds a list of peers to the lazy connection manager
+// suppose these peers was in connected or in connecting states
+func (m *Manager) AddActivePeers(ctx context.Context, peerCfg []lazyconn.PeerConfig) error {
+	m.managedPeersMu.Lock()
+	defer m.managedPeersMu.Unlock()
+
+	for _, cfg := range peerCfg {
+		if _, ok := m.managedPeers[cfg.PublicKey]; ok {
+			cfg.Log.Errorf("peer already managed")
+			continue
+		}
+
+		if err := m.addActivePeer(ctx, cfg); err != nil {
+			cfg.Log.Errorf("failed to add peer to lazy connection manager: %v", err)
+			return err
+		}
+	}
+	return nil
+}
+
+func (m *Manager) RemovePeer(peerID string) {
+	m.managedPeersMu.Lock()
+	defer m.managedPeersMu.Unlock()
+
+	m.removePeer(peerID)
+}
+
+// ActivatePeer activates a peer connection when a signal message is received
+func (m *Manager) ActivatePeer(ctx context.Context, peerID string) (found bool) {
+	m.managedPeersMu.Lock()
+	defer m.managedPeersMu.Unlock()
+
+	cfg, ok := m.managedPeers[peerID]
+	if !ok {
+		return false
+	}
+
+	mp, ok := m.managedPeersByConnID[cfg.PeerConnID]
+	if !ok {
+		return false
+	}
+
+	// signal messages coming continuously after success activation, with this avoid the multiple activation
+	if mp.expectedWatcher == watcherInactivity {
+		return false
+	}
+
+	mp.expectedWatcher = watcherInactivity
+
+	m.activityManager.RemovePeer(cfg.Log, cfg.PeerConnID)
+
+	im, ok := m.inactivityMonitors[cfg.PeerConnID]
+	if !ok {
+		cfg.Log.Errorf("inactivity monitor not found for peer")
+		return false
+	}
+
+	mp.peerCfg.Log.Infof("starting inactivity monitor")
+	go im.Start(ctx, m.onInactive)
+
+	return true
+}
+
+func (m *Manager) addActivePeer(ctx context.Context, peerCfg lazyconn.PeerConfig) error {
+	if _, ok := m.managedPeers[peerCfg.PublicKey]; ok {
+		peerCfg.Log.Warnf("peer already managed")
+		return nil
+	}
+
+	im := inactivity.NewInactivityMonitor(peerCfg.PeerConnID, m.inactivityThreshold)
+	m.inactivityMonitors[peerCfg.PeerConnID] = im
+
+	m.managedPeers[peerCfg.PublicKey] = &peerCfg
+	m.managedPeersByConnID[peerCfg.PeerConnID] = &managedPeer{
+		peerCfg:         &peerCfg,
+		expectedWatcher: watcherInactivity,
+	}
+
+	peerCfg.Log.Infof("starting inactivity monitor on peer that has been removed from exclude list")
+	go im.Start(ctx, m.onInactive)
+	return nil
+}
+
+func (m *Manager) removePeer(peerID string) {
+	cfg, ok := m.managedPeers[peerID]
+	if !ok {
+		return
+	}
+
+	cfg.Log.Infof("removing lazy peer")
+
+	if im, ok := m.inactivityMonitors[cfg.PeerConnID]; ok {
+		im.Stop()
+		delete(m.inactivityMonitors, cfg.PeerConnID)
+		cfg.Log.Debugf("inactivity monitor stopped")
+	}
+
+	m.activityManager.RemovePeer(cfg.Log, cfg.PeerConnID)
+	delete(m.managedPeers, peerID)
+	delete(m.managedPeersByConnID, cfg.PeerConnID)
+}
+
+func (m *Manager) close() {
+	m.managedPeersMu.Lock()
+	defer m.managedPeersMu.Unlock()
+
+	m.cancel()
+
+	m.connStateDispatcher.RemoveListener(m.connStateListener)
+	m.activityManager.Close()
+	for _, iw := range m.inactivityMonitors {
+		iw.Stop()
+	}
+	m.inactivityMonitors = make(map[peerid.ConnID]*inactivity.Monitor)
+	m.managedPeers = make(map[string]*lazyconn.PeerConfig)
+	m.managedPeersByConnID = make(map[peerid.ConnID]*managedPeer)
+	log.Infof("lazy connection manager closed")
+}
+
+func (m *Manager) onPeerActivity(ctx context.Context, peerConnID peerid.ConnID) {
+	m.managedPeersMu.Lock()
+	defer m.managedPeersMu.Unlock()
+
+	mp, ok := m.managedPeersByConnID[peerConnID]
+	if !ok {
+		log.Errorf("peer not found by conn id: %v", peerConnID)
+		return
+	}
+
+	if mp.expectedWatcher != watcherActivity {
+		mp.peerCfg.Log.Warnf("ignore activity event")
+		return
+	}
+
+	mp.peerCfg.Log.Infof("detected peer activity")
+
+	mp.expectedWatcher = watcherInactivity
+
+	mp.peerCfg.Log.Infof("starting inactivity monitor")
+	go m.inactivityMonitors[peerConnID].Start(ctx, m.onInactive)
+
+	m.peerStore.PeerConnOpen(ctx, mp.peerCfg.PublicKey)
+}
+
+func (m *Manager) onPeerInactivityTimedOut(peerConnID peerid.ConnID) {
+	m.managedPeersMu.Lock()
+	defer m.managedPeersMu.Unlock()
+
+	mp, ok := m.managedPeersByConnID[peerConnID]
+	if !ok {
+		log.Errorf("peer not found by id: %v", peerConnID)
+		return
+	}
+
+	if mp.expectedWatcher != watcherInactivity {
+		mp.peerCfg.Log.Warnf("ignore inactivity event")
+		return
+	}
+
+	mp.peerCfg.Log.Infof("connection timed out")
+
+	// this is blocking operation, potentially can be optimized
+	m.peerStore.PeerConnClose(mp.peerCfg.PublicKey)
+
+	mp.peerCfg.Log.Infof("start activity monitor")
+
+	mp.expectedWatcher = watcherActivity
+
+	// just in case free up
+	m.inactivityMonitors[peerConnID].PauseTimer()
+
+	if err := m.activityManager.MonitorPeerActivity(*mp.peerCfg); err != nil {
+		mp.peerCfg.Log.Errorf("failed to create activity monitor: %v", err)
+		return
+	}
+}
+
+func (m *Manager) onPeerConnected(peerConnID peerid.ConnID) {
+	m.managedPeersMu.Lock()
+	defer m.managedPeersMu.Unlock()
+
+	mp, ok := m.managedPeersByConnID[peerConnID]
+	if !ok {
+		return
+	}
+
+	if mp.expectedWatcher != watcherInactivity {
+		return
+	}
+
+	iw, ok := m.inactivityMonitors[mp.peerCfg.PeerConnID]
+	if !ok {
+		mp.peerCfg.Log.Errorf("inactivity monitor not found for peer")
+		return
+	}
+
+	mp.peerCfg.Log.Infof("peer connected, pausing inactivity monitor while connection is not disconnected")
+	iw.PauseTimer()
+}
+
+func (m *Manager) onPeerDisconnected(peerConnID peerid.ConnID) {
+	m.managedPeersMu.Lock()
+	defer m.managedPeersMu.Unlock()
+
+	mp, ok := m.managedPeersByConnID[peerConnID]
+	if !ok {
+		return
+	}
+
+	if mp.expectedWatcher != watcherInactivity {
+		return
+	}
+
+	iw, ok := m.inactivityMonitors[mp.peerCfg.PeerConnID]
+	if !ok {
+		return
+	}
+
+	mp.peerCfg.Log.Infof("reset inactivity monitor timer")
+	iw.ResetTimer()
+}
diff --git a/client/internal/lazyconn/peercfg.go b/client/internal/lazyconn/peercfg.go
new file mode 100644
index 00000000..987d06a3
--- /dev/null
+++ b/client/internal/lazyconn/peercfg.go
@@ -0,0 +1,16 @@
+package lazyconn
+
+import (
+	"net/netip"
+
+	log "github.com/sirupsen/logrus"
+
+	"github.com/netbirdio/netbird/client/internal/peer/id"
+)
+
+type PeerConfig struct {
+	PublicKey  string
+	AllowedIPs []netip.Prefix
+	PeerConnID id.ConnID
+	Log        *log.Entry
+}
diff --git a/client/internal/lazyconn/support.go b/client/internal/lazyconn/support.go
new file mode 100644
index 00000000..5e765c2d
--- /dev/null
+++ b/client/internal/lazyconn/support.go
@@ -0,0 +1,41 @@
+package lazyconn
+
+import (
+	"strings"
+
+	"github.com/hashicorp/go-version"
+)
+
+var (
+	minVersion = version.Must(version.NewVersion("0.45.0"))
+)
+
+func IsSupported(agentVersion string) bool {
+	if agentVersion == "development" {
+		return true
+	}
+
+	// filter out versions like this: a6c5960, a7d5c522, d47be154
+	if !strings.Contains(agentVersion, ".") {
+		return false
+	}
+
+	normalizedVersion := normalizeVersion(agentVersion)
+	inputVer, err := version.NewVersion(normalizedVersion)
+	if err != nil {
+		return false
+	}
+
+	return inputVer.GreaterThanOrEqual(minVersion)
+}
+
+func normalizeVersion(version string) string {
+	// Remove prefixes like 'v' or 'a'
+	if len(version) > 0 && (version[0] == 'v' || version[0] == 'a') {
+		version = version[1:]
+	}
+
+	// Remove any suffixes like '-dirty', '-dev', '-SNAPSHOT', etc.
+	parts := strings.Split(version, "-")
+	return parts[0]
+}
diff --git a/client/internal/lazyconn/wgiface.go b/client/internal/lazyconn/wgiface.go
new file mode 100644
index 00000000..090a9319
--- /dev/null
+++ b/client/internal/lazyconn/wgiface.go
@@ -0,0 +1,14 @@
+package lazyconn
+
+import (
+	"net"
+	"net/netip"
+	"time"
+
+	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
+)
+
+type WGIface interface {
+	RemovePeer(peerKey string) error
+	UpdatePeer(peerKey string, allowedIps []netip.Prefix, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error
+}
diff --git a/client/internal/netflow/conntrack/conntrack.go b/client/internal/netflow/conntrack/conntrack.go
index f8440b91..d01adf13 100644
--- a/client/internal/netflow/conntrack/conntrack.go
+++ b/client/internal/netflow/conntrack/conntrack.go
@@ -232,7 +232,7 @@ func (c *ConnTrack) relevantFlow(mark uint32, srcIP, dstIP netip.Addr) bool {
 
 	// fallback if mark rules are not in place
 	wgnet := c.iface.Address().Network
-	return wgnet.Contains(srcIP.AsSlice()) || wgnet.Contains(dstIP.AsSlice())
+	return wgnet.Contains(srcIP) || wgnet.Contains(dstIP)
 }
 
 // mapRxPackets maps packet counts to RX based on flow direction
@@ -293,17 +293,15 @@ func (c *ConnTrack) inferDirection(mark uint32, srcIP, dstIP netip.Addr) nftypes
 	// fallback if marks are not set
 	wgaddr := c.iface.Address().IP
 	wgnetwork := c.iface.Address().Network
-	src, dst := srcIP.AsSlice(), dstIP.AsSlice()
-
 	switch {
-	case wgaddr.Equal(src):
+	case wgaddr == srcIP:
 		return nftypes.Egress
-	case wgaddr.Equal(dst):
+	case wgaddr == dstIP:
 		return nftypes.Ingress
-	case wgnetwork.Contains(src):
+	case wgnetwork.Contains(srcIP):
 		// netbird network -> resource network
 		return nftypes.Ingress
-	case wgnetwork.Contains(dst):
+	case wgnetwork.Contains(dstIP):
 		// resource network -> netbird network
 		return nftypes.Egress
 	}
diff --git a/client/internal/netflow/logger/logger.go b/client/internal/netflow/logger/logger.go
index a3bd091b..e28fdf2f 100644
--- a/client/internal/netflow/logger/logger.go
+++ b/client/internal/netflow/logger/logger.go
@@ -2,7 +2,7 @@ package logger
 
 import (
 	"context"
-	"net"
+	"net/netip"
 	"sync"
 	"sync/atomic"
 	"time"
@@ -23,17 +23,16 @@ type Logger struct {
 	rcvChan            atomic.Pointer[rcvChan]
 	cancel             context.CancelFunc
 	statusRecorder     *peer.Status
-	wgIfaceIPNet       net.IPNet
+	wgIfaceNet         netip.Prefix
 	dnsCollection      atomic.Bool
 	exitNodeCollection atomic.Bool
 	Store              types.Store
 }
 
-func New(statusRecorder *peer.Status, wgIfaceIPNet net.IPNet) *Logger {
-
+func New(statusRecorder *peer.Status, wgIfaceIPNet netip.Prefix) *Logger {
 	return &Logger{
 		statusRecorder: statusRecorder,
-		wgIfaceIPNet:   wgIfaceIPNet,
+		wgIfaceNet:     wgIfaceIPNet,
 		Store:          store.NewMemoryStore(),
 	}
 }
@@ -89,11 +88,11 @@ func (l *Logger) startReceiver() {
 			var isSrcExitNode bool
 			var isDestExitNode bool
 
-			if !l.wgIfaceIPNet.Contains(net.IP(event.SourceIP.AsSlice())) {
+			if !l.wgIfaceNet.Contains(event.SourceIP) {
 				event.SourceResourceID, isSrcExitNode = l.statusRecorder.CheckRoutes(event.SourceIP)
 			}
 
-			if !l.wgIfaceIPNet.Contains(net.IP(event.DestIP.AsSlice())) {
+			if !l.wgIfaceNet.Contains(event.DestIP) {
 				event.DestResourceID, isDestExitNode = l.statusRecorder.CheckRoutes(event.DestIP)
 			}
 
diff --git a/client/internal/netflow/manager.go b/client/internal/netflow/manager.go
index 0f1cdce3..e3b18846 100644
--- a/client/internal/netflow/manager.go
+++ b/client/internal/netflow/manager.go
@@ -4,7 +4,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
-	"net"
+	"net/netip"
 	"runtime"
 	"sync"
 	"time"
@@ -34,11 +34,11 @@ type Manager struct {
 
 // NewManager creates a new netflow manager
 func NewManager(iface nftypes.IFaceMapper, publicKey []byte, statusRecorder *peer.Status) *Manager {
-	var ipNet net.IPNet
+	var prefix netip.Prefix
 	if iface != nil {
-		ipNet = *iface.Address().Network
+		prefix = iface.Address().Network
 	}
-	flowLogger := logger.New(statusRecorder, ipNet)
+	flowLogger := logger.New(statusRecorder, prefix)
 
 	var ct nftypes.ConnTracker
 	if runtime.GOOS == "linux" && iface != nil && !iface.IsUserspaceBind() {
@@ -123,8 +123,14 @@ func (m *Manager) disableFlow() error {
 
 	m.logger.Close()
 
-	if m.receiverClient != nil {
-		return m.receiverClient.Close()
+	if m.receiverClient == nil {
+		return nil
+	}
+
+	err := m.receiverClient.Close()
+	m.receiverClient = nil
+	if err != nil {
+		return fmt.Errorf("close: %w", err)
 	}
 
 	return nil
diff --git a/client/internal/networkmonitor/check_change_bsd.go b/client/internal/networkmonitor/check_change_bsd.go
index bb327a87..f5eb2c73 100644
--- a/client/internal/networkmonitor/check_change_bsd.go
+++ b/client/internal/networkmonitor/check_change_bsd.go
@@ -19,7 +19,7 @@ import (
 func checkChange(ctx context.Context, nexthopv4, nexthopv6 systemops.Nexthop) error {
 	fd, err := unix.Socket(syscall.AF_ROUTE, syscall.SOCK_RAW, syscall.AF_UNSPEC)
 	if err != nil {
-		return fmt.Errorf("failed to open routing socket: %v", err)
+		return fmt.Errorf("open routing socket: %v", err)
 	}
 	defer func() {
 		err := unix.Close(fd)
diff --git a/client/internal/networkmonitor/check_change_windows.go b/client/internal/networkmonitor/check_change_windows.go
index 58286573..81458486 100644
--- a/client/internal/networkmonitor/check_change_windows.go
+++ b/client/internal/networkmonitor/check_change_windows.go
@@ -13,7 +13,7 @@ import (
 func checkChange(ctx context.Context, nexthopv4, nexthopv6 systemops.Nexthop) error {
 	routeMonitor, err := systemops.NewRouteMonitor(ctx)
 	if err != nil {
-		return fmt.Errorf("failed to create route monitor: %w", err)
+		return fmt.Errorf("create route monitor: %w", err)
 	}
 	defer func() {
 		if err := routeMonitor.Stop(); err != nil {
@@ -38,32 +38,46 @@ func checkChange(ctx context.Context, nexthopv4, nexthopv6 systemops.Nexthop) er
 }
 
 func routeChanged(route systemops.RouteUpdate, nexthopv4, nexthopv6 systemops.Nexthop) bool {
-	intf := "<nil>"
-	if route.Interface != nil {
-		intf = route.Interface.Name
-		if isSoftInterface(intf) {
-			log.Debugf("Network monitor: ignoring default route change for soft interface %s", intf)
-			return false
-		}
+	if intf := route.NextHop.Intf; intf != nil && isSoftInterface(intf.Name) {
+		log.Debugf("Network monitor: ignoring default route change for next hop with soft interface %s", route.NextHop)
+		return false
+	}
+
+	// TODO: for the empty nexthop ip (on-link), determine the family differently
+	nexthop := nexthopv4
+	if route.NextHop.IP.Is6() {
+		nexthop = nexthopv6
 	}
 
 	switch route.Type {
-	case systemops.RouteModified:
-		// TODO: get routing table to figure out if our route is affected for modified routes
-		log.Infof("Network monitor: default route changed: via %s, interface %s", route.NextHop, intf)
-		return true
-	case systemops.RouteAdded:
-		if route.NextHop.Is4() && route.NextHop != nexthopv4.IP || route.NextHop.Is6() && route.NextHop != nexthopv6.IP {
-			log.Infof("Network monitor: default route added: via %s, interface %s", route.NextHop, intf)
-			return true
-		}
+	case systemops.RouteModified, systemops.RouteAdded:
+		return handleRouteAddedOrModified(route, nexthop)
 	case systemops.RouteDeleted:
-		if nexthopv4.Intf != nil && route.NextHop == nexthopv4.IP || nexthopv6.Intf != nil && route.NextHop == nexthopv6.IP {
-			log.Infof("Network monitor: default route removed: via %s, interface %s", route.NextHop, intf)
-			return true
+		return handleRouteDeleted(route, nexthop)
+	}
+
+	return false
+}
+
+func handleRouteAddedOrModified(route systemops.RouteUpdate, nexthop systemops.Nexthop) bool {
+	// For added/modified routes, we care about different next hops
+	if !nexthop.Equal(route.NextHop) {
+		action := "changed"
+		if route.Type == systemops.RouteAdded {
+			action = "added"
 		}
+		log.Infof("Network monitor: default route %s: via %s", action, route.NextHop)
+		return true
 	}
+	return false
+}
 
+func handleRouteDeleted(route systemops.RouteUpdate, nexthop systemops.Nexthop) bool {
+	// For deleted routes, we care about our tracked next hop being deleted
+	if nexthop.Equal(route.NextHop) {
+		log.Infof("Network monitor: default route removed: via %s", route.NextHop)
+		return true
+	}
 	return false
 }
 
diff --git a/client/internal/networkmonitor/monitor.go b/client/internal/networkmonitor/monitor.go
index 5896b66b..accdd9c9 100644
--- a/client/internal/networkmonitor/monitor.go
+++ b/client/internal/networkmonitor/monitor.go
@@ -118,9 +118,12 @@ func (nw *NetworkMonitor) Stop() {
 }
 
 func (nw *NetworkMonitor) checkChanges(ctx context.Context, event chan struct{}, nexthop4 systemops.Nexthop, nexthop6 systemops.Nexthop) {
+	defer close(event)
 	for {
 		if err := checkChangeFn(ctx, nexthop4, nexthop6); err != nil {
-			close(event)
+			if !errors.Is(err, context.Canceled) {
+				log.Errorf("Network monitor: failed to check for changes: %v", err)
+			}
 			return
 		}
 		// prevent blocking
diff --git a/client/internal/peer/conn.go b/client/internal/peer/conn.go
index 44e8997b..b3302387 100644
--- a/client/internal/peer/conn.go
+++ b/client/internal/peer/conn.go
@@ -17,8 +17,12 @@ import (
 
 	"github.com/netbirdio/netbird/client/iface/configurer"
 	"github.com/netbirdio/netbird/client/iface/wgproxy"
+	"github.com/netbirdio/netbird/client/internal/peer/conntype"
+	"github.com/netbirdio/netbird/client/internal/peer/dispatcher"
 	"github.com/netbirdio/netbird/client/internal/peer/guard"
 	icemaker "github.com/netbirdio/netbird/client/internal/peer/ice"
+	"github.com/netbirdio/netbird/client/internal/peer/id"
+	"github.com/netbirdio/netbird/client/internal/peer/worker"
 	"github.com/netbirdio/netbird/client/internal/stdnet"
 	relayClient "github.com/netbirdio/netbird/relay/client"
 	"github.com/netbirdio/netbird/route"
@@ -26,32 +30,20 @@ import (
 	semaphoregroup "github.com/netbirdio/netbird/util/semaphore-group"
 )
 
-type ConnPriority int
-
-func (cp ConnPriority) String() string {
-	switch cp {
-	case connPriorityNone:
-		return "None"
-	case connPriorityRelay:
-		return "PriorityRelay"
-	case connPriorityICETurn:
-		return "PriorityICETurn"
-	case connPriorityICEP2P:
-		return "PriorityICEP2P"
-	default:
-		return fmt.Sprintf("ConnPriority(%d)", cp)
-	}
-}
-
 const (
 	defaultWgKeepAlive = 25 * time.Second
-
-	connPriorityNone    ConnPriority = 0
-	connPriorityRelay   ConnPriority = 1
-	connPriorityICETurn ConnPriority = 2
-	connPriorityICEP2P  ConnPriority = 3
 )
 
+type ServiceDependencies struct {
+	StatusRecorder     *Status
+	Signaler           *Signaler
+	IFaceDiscover      stdnet.ExternalIFaceDiscover
+	RelayManager       *relayClient.Manager
+	SrWatcher          *guard.SRWatcher
+	Semaphore          *semaphoregroup.SemaphoreGroup
+	PeerConnDispatcher *dispatcher.ConnectionDispatcher
+}
+
 type WgConfig struct {
 	WgListenPort int
 	RemoteKey    string
@@ -76,6 +68,8 @@ type ConnConfig struct {
 	// LocalKey is a public key of a local peer
 	LocalKey string
 
+	AgentVersion string
+
 	Timeout time.Duration
 
 	WgConfig WgConfig
@@ -89,22 +83,23 @@ type ConnConfig struct {
 }
 
 type Conn struct {
-	log            *log.Entry
+	Log            *log.Entry
 	mu             sync.Mutex
 	ctx            context.Context
 	ctxCancel      context.CancelFunc
 	config         ConnConfig
 	statusRecorder *Status
 	signaler       *Signaler
+	iFaceDiscover  stdnet.ExternalIFaceDiscover
 	relayManager   *relayClient.Manager
-	handshaker     *Handshaker
+	srWatcher      *guard.SRWatcher
 
 	onConnected    func(remoteWireGuardKey string, remoteRosenpassPubKey []byte, wireGuardIP string, remoteRosenpassAddr string)
 	onDisconnected func(remotePeer string)
 
-	statusRelay         *AtomicConnStatus
-	statusICE           *AtomicConnStatus
-	currentConnPriority ConnPriority
+	statusRelay         *worker.AtomicWorkerStatus
+	statusICE           *worker.AtomicWorkerStatus
+	currentConnPriority conntype.ConnPriority
 	opened              bool // this flag is used to prevent close in case of not opened connection
 
 	workerICE   *WorkerICE
@@ -120,9 +115,12 @@ type Conn struct {
 
 	wgProxyICE   wgproxy.Proxy
 	wgProxyRelay wgproxy.Proxy
+	handshaker   *Handshaker
 
-	guard     *guard.Guard
-	semaphore *semaphoregroup.SemaphoreGroup
+	guard              *guard.Guard
+	semaphore          *semaphoregroup.SemaphoreGroup
+	peerConnDispatcher *dispatcher.ConnectionDispatcher
+	wg                 sync.WaitGroup
 
 	// debug purpose
 	dumpState *stateDump
@@ -130,91 +128,101 @@ type Conn struct {
 
 // NewConn creates a new not opened Conn to the remote peer.
 // To establish a connection run Conn.Open
-func NewConn(engineCtx context.Context, config ConnConfig, statusRecorder *Status, signaler *Signaler, iFaceDiscover stdnet.ExternalIFaceDiscover, relayManager *relayClient.Manager, srWatcher *guard.SRWatcher, semaphore *semaphoregroup.SemaphoreGroup) (*Conn, error) {
+func NewConn(config ConnConfig, services ServiceDependencies) (*Conn, error) {
 	if len(config.WgConfig.AllowedIps) == 0 {
 		return nil, fmt.Errorf("allowed IPs is empty")
 	}
 
-	ctx, ctxCancel := context.WithCancel(engineCtx)
 	connLog := log.WithField("peer", config.Key)
 
 	var conn = &Conn{
-		log:            connLog,
-		ctx:            ctx,
-		ctxCancel:      ctxCancel,
-		config:         config,
-		statusRecorder: statusRecorder,
-		signaler:       signaler,
-		relayManager:   relayManager,
-		statusRelay:    NewAtomicConnStatus(),
-		statusICE:      NewAtomicConnStatus(),
-		semaphore:      semaphore,
-		dumpState:      newStateDump(config.Key, connLog, statusRecorder),
-	}
-
-	ctrl := isController(config)
-	conn.workerRelay = NewWorkerRelay(connLog, ctrl, config, conn, relayManager, conn.dumpState)
+		Log:                connLog,
+		config:             config,
+		statusRecorder:     services.StatusRecorder,
+		signaler:           services.Signaler,
+		iFaceDiscover:      services.IFaceDiscover,
+		relayManager:       services.RelayManager,
+		srWatcher:          services.SrWatcher,
+		semaphore:          services.Semaphore,
+		peerConnDispatcher: services.PeerConnDispatcher,
+		statusRelay:        worker.NewAtomicStatus(),
+		statusICE:          worker.NewAtomicStatus(),
+		dumpState:          newStateDump(config.Key, connLog, services.StatusRecorder),
+	}
+
+	return conn, nil
+}
+
+// Open opens connection to the remote peer
+// It will try to establish a connection using ICE and in parallel with relay. The higher priority connection type will
+// be used.
+func (conn *Conn) Open(engineCtx context.Context) error {
+	conn.semaphore.Add(engineCtx)
+
+	conn.mu.Lock()
+	defer conn.mu.Unlock()
+
+	if conn.opened {
+		conn.semaphore.Done(engineCtx)
+		return nil
+	}
+
+	conn.ctx, conn.ctxCancel = context.WithCancel(engineCtx)
+
+	conn.workerRelay = NewWorkerRelay(conn.Log, isController(conn.config), conn.config, conn, conn.relayManager, conn.dumpState)
 
 	relayIsSupportedLocally := conn.workerRelay.RelayIsSupportedLocally()
-	workerICE, err := NewWorkerICE(ctx, connLog, config, conn, signaler, iFaceDiscover, statusRecorder, relayIsSupportedLocally)
+	workerICE, err := NewWorkerICE(conn.ctx, conn.Log, conn.config, conn, conn.signaler, conn.iFaceDiscover, conn.statusRecorder, relayIsSupportedLocally)
 	if err != nil {
-		return nil, err
+		return err
 	}
 	conn.workerICE = workerICE
 
-	conn.handshaker = NewHandshaker(ctx, connLog, config, signaler, conn.workerICE, conn.workerRelay)
+	conn.handshaker = NewHandshaker(conn.Log, conn.config, conn.signaler, conn.workerICE, conn.workerRelay)
 
 	conn.handshaker.AddOnNewOfferListener(conn.workerRelay.OnNewOffer)
 	if os.Getenv("NB_FORCE_RELAY") != "true" {
 		conn.handshaker.AddOnNewOfferListener(conn.workerICE.OnNewOffer)
 	}
 
-	conn.guard = guard.NewGuard(connLog, ctrl, conn.isConnectedOnAllWay, config.Timeout, srWatcher)
+	conn.guard = guard.NewGuard(conn.Log, conn.isConnectedOnAllWay, conn.config.Timeout, conn.srWatcher)
 
-	go conn.handshaker.Listen()
-
-	go conn.dumpState.Start(ctx)
-	return conn, nil
-}
-
-// Open opens connection to the remote peer
-// It will try to establish a connection using ICE and in parallel with relay. The higher priority connection type will
-// be used.
-func (conn *Conn) Open() {
-	conn.semaphore.Add(conn.ctx)
-	conn.log.Debugf("open connection to peer")
-
-	conn.mu.Lock()
-	defer conn.mu.Unlock()
-	conn.opened = true
+	conn.wg.Add(1)
+	go func() {
+		defer conn.wg.Done()
+		conn.handshaker.Listen(conn.ctx)
+	}()
+	go conn.dumpState.Start(conn.ctx)
 
 	peerState := State{
 		PubKey:           conn.config.Key,
-		IP:               conn.config.WgConfig.AllowedIps[0].Addr().String(),
 		ConnStatusUpdate: time.Now(),
-		ConnStatus:       StatusDisconnected,
+		ConnStatus:       StatusConnecting,
 		Mux:              new(sync.RWMutex),
 	}
-	err := conn.statusRecorder.UpdatePeerState(peerState)
-	if err != nil {
-		conn.log.Warnf("error while updating the state err: %v", err)
+	if err := conn.statusRecorder.UpdatePeerState(peerState); err != nil {
+		conn.Log.Warnf("error while updating the state err: %v", err)
 	}
 
-	go conn.startHandshakeAndReconnect(conn.ctx)
-}
-
-func (conn *Conn) startHandshakeAndReconnect(ctx context.Context) {
-	defer conn.semaphore.Done(conn.ctx)
-	conn.waitInitialRandomSleepTime(ctx)
+	conn.wg.Add(1)
+	go func() {
+		defer conn.wg.Done()
+		conn.waitInitialRandomSleepTime(conn.ctx)
+		conn.semaphore.Done(conn.ctx)
 
-	conn.dumpState.SendOffer()
-	err := conn.handshaker.sendOffer()
-	if err != nil {
-		conn.log.Errorf("failed to send initial offer: %v", err)
-	}
+		conn.dumpState.SendOffer()
+		if err := conn.handshaker.sendOffer(); err != nil {
+			conn.Log.Errorf("failed to send initial offer: %v", err)
+		}
 
-	go conn.guard.Start(ctx)
-	go conn.listenGuardEvent(ctx)
+		conn.wg.Add(1)
+		go func() {
+			conn.guard.Start(conn.ctx, conn.onGuardEvent)
+			conn.wg.Done()
+		}()
+	}()
+	conn.opened = true
+	return nil
 }
 
 // Close closes this peer Conn issuing a close event to the Conn closeCh
@@ -223,14 +231,14 @@ func (conn *Conn) Close() {
 	defer conn.wgWatcherWg.Wait()
 	defer conn.mu.Unlock()
 
-	conn.log.Infof("close peer connection")
-	conn.ctxCancel()
-
 	if !conn.opened {
-		conn.log.Debugf("ignore close connection to peer")
+		conn.Log.Debugf("ignore close connection to peer")
 		return
 	}
 
+	conn.Log.Infof("close peer connection")
+	conn.ctxCancel()
+
 	conn.workerRelay.DisableWgWatcher()
 	conn.workerRelay.CloseConn()
 	conn.workerICE.Close()
@@ -238,7 +246,7 @@ func (conn *Conn) Close() {
 	if conn.wgProxyRelay != nil {
 		err := conn.wgProxyRelay.CloseConn()
 		if err != nil {
-			conn.log.Errorf("failed to close wg proxy for relay: %v", err)
+			conn.Log.Errorf("failed to close wg proxy for relay: %v", err)
 		}
 		conn.wgProxyRelay = nil
 	}
@@ -246,13 +254,13 @@ func (conn *Conn) Close() {
 	if conn.wgProxyICE != nil {
 		err := conn.wgProxyICE.CloseConn()
 		if err != nil {
-			conn.log.Errorf("failed to close wg proxy for ice: %v", err)
+			conn.Log.Errorf("failed to close wg proxy for ice: %v", err)
 		}
 		conn.wgProxyICE = nil
 	}
 
 	if err := conn.removeWgPeer(); err != nil {
-		conn.log.Errorf("failed to remove wg endpoint: %v", err)
+		conn.Log.Errorf("failed to remove wg endpoint: %v", err)
 	}
 
 	conn.freeUpConnID()
@@ -262,14 +270,16 @@ func (conn *Conn) Close() {
 	}
 
 	conn.setStatusToDisconnected()
-	conn.log.Infof("peer connection has been closed")
+	conn.opened = false
+	conn.wg.Wait()
+	conn.Log.Infof("peer connection closed")
 }
 
 // OnRemoteAnswer handles an offer from the remote peer and returns true if the message was accepted, false otherwise
 // doesn't block, discards the message if connection wasn't ready
 func (conn *Conn) OnRemoteAnswer(answer OfferAnswer) bool {
 	conn.dumpState.RemoteAnswer()
-	conn.log.Infof("OnRemoteAnswer, priority: %s, status ICE: %s, status relay: %s", conn.currentConnPriority, conn.statusICE, conn.statusRelay)
+	conn.Log.Infof("OnRemoteAnswer, priority: %s, status ICE: %s, status relay: %s", conn.currentConnPriority, conn.statusICE, conn.statusRelay)
 	return conn.handshaker.OnRemoteAnswer(answer)
 }
 
@@ -298,7 +308,7 @@ func (conn *Conn) SetOnDisconnected(handler func(remotePeer string)) {
 
 func (conn *Conn) OnRemoteOffer(offer OfferAnswer) bool {
 	conn.dumpState.RemoteOffer()
-	conn.log.Infof("OnRemoteOffer, on status ICE: %s, status Relay: %s", conn.statusICE, conn.statusRelay)
+	conn.Log.Infof("OnRemoteOffer, on status ICE: %s, status Relay: %s", conn.statusICE, conn.statusRelay)
 	return conn.handshaker.OnRemoteOffer(offer)
 }
 
@@ -307,19 +317,24 @@ func (conn *Conn) WgConfig() WgConfig {
 	return conn.config.WgConfig
 }
 
-// Status returns current status of the Conn
-func (conn *Conn) Status() ConnStatus {
+// IsConnected unit tests only
+// refactor unit test to use status recorder use refactor status recorded to manage connection status in peer.Conn
+func (conn *Conn) IsConnected() bool {
 	conn.mu.Lock()
 	defer conn.mu.Unlock()
-	return conn.evalStatus()
+	return conn.currentConnPriority != conntype.None
 }
 
 func (conn *Conn) GetKey() string {
 	return conn.config.Key
 }
 
+func (conn *Conn) ConnID() id.ConnID {
+	return id.ConnID(conn)
+}
+
 // configureConnection starts proxying traffic from/to local Wireguard and sets connection status to StatusConnected
-func (conn *Conn) onICEConnectionIsReady(priority ConnPriority, iceConnInfo ICEConnInfo) {
+func (conn *Conn) onICEConnectionIsReady(priority conntype.ConnPriority, iceConnInfo ICEConnInfo) {
 	conn.mu.Lock()
 	defer conn.mu.Unlock()
 
@@ -327,21 +342,21 @@ func (conn *Conn) onICEConnectionIsReady(priority ConnPriority, iceConnInfo ICEC
 		return
 	}
 
-	if remoteConnNil(conn.log, iceConnInfo.RemoteConn) {
-		conn.log.Errorf("remote ICE connection is nil")
+	if remoteConnNil(conn.Log, iceConnInfo.RemoteConn) {
+		conn.Log.Errorf("remote ICE connection is nil")
 		return
 	}
 
 	// this never should happen, because Relay is the lower priority and ICE always close the deprecated connection before upgrade
 	// todo consider to remove this check
 	if conn.currentConnPriority > priority {
-		conn.log.Infof("current connection priority (%s) is higher than the new one (%s), do not upgrade connection", conn.currentConnPriority, priority)
-		conn.statusICE.Set(StatusConnected)
+		conn.Log.Infof("current connection priority (%s) is higher than the new one (%s), do not upgrade connection", conn.currentConnPriority, priority)
+		conn.statusICE.SetConnected()
 		conn.updateIceState(iceConnInfo)
 		return
 	}
 
-	conn.log.Infof("set ICE to active connection")
+	conn.Log.Infof("set ICE to active connection")
 	conn.dumpState.P2PConnected()
 
 	var (
@@ -353,7 +368,7 @@ func (conn *Conn) onICEConnectionIsReady(priority ConnPriority, iceConnInfo ICEC
 		conn.dumpState.NewLocalProxy()
 		wgProxy, err = conn.newProxy(iceConnInfo.RemoteConn)
 		if err != nil {
-			conn.log.Errorf("failed to add turn net.Conn to local proxy: %v", err)
+			conn.Log.Errorf("failed to add turn net.Conn to local proxy: %v", err)
 			return
 		}
 		ep = wgProxy.EndpointAddr()
@@ -369,7 +384,7 @@ func (conn *Conn) onICEConnectionIsReady(priority ConnPriority, iceConnInfo ICEC
 	}
 
 	if err := conn.runBeforeAddPeerHooks(ep.IP); err != nil {
-		conn.log.Errorf("Before add peer hook failed: %v", err)
+		conn.Log.Errorf("Before add peer hook failed: %v", err)
 	}
 
 	conn.workerRelay.DisableWgWatcher()
@@ -388,10 +403,16 @@ func (conn *Conn) onICEConnectionIsReady(priority ConnPriority, iceConnInfo ICEC
 		return
 	}
 	wgConfigWorkaround()
+
+	oldState := conn.currentConnPriority
 	conn.currentConnPriority = priority
-	conn.statusICE.Set(StatusConnected)
+	conn.statusICE.SetConnected()
 	conn.updateIceState(iceConnInfo)
 	conn.doOnConnected(iceConnInfo.RosenpassPubKey, iceConnInfo.RosenpassAddr)
+
+	if oldState == conntype.None {
+		conn.peerConnDispatcher.NotifyConnected(conn.ConnID())
+	}
 }
 
 func (conn *Conn) onICEStateDisconnected() {
@@ -402,22 +423,22 @@ func (conn *Conn) onICEStateDisconnected() {
 		return
 	}
 
-	conn.log.Tracef("ICE connection state changed to disconnected")
+	conn.Log.Tracef("ICE connection state changed to disconnected")
 
 	if conn.wgProxyICE != nil {
 		if err := conn.wgProxyICE.CloseConn(); err != nil {
-			conn.log.Warnf("failed to close deprecated wg proxy conn: %v", err)
+			conn.Log.Warnf("failed to close deprecated wg proxy conn: %v", err)
 		}
 	}
 
 	// switch back to relay connection
 	if conn.isReadyToUpgrade() {
-		conn.log.Infof("ICE disconnected, set Relay to active connection")
+		conn.Log.Infof("ICE disconnected, set Relay to active connection")
 		conn.dumpState.SwitchToRelay()
 		conn.wgProxyRelay.Work()
 
 		if err := conn.configureWGEndpoint(conn.wgProxyRelay.EndpointAddr(), conn.rosenpassRemoteKey); err != nil {
-			conn.log.Errorf("failed to switch to relay conn: %v", err)
+			conn.Log.Errorf("failed to switch to relay conn: %v", err)
 		}
 
 		conn.wgWatcherWg.Add(1)
@@ -425,17 +446,18 @@ func (conn *Conn) onICEStateDisconnected() {
 			defer conn.wgWatcherWg.Done()
 			conn.workerRelay.EnableWgWatcher(conn.ctx)
 		}()
-		conn.currentConnPriority = connPriorityRelay
+		conn.currentConnPriority = conntype.Relay
 	} else {
-		conn.log.Infof("ICE disconnected, do not switch to Relay. Reset priority to: %s", connPriorityNone.String())
-		conn.currentConnPriority = connPriorityNone
+		conn.Log.Infof("ICE disconnected, do not switch to Relay. Reset priority to: %s", conntype.None.String())
+		conn.currentConnPriority = conntype.None
+		conn.peerConnDispatcher.NotifyDisconnected(conn.ConnID())
 	}
 
-	changed := conn.statusICE.Get() != StatusDisconnected
+	changed := conn.statusICE.Get() != worker.StatusDisconnected
 	if changed {
 		conn.guard.SetICEConnDisconnected()
 	}
-	conn.statusICE.Set(StatusDisconnected)
+	conn.statusICE.SetDisconnected()
 
 	peerState := State{
 		PubKey:           conn.config.Key,
@@ -446,7 +468,7 @@ func (conn *Conn) onICEStateDisconnected() {
 
 	err := conn.statusRecorder.UpdatePeerICEStateToDisconnected(peerState)
 	if err != nil {
-		conn.log.Warnf("unable to set peer's state to disconnected ice, got error: %v", err)
+		conn.Log.Warnf("unable to set peer's state to disconnected ice, got error: %v", err)
 	}
 }
 
@@ -456,41 +478,41 @@ func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) {
 
 	if conn.ctx.Err() != nil {
 		if err := rci.relayedConn.Close(); err != nil {
-			conn.log.Warnf("failed to close unnecessary relayed connection: %v", err)
+			conn.Log.Warnf("failed to close unnecessary relayed connection: %v", err)
 		}
 		return
 	}
 
 	conn.dumpState.RelayConnected()
-	conn.log.Debugf("Relay connection has been established, setup the WireGuard")
+	conn.Log.Debugf("Relay connection has been established, setup the WireGuard")
 
 	wgProxy, err := conn.newProxy(rci.relayedConn)
 	if err != nil {
-		conn.log.Errorf("failed to add relayed net.Conn to local proxy: %v", err)
+		conn.Log.Errorf("failed to add relayed net.Conn to local proxy: %v", err)
 		return
 	}
 	conn.dumpState.NewLocalProxy()
 
-	conn.log.Infof("created new wgProxy for relay connection: %s", wgProxy.EndpointAddr().String())
+	conn.Log.Infof("created new wgProxy for relay connection: %s", wgProxy.EndpointAddr().String())
 
 	if conn.isICEActive() {
-		conn.log.Infof("do not switch to relay because current priority is: %s", conn.currentConnPriority.String())
+		conn.Log.Debugf("do not switch to relay because current priority is: %s", conn.currentConnPriority.String())
 		conn.setRelayedProxy(wgProxy)
-		conn.statusRelay.Set(StatusConnected)
+		conn.statusRelay.SetConnected()
 		conn.updateRelayStatus(rci.relayedConn.RemoteAddr().String(), rci.rosenpassPubKey)
 		return
 	}
 
 	if err := conn.runBeforeAddPeerHooks(wgProxy.EndpointAddr().IP); err != nil {
-		conn.log.Errorf("Before add peer hook failed: %v", err)
+		conn.Log.Errorf("Before add peer hook failed: %v", err)
 	}
 
 	wgProxy.Work()
 	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)
+			conn.Log.Warnf("Failed to close relay connection: %v", err)
 		}
-		conn.log.Errorf("Failed to update WireGuard peer configuration: %v", err)
+		conn.Log.Errorf("Failed to update WireGuard peer configuration: %v", err)
 		return
 	}
 
@@ -502,12 +524,13 @@ func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) {
 
 	wgConfigWorkaround()
 	conn.rosenpassRemoteKey = rci.rosenpassPubKey
-	conn.currentConnPriority = connPriorityRelay
-	conn.statusRelay.Set(StatusConnected)
+	conn.currentConnPriority = conntype.Relay
+	conn.statusRelay.SetConnected()
 	conn.setRelayedProxy(wgProxy)
 	conn.updateRelayStatus(rci.relayedConn.RemoteAddr().String(), rci.rosenpassPubKey)
-	conn.log.Infof("start to communicate with peer via relay")
+	conn.Log.Infof("start to communicate with peer via relay")
 	conn.doOnConnected(rci.rosenpassPubKey, rci.rosenpassAddr)
+	conn.peerConnDispatcher.NotifyConnected(conn.ConnID())
 }
 
 func (conn *Conn) onRelayDisconnected() {
@@ -518,14 +541,15 @@ func (conn *Conn) onRelayDisconnected() {
 		return
 	}
 
-	conn.log.Infof("relay connection is disconnected")
+	conn.Log.Debugf("relay connection is disconnected")
 
-	if conn.currentConnPriority == connPriorityRelay {
-		conn.log.Infof("clean up WireGuard config")
+	if conn.currentConnPriority == conntype.Relay {
+		conn.Log.Debugf("clean up WireGuard config")
 		if err := conn.removeWgPeer(); err != nil {
-			conn.log.Errorf("failed to remove wg endpoint: %v", err)
+			conn.Log.Errorf("failed to remove wg endpoint: %v", err)
 		}
-		conn.currentConnPriority = connPriorityNone
+		conn.currentConnPriority = conntype.None
+		conn.peerConnDispatcher.NotifyDisconnected(conn.ConnID())
 	}
 
 	if conn.wgProxyRelay != nil {
@@ -533,11 +557,11 @@ func (conn *Conn) onRelayDisconnected() {
 		conn.wgProxyRelay = nil
 	}
 
-	changed := conn.statusRelay.Get() != StatusDisconnected
+	changed := conn.statusRelay.Get() != worker.StatusDisconnected
 	if changed {
 		conn.guard.SetRelayedConnDisconnected()
 	}
-	conn.statusRelay.Set(StatusDisconnected)
+	conn.statusRelay.SetDisconnected()
 
 	peerState := State{
 		PubKey:           conn.config.Key,
@@ -546,22 +570,15 @@ func (conn *Conn) onRelayDisconnected() {
 		ConnStatusUpdate: time.Now(),
 	}
 	if err := conn.statusRecorder.UpdatePeerRelayedStateToDisconnected(peerState); err != nil {
-		conn.log.Warnf("unable to save peer's state to Relay disconnected, got error: %v", err)
+		conn.Log.Warnf("unable to save peer's state to Relay disconnected, got error: %v", err)
 	}
 }
 
-func (conn *Conn) listenGuardEvent(ctx context.Context) {
-	for {
-		select {
-		case <-conn.guard.Reconnect:
-			conn.log.Infof("send offer to peer")
-			conn.dumpState.SendOffer()
-			if err := conn.handshaker.SendOffer(); err != nil {
-				conn.log.Errorf("failed to send offer: %v", err)
-			}
-		case <-ctx.Done():
-			return
-		}
+func (conn *Conn) onGuardEvent() {
+	conn.Log.Debugf("send offer to peer")
+	conn.dumpState.SendOffer()
+	if err := conn.handshaker.SendOffer(); err != nil {
+		conn.Log.Errorf("failed to send offer: %v", err)
 	}
 }
 
@@ -588,7 +605,7 @@ func (conn *Conn) updateRelayStatus(relayServerAddr string, rosenpassPubKey []by
 
 	err := conn.statusRecorder.UpdatePeerRelayedState(peerState)
 	if err != nil {
-		conn.log.Warnf("unable to save peer's Relay state, got error: %v", err)
+		conn.Log.Warnf("unable to save peer's Relay state, got error: %v", err)
 	}
 }
 
@@ -607,17 +624,18 @@ func (conn *Conn) updateIceState(iceConnInfo ICEConnInfo) {
 
 	err := conn.statusRecorder.UpdatePeerICEState(peerState)
 	if err != nil {
-		conn.log.Warnf("unable to save peer's ICE state, got error: %v", err)
+		conn.Log.Warnf("unable to save peer's ICE state, got error: %v", err)
 	}
 }
 
 func (conn *Conn) setStatusToDisconnected() {
-	conn.statusRelay.Set(StatusDisconnected)
-	conn.statusICE.Set(StatusDisconnected)
+	conn.statusRelay.SetDisconnected()
+	conn.statusICE.SetDisconnected()
+	conn.currentConnPriority = conntype.None
 
 	peerState := State{
 		PubKey:           conn.config.Key,
-		ConnStatus:       StatusDisconnected,
+		ConnStatus:       StatusIdle,
 		ConnStatusUpdate: time.Now(),
 		Mux:              new(sync.RWMutex),
 	}
@@ -625,10 +643,10 @@ func (conn *Conn) setStatusToDisconnected() {
 	if err != nil {
 		// pretty common error because by that time Engine can already remove the peer and status won't be available.
 		// todo rethink status updates
-		conn.log.Debugf("error while updating peer's state, err: %v", err)
+		conn.Log.Debugf("error while updating peer's state, err: %v", err)
 	}
 	if err := conn.statusRecorder.UpdateWireGuardPeerState(conn.config.Key, configurer.WGStats{}); err != nil {
-		conn.log.Debugf("failed to reset wireguard stats for peer: %s", err)
+		conn.Log.Debugf("failed to reset wireguard stats for peer: %s", err)
 	}
 }
 
@@ -656,32 +674,24 @@ func (conn *Conn) waitInitialRandomSleepTime(ctx context.Context) {
 }
 
 func (conn *Conn) isRelayed() bool {
-	if conn.statusRelay.Get() == StatusDisconnected && (conn.statusICE.Get() == StatusDisconnected || conn.statusICE.Get() == StatusConnecting) {
-		return false
-	}
-
-	if conn.currentConnPriority == connPriorityICEP2P {
+	switch conn.currentConnPriority {
+	case conntype.Relay, conntype.ICETurn:
+		return true
+	default:
 		return false
 	}
-
-	return true
 }
 
 func (conn *Conn) evalStatus() ConnStatus {
-	if conn.statusRelay.Get() == StatusConnected || conn.statusICE.Get() == StatusConnected {
+	if conn.statusRelay.Get() == worker.StatusConnected || conn.statusICE.Get() == worker.StatusConnected {
 		return StatusConnected
 	}
 
-	if conn.statusRelay.Get() == StatusConnecting || conn.statusICE.Get() == StatusConnecting {
-		return StatusConnecting
-	}
-
-	return StatusDisconnected
+	return StatusConnecting
 }
 
 func (conn *Conn) isConnectedOnAllWay() (connected bool) {
-	conn.mu.Lock()
-	defer conn.mu.Unlock()
+	// would be better to protect this with a mutex, but it could cause deadlock with Close function
 
 	defer func() {
 		if !connected {
@@ -689,12 +699,12 @@ func (conn *Conn) isConnectedOnAllWay() (connected bool) {
 		}
 	}()
 
-	if conn.statusICE.Get() == StatusDisconnected {
+	if conn.statusICE.Get() == worker.StatusDisconnected {
 		return false
 	}
 
 	if conn.workerRelay.IsRelayConnectionSupportedWithPeer() {
-		if conn.statusRelay.Get() != StatusConnected {
+		if conn.statusRelay.Get() == worker.StatusDisconnected {
 			return false
 		}
 	}
@@ -716,7 +726,7 @@ func (conn *Conn) freeUpConnID() {
 	if conn.connIDRelay != "" {
 		for _, hook := range conn.afterRemovePeerHooks {
 			if err := hook(conn.connIDRelay); err != nil {
-				conn.log.Errorf("After remove peer hook failed: %v", err)
+				conn.Log.Errorf("After remove peer hook failed: %v", err)
 			}
 		}
 		conn.connIDRelay = ""
@@ -725,7 +735,7 @@ func (conn *Conn) freeUpConnID() {
 	if conn.connIDICE != "" {
 		for _, hook := range conn.afterRemovePeerHooks {
 			if err := hook(conn.connIDICE); err != nil {
-				conn.log.Errorf("After remove peer hook failed: %v", err)
+				conn.Log.Errorf("After remove peer hook failed: %v", err)
 			}
 		}
 		conn.connIDICE = ""
@@ -733,7 +743,7 @@ func (conn *Conn) freeUpConnID() {
 }
 
 func (conn *Conn) newProxy(remoteConn net.Conn) (wgproxy.Proxy, error) {
-	conn.log.Debugf("setup proxied WireGuard connection")
+	conn.Log.Debugf("setup proxied WireGuard connection")
 	udpAddr := &net.UDPAddr{
 		IP:   conn.config.WgConfig.AllowedIps[0].Addr().AsSlice(),
 		Port: conn.config.WgConfig.WgListenPort,
@@ -741,18 +751,18 @@ func (conn *Conn) newProxy(remoteConn net.Conn) (wgproxy.Proxy, error) {
 
 	wgProxy := conn.config.WgConfig.WgInterface.GetProxy()
 	if err := wgProxy.AddTurnConn(conn.ctx, udpAddr, remoteConn); err != nil {
-		conn.log.Errorf("failed to add turn net.Conn to local proxy: %v", err)
+		conn.Log.Errorf("failed to add turn net.Conn to local proxy: %v", err)
 		return nil, err
 	}
 	return wgProxy, nil
 }
 
 func (conn *Conn) isReadyToUpgrade() bool {
-	return conn.wgProxyRelay != nil && conn.currentConnPriority != connPriorityRelay
+	return conn.wgProxyRelay != nil && conn.currentConnPriority != conntype.Relay
 }
 
 func (conn *Conn) isICEActive() bool {
-	return (conn.currentConnPriority == connPriorityICEP2P || conn.currentConnPriority == connPriorityICETurn) && conn.statusICE.Get() == StatusConnected
+	return (conn.currentConnPriority == conntype.ICEP2P || conn.currentConnPriority == conntype.ICETurn) && conn.statusICE.Get() == worker.StatusConnected
 }
 
 func (conn *Conn) removeWgPeer() error {
@@ -760,10 +770,10 @@ func (conn *Conn) removeWgPeer() error {
 }
 
 func (conn *Conn) handleConfigurationFailure(err error, wgProxy wgproxy.Proxy) {
-	conn.log.Warnf("Failed to update wg peer configuration: %v", err)
+	conn.Log.Warnf("Failed to update wg peer configuration: %v", err)
 	if wgProxy != nil {
 		if ierr := wgProxy.CloseConn(); ierr != nil {
-			conn.log.Warnf("Failed to close wg proxy: %v", ierr)
+			conn.Log.Warnf("Failed to close wg proxy: %v", ierr)
 		}
 	}
 	if conn.wgProxyRelay != nil {
@@ -773,16 +783,16 @@ func (conn *Conn) handleConfigurationFailure(err error, wgProxy wgproxy.Proxy) {
 
 func (conn *Conn) logTraceConnState() {
 	if conn.workerRelay.IsRelayConnectionSupportedWithPeer() {
-		conn.log.Tracef("connectivity guard check, relay state: %s, ice state: %s", conn.statusRelay, conn.statusICE)
+		conn.Log.Tracef("connectivity guard check, relay state: %s, ice state: %s", conn.statusRelay, conn.statusICE)
 	} else {
-		conn.log.Tracef("connectivity guard check, ice state: %s", conn.statusICE)
+		conn.Log.Tracef("connectivity guard check, ice state: %s", conn.statusICE)
 	}
 }
 
 func (conn *Conn) setRelayedProxy(proxy wgproxy.Proxy) {
 	if conn.wgProxyRelay != nil {
 		if err := conn.wgProxyRelay.CloseConn(); err != nil {
-			conn.log.Warnf("failed to close deprecated wg proxy conn: %v", err)
+			conn.Log.Warnf("failed to close deprecated wg proxy conn: %v", err)
 		}
 	}
 	conn.wgProxyRelay = proxy
@@ -793,6 +803,10 @@ func (conn *Conn) AllowedIP() netip.Addr {
 	return conn.config.WgConfig.AllowedIps[0].Addr()
 }
 
+func (conn *Conn) AgentVersionString() string {
+	return conn.config.AgentVersion
+}
+
 func (conn *Conn) presharedKey(remoteRosenpassKey []byte) *wgtypes.Key {
 	if conn.config.RosenpassConfig.PubKey == nil {
 		return conn.config.WgConfig.PreSharedKey
@@ -804,7 +818,7 @@ func (conn *Conn) presharedKey(remoteRosenpassKey []byte) *wgtypes.Key {
 
 	determKey, err := conn.rosenpassDetermKey()
 	if err != nil {
-		conn.log.Errorf("failed to generate Rosenpass initial key: %v", err)
+		conn.Log.Errorf("failed to generate Rosenpass initial key: %v", err)
 		return conn.config.WgConfig.PreSharedKey
 	}
 
diff --git a/client/internal/peer/conn_status.go b/client/internal/peer/conn_status.go
index 3c747864..73acc5ef 100644
--- a/client/internal/peer/conn_status.go
+++ b/client/internal/peer/conn_status.go
@@ -1,58 +1,29 @@
 package peer
 
 import (
-	"sync/atomic"
-
 	log "github.com/sirupsen/logrus"
 )
 
 const (
-	// StatusConnected indicate the peer is in connected state
-	StatusConnected ConnStatus = iota
+	// StatusIdle indicate the peer is in disconnected state
+	StatusIdle ConnStatus = iota
 	// StatusConnecting indicate the peer is in connecting state
 	StatusConnecting
-	// StatusDisconnected indicate the peer is in disconnected state
-	StatusDisconnected
+	// StatusConnected indicate the peer is in connected state
+	StatusConnected
 )
 
 // ConnStatus describe the status of a peer's connection
 type ConnStatus int32
 
-// AtomicConnStatus is a thread-safe wrapper for ConnStatus
-type AtomicConnStatus struct {
-	status atomic.Int32
-}
-
-// NewAtomicConnStatus creates a new AtomicConnStatus with the given initial status
-func NewAtomicConnStatus() *AtomicConnStatus {
-	acs := &AtomicConnStatus{}
-	acs.Set(StatusDisconnected)
-	return acs
-}
-
-// Get returns the current connection status
-func (acs *AtomicConnStatus) Get() ConnStatus {
-	return ConnStatus(acs.status.Load())
-}
-
-// Set updates the connection status
-func (acs *AtomicConnStatus) Set(status ConnStatus) {
-	acs.status.Store(int32(status))
-}
-
-// String returns the string representation of the current status
-func (acs *AtomicConnStatus) String() string {
-	return acs.Get().String()
-}
-
 func (s ConnStatus) String() string {
 	switch s {
 	case StatusConnecting:
 		return "Connecting"
 	case StatusConnected:
 		return "Connected"
-	case StatusDisconnected:
-		return "Disconnected"
+	case StatusIdle:
+		return "Idle"
 	default:
 		log.Errorf("unknown status: %d", s)
 		return "INVALID_PEER_CONNECTION_STATUS"
diff --git a/client/internal/peer/conntype/priority.go b/client/internal/peer/conntype/priority.go
new file mode 100644
index 00000000..6746ca7d
--- /dev/null
+++ b/client/internal/peer/conntype/priority.go
@@ -0,0 +1,29 @@
+package conntype
+
+import (
+	"fmt"
+)
+
+const (
+	None    ConnPriority = 0
+	Relay   ConnPriority = 1
+	ICETurn ConnPriority = 2
+	ICEP2P  ConnPriority = 3
+)
+
+type ConnPriority int
+
+func (cp ConnPriority) String() string {
+	switch cp {
+	case None:
+		return "None"
+	case Relay:
+		return "PriorityRelay"
+	case ICETurn:
+		return "PriorityICETurn"
+	case ICEP2P:
+		return "PriorityICEP2P"
+	default:
+		return fmt.Sprintf("ConnPriority(%d)", cp)
+	}
+}
diff --git a/client/internal/peer/dispatcher/dispatcher.go b/client/internal/peer/dispatcher/dispatcher.go
new file mode 100644
index 00000000..06124bc3
--- /dev/null
+++ b/client/internal/peer/dispatcher/dispatcher.go
@@ -0,0 +1,52 @@
+package dispatcher
+
+import (
+	"sync"
+
+	"github.com/netbirdio/netbird/client/internal/peer/id"
+)
+
+type ConnectionListener struct {
+	OnConnected    func(peerID id.ConnID)
+	OnDisconnected func(peerID id.ConnID)
+}
+
+type ConnectionDispatcher struct {
+	listeners map[*ConnectionListener]struct{}
+	mu        sync.Mutex
+}
+
+func NewConnectionDispatcher() *ConnectionDispatcher {
+	return &ConnectionDispatcher{
+		listeners: make(map[*ConnectionListener]struct{}),
+	}
+}
+
+func (e *ConnectionDispatcher) AddListener(listener *ConnectionListener) {
+	e.mu.Lock()
+	defer e.mu.Unlock()
+	e.listeners[listener] = struct{}{}
+}
+
+func (e *ConnectionDispatcher) RemoveListener(listener *ConnectionListener) {
+	e.mu.Lock()
+	defer e.mu.Unlock()
+
+	delete(e.listeners, listener)
+}
+
+func (e *ConnectionDispatcher) NotifyConnected(peerConnID id.ConnID) {
+	e.mu.Lock()
+	defer e.mu.Unlock()
+	for listener := range e.listeners {
+		listener.OnConnected(peerConnID)
+	}
+}
+
+func (e *ConnectionDispatcher) NotifyDisconnected(peerConnID id.ConnID) {
+	e.mu.Lock()
+	defer e.mu.Unlock()
+	for listener := range e.listeners {
+		listener.OnDisconnected(peerConnID)
+	}
+}
diff --git a/client/internal/peer/guard/guard.go b/client/internal/peer/guard/guard.go
index 1fc2b4a4..15510432 100644
--- a/client/internal/peer/guard/guard.go
+++ b/client/internal/peer/guard/guard.go
@@ -8,10 +8,6 @@ import (
 	log "github.com/sirupsen/logrus"
 )
 
-const (
-	reconnectMaxElapsedTime = 30 * time.Minute
-)
-
 type isConnectedFunc func() bool
 
 // Guard is responsible for the reconnection logic.
@@ -25,7 +21,6 @@ type isConnectedFunc func() bool
 type Guard struct {
 	Reconnect               chan struct{}
 	log                     *log.Entry
-	isController            bool
 	isConnectedOnAllWay     isConnectedFunc
 	timeout                 time.Duration
 	srWatcher               *SRWatcher
@@ -33,11 +28,10 @@ type Guard struct {
 	iCEConnDisconnected     chan struct{}
 }
 
-func NewGuard(log *log.Entry, isController bool, isConnectedFn isConnectedFunc, timeout time.Duration, srWatcher *SRWatcher) *Guard {
+func NewGuard(log *log.Entry, isConnectedFn isConnectedFunc, timeout time.Duration, srWatcher *SRWatcher) *Guard {
 	return &Guard{
 		Reconnect:               make(chan struct{}, 1),
 		log:                     log,
-		isController:            isController,
 		isConnectedOnAllWay:     isConnectedFn,
 		timeout:                 timeout,
 		srWatcher:               srWatcher,
@@ -46,12 +40,8 @@ func NewGuard(log *log.Entry, isController bool, isConnectedFn isConnectedFunc,
 	}
 }
 
-func (g *Guard) Start(ctx context.Context) {
-	if g.isController {
-		g.reconnectLoopWithRetry(ctx)
-	} else {
-		g.listenForDisconnectEvents(ctx)
-	}
+func (g *Guard) Start(ctx context.Context, eventCallback func()) {
+	g.reconnectLoopWithRetry(ctx, eventCallback)
 }
 
 func (g *Guard) SetRelayedConnDisconnected() {
@@ -68,9 +58,9 @@ func (g *Guard) SetICEConnDisconnected() {
 	}
 }
 
-// reconnectLoopWithRetry periodically check (max 30 min) the connection status.
+// reconnectLoopWithRetry periodically check the connection status.
 // Try to send offer while the P2P is not established or while the Relay is not connected if is it supported
-func (g *Guard) reconnectLoopWithRetry(ctx context.Context) {
+func (g *Guard) reconnectLoopWithRetry(ctx context.Context, callback func()) {
 	waitForInitialConnectionTry(ctx)
 
 	srReconnectedChan := g.srWatcher.NewListener()
@@ -93,7 +83,7 @@ func (g *Guard) reconnectLoopWithRetry(ctx context.Context) {
 			}
 
 			if !g.isConnectedOnAllWay() {
-				g.triggerOfferSending()
+				callback()
 			}
 
 		case <-g.relayedConnDisconnected:
@@ -121,39 +111,12 @@ func (g *Guard) reconnectLoopWithRetry(ctx context.Context) {
 	}
 }
 
-// listenForDisconnectEvents is used when the peer is not a controller and it should reconnect to the peer
-// when the connection is lost. It will try to establish a connection only once time if before the connection was established
-// It track separately the ice and relay connection status. Just because a lower priority connection reestablished it does not
-// mean that to switch to it. We always force to use the higher priority connection.
-func (g *Guard) listenForDisconnectEvents(ctx context.Context) {
-	srReconnectedChan := g.srWatcher.NewListener()
-	defer g.srWatcher.RemoveListener(srReconnectedChan)
-
-	g.log.Infof("start listen for reconnect events...")
-	for {
-		select {
-		case <-g.relayedConnDisconnected:
-			g.log.Debugf("Relay connection changed, triggering reconnect")
-			g.triggerOfferSending()
-		case <-g.iCEConnDisconnected:
-			g.log.Debugf("ICE state changed, try to send new offer")
-			g.triggerOfferSending()
-		case <-srReconnectedChan:
-			g.triggerOfferSending()
-		case <-ctx.Done():
-			g.log.Debugf("context is done, stop reconnect loop")
-			return
-		}
-	}
-}
-
 func (g *Guard) prepareExponentTicker(ctx context.Context) *backoff.Ticker {
 	bo := backoff.WithContext(&backoff.ExponentialBackOff{
 		InitialInterval:     800 * time.Millisecond,
 		RandomizationFactor: 0.1,
 		Multiplier:          2,
 		MaxInterval:         g.timeout,
-		MaxElapsedTime:      reconnectMaxElapsedTime,
 		Stop:                backoff.Stop,
 		Clock:               backoff.SystemClock,
 	}, ctx)
@@ -164,13 +127,6 @@ func (g *Guard) prepareExponentTicker(ctx context.Context) *backoff.Ticker {
 	return ticker
 }
 
-func (g *Guard) triggerOfferSending() {
-	select {
-	case g.Reconnect <- struct{}{}:
-	default:
-	}
-}
-
 // Give chance to the peer to establish the initial connection.
 // With it, we can decrease to send necessary offer
 func waitForInitialConnectionTry(ctx context.Context) {
diff --git a/client/internal/peer/handshaker.go b/client/internal/peer/handshaker.go
index 224ea026..bf4335fe 100644
--- a/client/internal/peer/handshaker.go
+++ b/client/internal/peer/handshaker.go
@@ -43,7 +43,6 @@ type OfferAnswer struct {
 
 type Handshaker struct {
 	mu                  sync.Mutex
-	ctx                 context.Context
 	log                 *log.Entry
 	config              ConnConfig
 	signaler            *Signaler
@@ -57,9 +56,8 @@ type Handshaker struct {
 	remoteAnswerCh chan OfferAnswer
 }
 
-func NewHandshaker(ctx context.Context, log *log.Entry, config ConnConfig, signaler *Signaler, ice *WorkerICE, relay *WorkerRelay) *Handshaker {
+func NewHandshaker(log *log.Entry, config ConnConfig, signaler *Signaler, ice *WorkerICE, relay *WorkerRelay) *Handshaker {
 	return &Handshaker{
-		ctx:            ctx,
 		log:            log,
 		config:         config,
 		signaler:       signaler,
@@ -74,10 +72,10 @@ func (h *Handshaker) AddOnNewOfferListener(offer func(remoteOfferAnswer *OfferAn
 	h.onNewOfferListeners = append(h.onNewOfferListeners, offer)
 }
 
-func (h *Handshaker) Listen() {
+func (h *Handshaker) Listen(ctx context.Context) {
 	for {
 		h.log.Info("wait for remote offer confirmation")
-		remoteOfferAnswer, err := h.waitForRemoteOfferConfirmation()
+		remoteOfferAnswer, err := h.waitForRemoteOfferConfirmation(ctx)
 		if err != nil {
 			var connectionClosedError *ConnectionClosedError
 			if errors.As(err, &connectionClosedError) {
@@ -127,7 +125,7 @@ func (h *Handshaker) OnRemoteAnswer(answer OfferAnswer) bool {
 	}
 }
 
-func (h *Handshaker) waitForRemoteOfferConfirmation() (*OfferAnswer, error) {
+func (h *Handshaker) waitForRemoteOfferConfirmation(ctx context.Context) (*OfferAnswer, error) {
 	select {
 	case remoteOfferAnswer := <-h.remoteOffersCh:
 		// received confirmation from the remote peer -> ready to proceed
@@ -137,7 +135,7 @@ func (h *Handshaker) waitForRemoteOfferConfirmation() (*OfferAnswer, error) {
 		return &remoteOfferAnswer, nil
 	case remoteOfferAnswer := <-h.remoteAnswerCh:
 		return &remoteOfferAnswer, nil
-	case <-h.ctx.Done():
+	case <-ctx.Done():
 		// closed externally
 		return nil, NewConnectionClosedError(h.config.Key)
 	}
diff --git a/client/internal/peer/id/connid.go b/client/internal/peer/id/connid.go
new file mode 100644
index 00000000..43c4c730
--- /dev/null
+++ b/client/internal/peer/id/connid.go
@@ -0,0 +1,5 @@
+package id
+
+import "unsafe"
+
+type ConnID unsafe.Pointer
diff --git a/client/internal/peer/iface.go b/client/internal/peer/iface.go
index 32ac5c7d..0bcc7a68 100644
--- a/client/internal/peer/iface.go
+++ b/client/internal/peer/iface.go
@@ -15,7 +15,7 @@ import (
 type WGIface interface {
 	UpdatePeer(peerKey string, allowedIps []netip.Prefix, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error
 	RemovePeer(peerKey string) error
-	GetStats(peerKey string) (configurer.WGStats, error)
+	GetStats() (map[string]configurer.WGStats, error)
 	GetProxy() wgproxy.Proxy
 	Address() wgaddr.Address
 }
diff --git a/client/internal/peer/notifier.go b/client/internal/peer/notifier.go
index f1175c2c..8d1954fe 100644
--- a/client/internal/peer/notifier.go
+++ b/client/internal/peer/notifier.go
@@ -18,6 +18,8 @@ type notifier struct {
 	currentClientState bool
 	lastNotification   int
 	lastNumberOfPeers  int
+	lastFqdnAddress    string
+	lastIPAddress      string
 }
 
 func newNotifier() *notifier {
@@ -25,15 +27,22 @@ func newNotifier() *notifier {
 }
 
 func (n *notifier) setListener(listener Listener) {
-	n.listenersLock.Lock()
-	defer n.listenersLock.Unlock()
-
 	n.serverStateLock.Lock()
-	n.notifyListener(listener, n.lastNotification)
-	listener.OnPeersListChanged(n.lastNumberOfPeers)
+	lastNotification := n.lastNotification
+	numOfPeers := n.lastNumberOfPeers
+	fqdnAddress := n.lastFqdnAddress
+	address := n.lastIPAddress
 	n.serverStateLock.Unlock()
 
+	n.listenersLock.Lock()
+	defer n.listenersLock.Unlock()
+
 	n.listener = listener
+
+	listener.OnAddressChanged(fqdnAddress, address)
+	notifyListener(listener, lastNotification)
+	// run on go routine to avoid on Java layer to call go functions on same thread
+	go listener.OnPeersListChanged(numOfPeers)
 }
 
 func (n *notifier) removeListener() {
@@ -44,41 +53,44 @@ func (n *notifier) removeListener() {
 
 func (n *notifier) updateServerStates(mgmState bool, signalState bool) {
 	n.serverStateLock.Lock()
-	defer n.serverStateLock.Unlock()
-
 	calculatedState := n.calculateState(mgmState, signalState)
 
 	if !n.isServerStateChanged(calculatedState) {
+		n.serverStateLock.Unlock()
 		return
 	}
 
 	n.lastNotification = calculatedState
+	n.serverStateLock.Unlock()
 
-	n.notify(n.lastNotification)
+	n.notify(calculatedState)
 }
 
 func (n *notifier) clientStart() {
 	n.serverStateLock.Lock()
-	defer n.serverStateLock.Unlock()
 	n.currentClientState = true
 	n.lastNotification = stateConnecting
-	n.notify(n.lastNotification)
+	n.serverStateLock.Unlock()
+
+	n.notify(stateConnecting)
 }
 
 func (n *notifier) clientStop() {
 	n.serverStateLock.Lock()
-	defer n.serverStateLock.Unlock()
 	n.currentClientState = false
 	n.lastNotification = stateDisconnected
-	n.notify(n.lastNotification)
+	n.serverStateLock.Unlock()
+
+	n.notify(stateDisconnected)
 }
 
 func (n *notifier) clientTearDown() {
 	n.serverStateLock.Lock()
-	defer n.serverStateLock.Unlock()
 	n.currentClientState = false
 	n.lastNotification = stateDisconnecting
-	n.notify(n.lastNotification)
+	n.serverStateLock.Unlock()
+
+	n.notify(stateDisconnecting)
 }
 
 func (n *notifier) isServerStateChanged(newState int) bool {
@@ -87,26 +99,14 @@ func (n *notifier) isServerStateChanged(newState int) bool {
 
 func (n *notifier) notify(state int) {
 	n.listenersLock.Lock()
-	defer n.listenersLock.Unlock()
-	if n.listener == nil {
+	listener := n.listener
+	n.listenersLock.Unlock()
+
+	if listener == nil {
 		return
 	}
-	n.notifyListener(n.listener, state)
-}
 
-func (n *notifier) notifyListener(l Listener, state int) {
-	go func() {
-		switch state {
-		case stateDisconnected:
-			l.OnDisconnected()
-		case stateConnected:
-			l.OnConnected()
-		case stateConnecting:
-			l.OnConnecting()
-		case stateDisconnecting:
-			l.OnDisconnecting()
-		}
-	}()
+	notifyListener(listener, state)
 }
 
 func (n *notifier) calculateState(managementConn, signalConn bool) int {
@@ -126,20 +126,48 @@ func (n *notifier) calculateState(managementConn, signalConn bool) int {
 }
 
 func (n *notifier) peerListChanged(numOfPeers int) {
+	n.serverStateLock.Lock()
 	n.lastNumberOfPeers = numOfPeers
+	n.serverStateLock.Unlock()
+
 	n.listenersLock.Lock()
-	defer n.listenersLock.Unlock()
-	if n.listener == nil {
+	listener := n.listener
+	n.listenersLock.Unlock()
+
+	if listener == nil {
 		return
 	}
-	n.listener.OnPeersListChanged(numOfPeers)
+
+	// run on go routine to avoid on Java layer to call go functions on same thread
+	go listener.OnPeersListChanged(numOfPeers)
 }
 
 func (n *notifier) localAddressChanged(fqdn, address string) {
+	n.serverStateLock.Lock()
+	n.lastFqdnAddress = fqdn
+	n.lastIPAddress = address
+	n.serverStateLock.Unlock()
+
 	n.listenersLock.Lock()
-	defer n.listenersLock.Unlock()
-	if n.listener == nil {
+	listener := n.listener
+	n.listenersLock.Unlock()
+
+	if listener == nil {
 		return
 	}
-	n.listener.OnAddressChanged(fqdn, address)
+
+	listener.OnAddressChanged(fqdn, address)
+}
+
+func notifyListener(l Listener, state int) {
+	switch state {
+	case stateDisconnected:
+		l.OnDisconnected()
+	case stateConnected:
+		l.OnConnected()
+	case stateConnecting:
+		l.OnConnecting()
+	case stateDisconnecting:
+		l.OnDisconnecting()
+	}
 }
diff --git a/client/internal/peer/route.go b/client/internal/peer/route.go
index c3567dcc..e5e315e3 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 9b3fc744..ed2f1fe4 100644
--- a/client/internal/peer/status.go
+++ b/client/internal/peer/status.go
@@ -1,7 +1,9 @@
 package peer
 
 import (
+	"context"
 	"errors"
+	"fmt"
 	"net/netip"
 	"slices"
 	"sync"
@@ -21,6 +23,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
@@ -30,6 +33,10 @@ type ResolvedDomainInfo struct {
 	ParentDomain domain.Domain
 }
 
+type WGIfaceStatus interface {
+	FullStats() (*configurer.Stats, error)
+}
+
 type EventListener interface {
 	OnEvent(event *proto.SystemEvent)
 }
@@ -134,21 +141,42 @@ type NSGroupState struct {
 
 // FullStatus contains the full state held by the Status instance
 type FullStatus struct {
-	Peers                []State
-	ManagementState      ManagementState
-	SignalState          SignalState
-	LocalPeerState       LocalPeerState
-	RosenpassState       RosenpassState
-	Relays               []relay.ProbeResult
-	NSGroupStates        []NSGroupState
-	NumOfForwardingRules int
+	Peers                 []State
+	ManagementState       ManagementState
+	SignalState           SignalState
+	LocalPeerState        LocalPeerState
+	RosenpassState        RosenpassState
+	Relays                []relay.ProbeResult
+	NSGroupStates         []NSGroupState
+	NumOfForwardingRules  int
+	LazyConnectionEnabled bool
+}
+
+type StatusChangeSubscription struct {
+	peerID     string
+	id         string
+	eventsChan chan struct{}
+	ctx        context.Context
+}
+
+func newStatusChangeSubscription(ctx context.Context, peerID string) *StatusChangeSubscription {
+	return &StatusChangeSubscription{
+		ctx:        ctx,
+		peerID:     peerID,
+		id:         uuid.New().String(),
+		eventsChan: make(chan struct{}, 1),
+	}
+}
+
+func (s *StatusChangeSubscription) Events() chan struct{} {
+	return s.eventsChan
 }
 
 // Status holds a state of peers, signal, management connections and relays
 type Status struct {
 	mux                   sync.Mutex
 	peers                 map[string]State
-	changeNotify          map[string]chan struct{}
+	changeNotify          map[string]map[string]*StatusChangeSubscription // map[peerID]map[subscriptionID]*StatusChangeSubscription
 	signalState           bool
 	signalError           error
 	managementState       bool
@@ -163,6 +191,7 @@ type Status struct {
 	rosenpassPermissive   bool
 	nsGroupStates         []NSGroupState
 	resolvedDomainsStates map[domain.Domain]ResolvedDomainInfo
+	lazyConnectionEnabled bool
 
 	// To reduce the number of notification invocation this bool will be true when need to call the notification
 	// Some Peer actions mostly used by in a batch when the network map has been synchronized. In these type of events
@@ -178,13 +207,14 @@ type Status struct {
 	ingressGwMgr *ingressgw.Manager
 
 	routeIDLookup routeIDLookup
+	wgIface       WGIfaceStatus
 }
 
 // NewRecorder returns a new Status instance
 func NewRecorder(mgmAddress string) *Status {
 	return &Status{
 		peers:                 make(map[string]State),
-		changeNotify:          make(map[string]chan struct{}),
+		changeNotify:          make(map[string]map[string]*StatusChangeSubscription),
 		eventStreams:          make(map[string]chan *proto.SystemEvent),
 		eventQueue:            NewEventQueue(eventQueueSize),
 		offlinePeers:          make([]State, 0),
@@ -218,7 +248,7 @@ func (d *Status) ReplaceOfflinePeers(replacement []State) {
 }
 
 // AddPeer adds peer to Daemon status map
-func (d *Status) AddPeer(peerPubKey string, fqdn string) error {
+func (d *Status) AddPeer(peerPubKey string, fqdn string, ip string) error {
 	d.mux.Lock()
 	defer d.mux.Unlock()
 
@@ -228,7 +258,8 @@ func (d *Status) AddPeer(peerPubKey string, fqdn string) error {
 	}
 	d.peers[peerPubKey] = State{
 		PubKey:     peerPubKey,
-		ConnStatus: StatusDisconnected,
+		IP:         ip,
+		ConnStatus: StatusIdle,
 		FQDN:       fqdn,
 		Mux:        new(sync.RWMutex),
 	}
@@ -285,11 +316,7 @@ func (d *Status) UpdatePeerState(receivedState State) error {
 		return errors.New("peer doesn't exist")
 	}
 
-	if receivedState.IP != "" {
-		peerState.IP = receivedState.IP
-	}
-
-	skipNotification := shouldSkipNotify(receivedState.ConnStatus, peerState)
+	oldState := peerState.ConnStatus
 
 	if receivedState.ConnStatus != peerState.ConnStatus {
 		peerState.ConnStatus = receivedState.ConnStatus
@@ -305,15 +332,18 @@ func (d *Status) UpdatePeerState(receivedState State) error {
 
 	d.peers[receivedState.PubKey] = peerState
 
-	if skipNotification {
-		return nil
+	if hasConnStatusChanged(oldState, receivedState.ConnStatus) {
+		d.notifyPeerListChanged()
 	}
 
-	d.notifyPeerListChanged()
+	// when we close the connection we will not notify the router manager
+	if receivedState.ConnStatus == StatusIdle {
+		d.notifyPeerStateChangeListeners(receivedState.PubKey)
+	}
 	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()
 
@@ -376,11 +406,8 @@ func (d *Status) UpdatePeerICEState(receivedState State) error {
 		return errors.New("peer doesn't exist")
 	}
 
-	if receivedState.IP != "" {
-		peerState.IP = receivedState.IP
-	}
-
-	skipNotification := shouldSkipNotify(receivedState.ConnStatus, peerState)
+	oldState := peerState.ConnStatus
+	oldIsRelayed := peerState.Relayed
 
 	peerState.ConnStatus = receivedState.ConnStatus
 	peerState.ConnStatusUpdate = receivedState.ConnStatusUpdate
@@ -393,12 +420,13 @@ func (d *Status) UpdatePeerICEState(receivedState State) error {
 
 	d.peers[receivedState.PubKey] = peerState
 
-	if skipNotification {
-		return nil
+	if hasConnStatusChanged(oldState, receivedState.ConnStatus) {
+		d.notifyPeerListChanged()
 	}
 
-	d.notifyPeerStateChangeListeners(receivedState.PubKey)
-	d.notifyPeerListChanged()
+	if hasStatusOrRelayedChange(oldState, receivedState.ConnStatus, oldIsRelayed, receivedState.Relayed) {
+		d.notifyPeerStateChangeListeners(receivedState.PubKey)
+	}
 	return nil
 }
 
@@ -411,7 +439,8 @@ func (d *Status) UpdatePeerRelayedState(receivedState State) error {
 		return errors.New("peer doesn't exist")
 	}
 
-	skipNotification := shouldSkipNotify(receivedState.ConnStatus, peerState)
+	oldState := peerState.ConnStatus
+	oldIsRelayed := peerState.Relayed
 
 	peerState.ConnStatus = receivedState.ConnStatus
 	peerState.ConnStatusUpdate = receivedState.ConnStatusUpdate
@@ -421,12 +450,13 @@ func (d *Status) UpdatePeerRelayedState(receivedState State) error {
 
 	d.peers[receivedState.PubKey] = peerState
 
-	if skipNotification {
-		return nil
+	if hasConnStatusChanged(oldState, receivedState.ConnStatus) {
+		d.notifyPeerListChanged()
 	}
 
-	d.notifyPeerStateChangeListeners(receivedState.PubKey)
-	d.notifyPeerListChanged()
+	if hasStatusOrRelayedChange(oldState, receivedState.ConnStatus, oldIsRelayed, receivedState.Relayed) {
+		d.notifyPeerStateChangeListeners(receivedState.PubKey)
+	}
 	return nil
 }
 
@@ -439,7 +469,8 @@ func (d *Status) UpdatePeerRelayedStateToDisconnected(receivedState State) error
 		return errors.New("peer doesn't exist")
 	}
 
-	skipNotification := shouldSkipNotify(receivedState.ConnStatus, peerState)
+	oldState := peerState.ConnStatus
+	oldIsRelayed := peerState.Relayed
 
 	peerState.ConnStatus = receivedState.ConnStatus
 	peerState.Relayed = receivedState.Relayed
@@ -448,12 +479,13 @@ func (d *Status) UpdatePeerRelayedStateToDisconnected(receivedState State) error
 
 	d.peers[receivedState.PubKey] = peerState
 
-	if skipNotification {
-		return nil
+	if hasConnStatusChanged(oldState, receivedState.ConnStatus) {
+		d.notifyPeerListChanged()
 	}
 
-	d.notifyPeerStateChangeListeners(receivedState.PubKey)
-	d.notifyPeerListChanged()
+	if hasStatusOrRelayedChange(oldState, receivedState.ConnStatus, oldIsRelayed, receivedState.Relayed) {
+		d.notifyPeerStateChangeListeners(receivedState.PubKey)
+	}
 	return nil
 }
 
@@ -466,7 +498,8 @@ func (d *Status) UpdatePeerICEStateToDisconnected(receivedState State) error {
 		return errors.New("peer doesn't exist")
 	}
 
-	skipNotification := shouldSkipNotify(receivedState.ConnStatus, peerState)
+	oldState := peerState.ConnStatus
+	oldIsRelayed := peerState.Relayed
 
 	peerState.ConnStatus = receivedState.ConnStatus
 	peerState.Relayed = receivedState.Relayed
@@ -478,12 +511,13 @@ func (d *Status) UpdatePeerICEStateToDisconnected(receivedState State) error {
 
 	d.peers[receivedState.PubKey] = peerState
 
-	if skipNotification {
-		return nil
+	if hasConnStatusChanged(oldState, receivedState.ConnStatus) {
+		d.notifyPeerListChanged()
 	}
 
-	d.notifyPeerStateChangeListeners(receivedState.PubKey)
-	d.notifyPeerListChanged()
+	if hasStatusOrRelayedChange(oldState, receivedState.ConnStatus, oldIsRelayed, receivedState.Relayed) {
+		d.notifyPeerStateChangeListeners(receivedState.PubKey)
+	}
 	return nil
 }
 
@@ -506,17 +540,12 @@ func (d *Status) UpdateWireGuardPeerState(pubKey string, wgStats configurer.WGSt
 	return nil
 }
 
-func shouldSkipNotify(receivedConnStatus ConnStatus, curr State) bool {
-	switch {
-	case receivedConnStatus == StatusConnecting:
-		return true
-	case receivedConnStatus == StatusDisconnected && curr.ConnStatus == StatusConnecting:
-		return true
-	case receivedConnStatus == StatusDisconnected && curr.ConnStatus == StatusDisconnected:
-		return curr.IP != ""
-	default:
-		return false
-	}
+func hasStatusOrRelayedChange(oldConnStatus, newConnStatus ConnStatus, oldRelayed, newRelayed bool) bool {
+	return oldRelayed != newRelayed || hasConnStatusChanged(newConnStatus, oldConnStatus)
+}
+
+func hasConnStatusChanged(oldStatus, newStatus ConnStatus) bool {
+	return newStatus != oldStatus
 }
 
 // UpdatePeerFQDN update peer's state fqdn only
@@ -549,19 +578,41 @@ func (d *Status) FinishPeerListModifications() {
 	d.notifyPeerListChanged()
 }
 
-// GetPeerStateChangeNotifier returns a change notifier channel for a peer
-func (d *Status) GetPeerStateChangeNotifier(peer string) <-chan struct{} {
+func (d *Status) SubscribeToPeerStateChanges(ctx context.Context, peerID string) *StatusChangeSubscription {
+	d.mux.Lock()
+	defer d.mux.Unlock()
+
+	sub := newStatusChangeSubscription(ctx, peerID)
+	if _, ok := d.changeNotify[peerID]; !ok {
+		d.changeNotify[peerID] = make(map[string]*StatusChangeSubscription)
+	}
+	d.changeNotify[peerID][sub.id] = sub
+
+	return sub
+}
+
+func (d *Status) UnsubscribePeerStateChanges(subscription *StatusChangeSubscription) {
 	d.mux.Lock()
 	defer d.mux.Unlock()
 
-	ch, found := d.changeNotify[peer]
-	if found {
-		return ch
+	if subscription == nil {
+		return
+	}
+
+	channels, ok := d.changeNotify[subscription.peerID]
+	if !ok {
+		return
+	}
+
+	sub, exists := channels[subscription.id]
+	if !exists {
+		return
 	}
 
-	ch = make(chan struct{})
-	d.changeNotify[peer] = ch
-	return ch
+	delete(channels, subscription.id)
+	if len(channels) == 0 {
+		delete(d.changeNotify, sub.peerID)
+	}
 }
 
 // GetLocalPeerState returns the local peer state
@@ -581,7 +632,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 +662,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
@@ -691,6 +739,12 @@ func (d *Status) UpdateRosenpass(rosenpassEnabled, rosenpassPermissive bool) {
 	d.rosenpassEnabled = rosenpassEnabled
 }
 
+func (d *Status) UpdateLazyConnection(enabled bool) {
+	d.mux.Lock()
+	defer d.mux.Unlock()
+	d.lazyConnectionEnabled = enabled
+}
+
 // MarkSignalDisconnected sets SignalState to disconnected
 func (d *Status) MarkSignalDisconnected(err error) {
 	d.mux.Lock()
@@ -723,7 +777,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()
 
@@ -763,6 +817,12 @@ func (d *Status) GetRosenpassState() RosenpassState {
 	}
 }
 
+func (d *Status) GetLazyConnection() bool {
+	d.mux.Lock()
+	defer d.mux.Unlock()
+	return d.lazyConnectionEnabled
+}
+
 func (d *Status) GetManagementState() ManagementState {
 	d.mux.Lock()
 	defer d.mux.Unlock()
@@ -874,12 +934,13 @@ func (d *Status) GetResolvedDomainsStates() map[domain.Domain]ResolvedDomainInfo
 // GetFullStatus gets full status
 func (d *Status) GetFullStatus() FullStatus {
 	fullStatus := FullStatus{
-		ManagementState:      d.GetManagementState(),
-		SignalState:          d.GetSignalState(),
-		Relays:               d.GetRelayStates(),
-		RosenpassState:       d.GetRosenpassState(),
-		NSGroupStates:        d.GetDNSStates(),
-		NumOfForwardingRules: len(d.ForwardingRules()),
+		ManagementState:       d.GetManagementState(),
+		SignalState:           d.GetSignalState(),
+		Relays:                d.GetRelayStates(),
+		RosenpassState:        d.GetRosenpassState(),
+		NSGroupStates:         d.GetDNSStates(),
+		NumOfForwardingRules:  len(d.ForwardingRules()),
+		LazyConnectionEnabled: d.GetLazyConnection(),
 	}
 
 	d.mux.Lock()
@@ -926,13 +987,20 @@ func (d *Status) onConnectionChanged() {
 
 // notifyPeerStateChangeListeners notifies route manager about the change in peer state
 func (d *Status) notifyPeerStateChangeListeners(peerID string) {
-	ch, found := d.changeNotify[peerID]
-	if !found {
+	subs, ok := d.changeNotify[peerID]
+	if !ok {
 		return
 	}
-
-	close(ch)
-	delete(d.changeNotify, peerID)
+	for _, sub := range subs {
+		// block the write because we do not want to miss notification
+		// must have to be sure we will run the GetPeerState() on separated thread
+		go func() {
+			select {
+			case sub.eventsChan <- struct{}{}:
+			case <-sub.ctx.Done():
+			}
+		}()
+	}
 }
 
 func (d *Status) notifyPeerListChanged() {
@@ -1016,6 +1084,23 @@ func (d *Status) GetEventHistory() []*proto.SystemEvent {
 	return d.eventQueue.GetAll()
 }
 
+func (d *Status) SetWgIface(wgInterface WGIfaceStatus) {
+	d.mux.Lock()
+	defer d.mux.Unlock()
+
+	d.wgIface = wgInterface
+}
+
+func (d *Status) PeersStatus() (*configurer.Stats, error) {
+	d.mux.Lock()
+	defer d.mux.Unlock()
+	if d.wgIface == nil {
+		return nil, fmt.Errorf("wgInterface is nil, cannot retrieve peers status")
+	}
+
+	return d.wgIface.FullStats()
+}
+
 type EventQueue struct {
 	maxSize int
 	events  []*proto.SystemEvent
diff --git a/client/internal/peer/wg_watcher.go b/client/internal/peer/wg_watcher.go
index 589f405b..218872c1 100644
--- a/client/internal/peer/wg_watcher.go
+++ b/client/internal/peer/wg_watcher.go
@@ -2,6 +2,7 @@ package peer
 
 import (
 	"context"
+	"fmt"
 	"sync"
 	"time"
 
@@ -20,7 +21,7 @@ var (
 )
 
 type WGInterfaceStater interface {
-	GetStats(key string) (configurer.WGStats, error)
+	GetStats() (map[string]configurer.WGStats, error)
 }
 
 type WGWatcher struct {
@@ -146,9 +147,13 @@ func (w *WGWatcher) handshakeCheck(lastHandshake time.Time) (*time.Time, bool) {
 }
 
 func (w *WGWatcher) wgState() (time.Time, error) {
-	wgState, err := w.wgIfaceStater.GetStats(w.peerKey)
+	wgStates, err := w.wgIfaceStater.GetStats()
 	if err != nil {
 		return time.Time{}, err
 	}
+	wgState, ok := wgStates[w.peerKey]
+	if !ok {
+		return time.Time{}, fmt.Errorf("peer %s not found in WireGuard endpoints", w.peerKey)
+	}
 	return wgState.LastHandshake, nil
 }
diff --git a/client/internal/peer/worker/state.go b/client/internal/peer/worker/state.go
new file mode 100644
index 00000000..14b53aa4
--- /dev/null
+++ b/client/internal/peer/worker/state.go
@@ -0,0 +1,55 @@
+package worker
+
+import (
+	"sync/atomic"
+
+	log "github.com/sirupsen/logrus"
+)
+
+const (
+	StatusDisconnected Status = iota
+	StatusConnected
+)
+
+type Status int32
+
+func (s Status) String() string {
+	switch s {
+	case StatusDisconnected:
+		return "Disconnected"
+	case StatusConnected:
+		return "Connected"
+	default:
+		log.Errorf("unknown status: %d", s)
+		return "unknown"
+	}
+}
+
+// AtomicWorkerStatus is a thread-safe wrapper for worker status
+type AtomicWorkerStatus struct {
+	status atomic.Int32
+}
+
+func NewAtomicStatus() *AtomicWorkerStatus {
+	acs := &AtomicWorkerStatus{}
+	acs.SetDisconnected()
+	return acs
+}
+
+// Get returns the current connection status
+func (acs *AtomicWorkerStatus) Get() Status {
+	return Status(acs.status.Load())
+}
+
+func (acs *AtomicWorkerStatus) SetConnected() {
+	acs.status.Store(int32(StatusConnected))
+}
+
+func (acs *AtomicWorkerStatus) SetDisconnected() {
+	acs.status.Store(int32(StatusDisconnected))
+}
+
+// String returns the string representation of the current status
+func (acs *AtomicWorkerStatus) String() string {
+	return acs.Get().String()
+}
diff --git a/client/internal/peer/worker_ice.go b/client/internal/peer/worker_ice.go
index 4ff13b45..e99c50d2 100644
--- a/client/internal/peer/worker_ice.go
+++ b/client/internal/peer/worker_ice.go
@@ -14,6 +14,7 @@ import (
 
 	"github.com/netbirdio/netbird/client/iface"
 	"github.com/netbirdio/netbird/client/iface/bind"
+	"github.com/netbirdio/netbird/client/internal/peer/conntype"
 	icemaker "github.com/netbirdio/netbird/client/internal/peer/ice"
 	"github.com/netbirdio/netbird/client/internal/stdnet"
 	"github.com/netbirdio/netbird/route"
@@ -397,10 +398,10 @@ func isRelayed(pair *ice.CandidatePair) bool {
 	return false
 }
 
-func selectedPriority(pair *ice.CandidatePair) ConnPriority {
+func selectedPriority(pair *ice.CandidatePair) conntype.ConnPriority {
 	if isRelayed(pair) {
-		return connPriorityICETurn
+		return conntype.ICETurn
 	} else {
-		return connPriorityICEP2P
+		return conntype.ICEP2P
 	}
 }
diff --git a/client/internal/peerstore/store.go b/client/internal/peerstore/store.go
index 15d34d3d..81ac7a5b 100644
--- a/client/internal/peerstore/store.go
+++ b/client/internal/peerstore/store.go
@@ -1,6 +1,7 @@
 package peerstore
 
 import (
+	"context"
 	"net/netip"
 	"sync"
 
@@ -79,6 +80,32 @@ func (s *Store) PeerConn(pubKey string) (*peer.Conn, bool) {
 	return p, true
 }
 
+func (s *Store) PeerConnOpen(ctx context.Context, pubKey string) {
+	s.peerConnsMu.RLock()
+	defer s.peerConnsMu.RUnlock()
+
+	p, ok := s.peerConns[pubKey]
+	if !ok {
+		return
+	}
+	// this can be blocked because of the connect open limiter semaphore
+	if err := p.Open(ctx); err != nil {
+		p.Log.Errorf("failed to open peer connection: %v", err)
+	}
+
+}
+
+func (s *Store) PeerConnClose(pubKey string) {
+	s.peerConnsMu.RLock()
+	defer s.peerConnsMu.RUnlock()
+
+	p, ok := s.peerConns[pubKey]
+	if !ok {
+		return
+	}
+	p.Close()
+}
+
 func (s *Store) PeersPubKey() []string {
 	s.peerConnsMu.RLock()
 	defer s.peerConnsMu.RUnlock()
diff --git a/client/internal/pkce_auth.go b/client/internal/pkce_auth.go
index 34eb2df1..f020d7a9 100644
--- a/client/internal/pkce_auth.go
+++ b/client/internal/pkce_auth.go
@@ -12,6 +12,7 @@ import (
 	"google.golang.org/grpc/status"
 
 	mgm "github.com/netbirdio/netbird/management/client"
+	"github.com/netbirdio/netbird/management/client/common"
 )
 
 // PKCEAuthorizationFlow represents PKCE Authorization Flow information
@@ -41,6 +42,8 @@ type PKCEAuthProviderConfig struct {
 	ClientCertPair *tls.Certificate
 	// DisablePromptLogin makes the PKCE flow to not prompt the user for login
 	DisablePromptLogin bool
+	// LoginFlag is used to configure the PKCE flow login behavior
+	LoginFlag common.LoginFlag
 }
 
 // GetPKCEAuthorizationFlowInfo initialize a PKCEAuthorizationFlow instance and return with it
@@ -100,6 +103,7 @@ func GetPKCEAuthorizationFlowInfo(ctx context.Context, privateKey string, mgmURL
 			UseIDToken:            protoPKCEAuthorizationFlow.GetProviderConfig().GetUseIDToken(),
 			ClientCertPair:        clientCert,
 			DisablePromptLogin:    protoPKCEAuthorizationFlow.GetProviderConfig().GetDisablePromptLogin(),
+			LoginFlag:             common.LoginFlag(protoPKCEAuthorizationFlow.GetProviderConfig().GetLoginFlag()),
 		},
 	}
 
diff --git a/client/internal/routemanager/client.go b/client/internal/routemanager/client/client.go
similarity index 51%
rename from client/internal/routemanager/client.go
rename to client/internal/routemanager/client/client.go
index 847949a5..5582591a 100644
--- a/client/internal/routemanager/client.go
+++ b/client/internal/routemanager/client/client.go
@@ -1,4 +1,4 @@
-package routemanager
+package client
 
 import (
 	"context"
@@ -7,10 +7,8 @@ import (
 	"runtime"
 	"time"
 
-	"github.com/hashicorp/go-multierror"
 	log "github.com/sirupsen/logrus"
 
-	nberrors "github.com/netbirdio/netbird/client/errors"
 	nbdns "github.com/netbirdio/netbird/client/internal/dns"
 	"github.com/netbirdio/netbird/client/internal/peer"
 	"github.com/netbirdio/netbird/client/internal/peerstore"
@@ -36,6 +34,7 @@ const (
 	reasonRouteUpdate
 	reasonPeerUpdate
 	reasonShutdown
+	reasonHA
 )
 
 type routerPeerStatus struct {
@@ -44,9 +43,9 @@ type routerPeerStatus struct {
 	latency   time.Duration
 }
 
-type routesUpdate struct {
-	updateSerial uint64
-	routes       []*route.Route
+type RoutesUpdate struct {
+	UpdateSerial uint64
+	Routes       []*route.Route
 }
 
 // RouteHandler defines the interface for handling routes
@@ -58,64 +57,54 @@ type RouteHandler interface {
 	RemoveAllowedIPs() error
 }
 
-type clientNetwork struct {
+type WatcherConfig struct {
+	Context          context.Context
+	DNSRouteInterval time.Duration
+	WGInterface      iface.WGIface
+	StatusRecorder   *peer.Status
+	Route            *route.Route
+	Handler          RouteHandler
+}
+
+// Watcher watches route and peer changes and updates allowed IPs accordingly.
+// Once stopped, it cannot be reused.
+type Watcher struct {
 	ctx                 context.Context
 	cancel              context.CancelFunc
 	statusRecorder      *peer.Status
 	wgInterface         iface.WGIface
 	routes              map[route.ID]*route.Route
-	routeUpdate         chan routesUpdate
+	routeUpdate         chan RoutesUpdate
 	peerStateUpdate     chan struct{}
-	routePeersNotifiers map[string]chan struct{}
+	routePeersNotifiers map[string]chan struct{} // map of peer key to channel for peer state changes
 	currentChosen       *route.Route
 	handler             RouteHandler
 	updateSerial        uint64
 }
 
-func newClientNetworkWatcher(
-	ctx context.Context,
-	dnsRouteInterval time.Duration,
-	wgInterface iface.WGIface,
-	statusRecorder *peer.Status,
-	rt *route.Route,
-	routeRefCounter *refcounter.RouteRefCounter,
-	allowedIPsRefCounter *refcounter.AllowedIPsRefCounter,
-	dnsServer nbdns.Server,
-	peerStore *peerstore.Store,
-	useNewDNSRoute bool,
-) *clientNetwork {
-	ctx, cancel := context.WithCancel(ctx)
+func NewWatcher(config WatcherConfig) *Watcher {
+	ctx, cancel := context.WithCancel(config.Context)
 
-	client := &clientNetwork{
+	client := &Watcher{
 		ctx:                 ctx,
 		cancel:              cancel,
-		statusRecorder:      statusRecorder,
-		wgInterface:         wgInterface,
+		statusRecorder:      config.StatusRecorder,
+		wgInterface:         config.WGInterface,
 		routes:              make(map[route.ID]*route.Route),
 		routePeersNotifiers: make(map[string]chan struct{}),
-		routeUpdate:         make(chan routesUpdate),
+		routeUpdate:         make(chan RoutesUpdate),
 		peerStateUpdate:     make(chan struct{}),
-		handler: handlerFromRoute(
-			rt,
-			routeRefCounter,
-			allowedIPsRefCounter,
-			dnsRouteInterval,
-			statusRecorder,
-			wgInterface,
-			dnsServer,
-			peerStore,
-			useNewDNSRoute,
-		),
+		handler:             config.Handler,
 	}
 	return client
 }
 
-func (c *clientNetwork) getRouterPeerStatuses() map[route.ID]routerPeerStatus {
+func (w *Watcher) getRouterPeerStatuses() map[route.ID]routerPeerStatus {
 	routePeerStatuses := make(map[route.ID]routerPeerStatus)
-	for _, r := range c.routes {
-		peerStatus, err := c.statusRecorder.GetPeer(r.Peer)
+	for _, r := range w.routes {
+		peerStatus, err := w.statusRecorder.GetPeer(r.Peer)
 		if err != nil {
-			log.Debugf("couldn't fetch peer state: %v", err)
+			log.Debugf("couldn't fetch peer state %v: %v", r.Peer, err)
 			continue
 		}
 		routePeerStatuses[r.ID] = routerPeerStatus{
@@ -128,7 +117,7 @@ func (c *clientNetwork) getRouterPeerStatuses() map[route.ID]routerPeerStatus {
 }
 
 // getBestRouteFromStatuses determines the most optimal route from the available routes
-// within a clientNetwork, taking into account peer connection status, route metrics, and
+// within a Watcher, taking into account peer connection status, route metrics, and
 // preference for non-relayed and direct connections.
 //
 // It follows these prioritization rules:
@@ -140,17 +129,17 @@ func (c *clientNetwork) getRouterPeerStatuses() map[route.ID]routerPeerStatus {
 // * Stability: In case of equal scores, the currently active route (if any) is maintained.
 //
 // It returns the ID of the selected optimal route.
-func (c *clientNetwork) getBestRouteFromStatuses(routePeerStatuses map[route.ID]routerPeerStatus) route.ID {
-	chosen := route.ID("")
+func (w *Watcher) getBestRouteFromStatuses(routePeerStatuses map[route.ID]routerPeerStatus) route.ID {
+	var chosen route.ID
 	chosenScore := float64(0)
 	currScore := float64(0)
 
-	currID := route.ID("")
-	if c.currentChosen != nil {
-		currID = c.currentChosen.ID
+	var currID route.ID
+	if w.currentChosen != nil {
+		currID = w.currentChosen.ID
 	}
 
-	for _, r := range c.routes {
+	for _, r := range w.routes {
 		tempScore := float64(0)
 		peerStatus, found := routePeerStatuses[r.ID]
 		if !found || !peerStatus.connected {
@@ -167,7 +156,7 @@ func (c *clientNetwork) getBestRouteFromStatuses(routePeerStatuses map[route.ID]
 		if peerStatus.latency != 0 {
 			latency = peerStatus.latency
 		} else {
-			log.Tracef("peer %s has 0 latency, range %s", r.Peer, c.handler)
+			log.Tracef("peer %s has 0 latency, range %s", r.Peer, w.handler)
 		}
 
 		// avoid negative tempScore on the higher latency calculation
@@ -197,149 +186,145 @@ func (c *clientNetwork) getBestRouteFromStatuses(routePeerStatuses map[route.ID]
 		}
 	}
 
-	log.Debugf("chosen route: %s, chosen score: %f, current route: %s, current score: %f", chosen, chosenScore, currID, currScore)
+	chosenID := chosen
+	if chosen == "" {
+		chosenID = "<none>"
+	}
+	currentID := currID
+	if currID == "" {
+		currentID = "<none>"
+	}
+
+	log.Debugf("chosen route: %s, chosen score: %f, current route: %s, current score: %f", chosenID, chosenScore, currentID, currScore)
 
 	switch {
 	case chosen == "":
 		var peers []string
-		for _, r := range c.routes {
+		for _, r := range w.routes {
 			peers = append(peers, r.Peer)
 		}
 
-		log.Warnf("The network [%v] has not been assigned a routing peer as no peers from the list %s are currently connected", c.handler, peers)
+		log.Infof("network [%v] has not been assigned a routing peer as no peers from the list %s are currently connected", w.handler, peers)
 	case chosen != currID:
 		// we compare the current score + 10ms to the chosen score to avoid flapping between routes
 		if currScore != 0 && currScore+0.01 > chosenScore {
-			log.Debugf("Keeping current routing peer because the score difference with latency is less than 0.01(10ms), current: %f, new: %f", currScore, chosenScore)
+			log.Debugf("keeping current routing peer %s for [%v]: the score difference with latency is less than 0.01(10ms): current: %f, new: %f",
+				w.currentChosen.Peer, w.handler, currScore, chosenScore)
 			return currID
 		}
 		var p string
-		if rt := c.routes[chosen]; rt != nil {
+		if rt := w.routes[chosen]; rt != nil {
 			p = rt.Peer
 		}
-		log.Infof("New chosen route is %s with peer %s with score %f for network [%v]", chosen, p, chosenScore, c.handler)
+		log.Infof("New chosen route is %s with peer %s with score %f for network [%v]", chosen, p, chosenScore, w.handler)
 	}
 
 	return chosen
 }
 
-func (c *clientNetwork) watchPeerStatusChanges(ctx context.Context, peerKey string, peerStateUpdate chan struct{}, closer chan struct{}) {
+func (w *Watcher) watchPeerStatusChanges(ctx context.Context, peerKey string, peerStateUpdate chan struct{}, closer chan struct{}) {
+	subscription := w.statusRecorder.SubscribeToPeerStateChanges(ctx, peerKey)
+	defer w.statusRecorder.UnsubscribePeerStateChanges(subscription)
+
 	for {
 		select {
 		case <-ctx.Done():
 			return
 		case <-closer:
 			return
-		case <-c.statusRecorder.GetPeerStateChangeNotifier(peerKey):
-			state, err := c.statusRecorder.GetPeer(peerKey)
-			if err != nil || state.ConnStatus == peer.StatusConnecting {
-				continue
-			}
+		case <-subscription.Events():
 			peerStateUpdate <- struct{}{}
-			log.Debugf("triggered route state update for Peer %s, state: %s", peerKey, state.ConnStatus)
+			log.Debugf("triggered route state update for Peer: %s", peerKey)
 		}
 	}
 }
 
-func (c *clientNetwork) startPeersStatusChangeWatcher() {
-	for _, r := range c.routes {
-		_, found := c.routePeersNotifiers[r.Peer]
-		if found {
+func (w *Watcher) startNewPeerStatusWatchers() {
+	for _, r := range w.routes {
+		if _, found := w.routePeersNotifiers[r.Peer]; found {
 			continue
 		}
 
 		closerChan := make(chan struct{})
-		c.routePeersNotifiers[r.Peer] = closerChan
-		go c.watchPeerStatusChanges(c.ctx, r.Peer, c.peerStateUpdate, closerChan)
+		w.routePeersNotifiers[r.Peer] = closerChan
+		go w.watchPeerStatusChanges(w.ctx, r.Peer, w.peerStateUpdate, closerChan)
 	}
 }
 
-func (c *clientNetwork) removeRouteFromWireGuardPeer() error {
-	if err := c.statusRecorder.RemovePeerStateRoute(c.currentChosen.Peer, c.handler.String()); err != nil {
-		log.Warnf("Failed to update peer state: %v", err)
+// addAllowedIPs adds the allowed IPs for the current chosen route to the handler.
+func (w *Watcher) addAllowedIPs(route *route.Route) error {
+	if err := w.handler.AddAllowedIPs(route.Peer); err != nil {
+		return fmt.Errorf("add allowed IPs for peer %s: %w", route.Peer, err)
 	}
 
-	if err := c.handler.RemoveAllowedIPs(); err != nil {
-		return fmt.Errorf("remove allowed IPs: %w", err)
+	if err := w.statusRecorder.AddPeerStateRoute(route.Peer, w.handler.String(), route.GetResourceID()); err != nil {
+		log.Warnf("Failed to update peer state: %v", err)
 	}
+
+	w.connectEvent(route)
 	return nil
 }
 
-func (c *clientNetwork) removeRouteFromPeerAndSystem(rsn reason) error {
-	if c.currentChosen == nil {
-		return nil
+func (w *Watcher) removeAllowedIPs(route *route.Route, rsn reason) error {
+	if err := w.statusRecorder.RemovePeerStateRoute(route.Peer, w.handler.String()); err != nil {
+		log.Warnf("Failed to update peer state: %v", err)
 	}
 
-	var merr *multierror.Error
-
-	if err := c.removeRouteFromWireGuardPeer(); err != nil {
-		merr = multierror.Append(merr, fmt.Errorf("remove allowed IPs for peer %s: %w", c.currentChosen.Peer, err))
-	}
-	if err := c.handler.RemoveRoute(); err != nil {
-		merr = multierror.Append(merr, fmt.Errorf("remove route: %w", err))
+	if err := w.handler.RemoveAllowedIPs(); err != nil {
+		return fmt.Errorf("remove allowed IPs: %w", err)
 	}
 
-	c.disconnectEvent(rsn)
+	w.disconnectEvent(route, rsn)
 
-	return nberrors.FormatErrorOrNil(merr)
+	return nil
 }
 
-func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem(rsn reason) error {
-	routerPeerStatuses := c.getRouterPeerStatuses()
+func (w *Watcher) recalculateRoutes(rsn reason) error {
+	routerPeerStatuses := w.getRouterPeerStatuses()
 
-	newChosenID := c.getBestRouteFromStatuses(routerPeerStatuses)
+	newChosenID := w.getBestRouteFromStatuses(routerPeerStatuses)
 
-	// If no route is chosen, remove the route from the peer and system
+	// If no route is chosen, remove the route from the peer
 	if newChosenID == "" {
-		if err := c.removeRouteFromPeerAndSystem(rsn); err != nil {
-			return fmt.Errorf("remove route for peer %s: %w", c.currentChosen.Peer, err)
+		if w.currentChosen == nil {
+			return nil
 		}
 
-		c.currentChosen = nil
+		if err := w.removeAllowedIPs(w.currentChosen, rsn); err != nil {
+			return fmt.Errorf("remove obsolete: %w", err)
+		}
+
+		w.currentChosen = nil
 
 		return nil
 	}
 
 	// If the chosen route is the same as the current route, do nothing
-	if c.currentChosen != nil && c.currentChosen.ID == newChosenID &&
-		c.currentChosen.Equal(c.routes[newChosenID]) {
+	if w.currentChosen != nil && w.currentChosen.ID == newChosenID &&
+		w.currentChosen.Equal(w.routes[newChosenID]) {
 		return nil
 	}
 
-	var isNew bool
-	if c.currentChosen == nil {
-		// If they were not previously assigned to another peer, add routes to the system first
-		if err := c.handler.AddRoute(c.ctx); err != nil {
-			return fmt.Errorf("add route: %w", err)
-		}
-		isNew = true
-	} else {
-		// Otherwise, remove the allowed IPs from the previous peer first
-		if err := c.removeRouteFromWireGuardPeer(); err != nil {
-			return fmt.Errorf("remove allowed IPs for peer %s: %w", c.currentChosen.Peer, err)
+	// If the chosen route was assigned to a different peer, remove the allowed IPs first
+	if isNew := w.currentChosen == nil; !isNew {
+		if err := w.removeAllowedIPs(w.currentChosen, reasonHA); err != nil {
+			return fmt.Errorf("remove old: %w", err)
 		}
 	}
 
-	c.currentChosen = c.routes[newChosenID]
-
-	if err := c.handler.AddAllowedIPs(c.currentChosen.Peer); err != nil {
-		return fmt.Errorf("add allowed IPs for peer %s: %w", c.currentChosen.Peer, err)
+	newChosenRoute := w.routes[newChosenID]
+	if err := w.addAllowedIPs(newChosenRoute); err != nil {
+		return fmt.Errorf("add new: %w", err)
 	}
 
-	if isNew {
-		c.connectEvent()
-	}
+	w.currentChosen = newChosenRoute
 
-	err := c.statusRecorder.AddPeerStateRoute(c.currentChosen.Peer, c.handler.String(), c.currentChosen.GetResourceID())
-	if err != nil {
-		return fmt.Errorf("add peer state route: %w", err)
-	}
 	return nil
 }
 
-func (c *clientNetwork) connectEvent() {
+func (w *Watcher) connectEvent(route *route.Route) {
 	var defaultRoute bool
-	for _, r := range c.routes {
+	for _, r := range w.routes {
 		if r.Network.Bits() == 0 {
 			defaultRoute = true
 			break
@@ -351,13 +336,13 @@ func (c *clientNetwork) connectEvent() {
 	}
 
 	meta := map[string]string{
-		"network": c.handler.String(),
+		"network": w.handler.String(),
 	}
-	if c.currentChosen != nil {
-		meta["id"] = string(c.currentChosen.NetID)
-		meta["peer"] = c.currentChosen.Peer
+	if route != nil {
+		meta["id"] = string(route.NetID)
+		meta["peer"] = route.Peer
 	}
-	c.statusRecorder.PublishEvent(
+	w.statusRecorder.PublishEvent(
 		proto.SystemEvent_INFO,
 		proto.SystemEvent_NETWORK,
 		"Default route added",
@@ -366,9 +351,9 @@ func (c *clientNetwork) connectEvent() {
 	)
 }
 
-func (c *clientNetwork) disconnectEvent(rsn reason) {
+func (w *Watcher) disconnectEvent(route *route.Route, rsn reason) {
 	var defaultRoute bool
-	for _, r := range c.routes {
+	for _, r := range w.routes {
 		if r.Network.Bits() == 0 {
 			defaultRoute = true
 			break
@@ -384,11 +369,11 @@ func (c *clientNetwork) disconnectEvent(rsn reason) {
 	var userMessage string
 	meta := make(map[string]string)
 
-	if c.currentChosen != nil {
-		meta["id"] = string(c.currentChosen.NetID)
-		meta["peer"] = c.currentChosen.Peer
+	if route != nil {
+		meta["id"] = string(route.NetID)
+		meta["peer"] = route.Peer
 	}
-	meta["network"] = c.handler.String()
+	meta["network"] = w.handler.String()
 	switch rsn {
 	case reasonShutdown:
 		severity = proto.SystemEvent_INFO
@@ -401,13 +386,17 @@ func (c *clientNetwork) disconnectEvent(rsn reason) {
 		severity = proto.SystemEvent_WARNING
 		message = "Default route disconnected due to peer unreachability"
 		userMessage = "Exit node connection lost. Your internet access might be affected."
+	case reasonHA:
+		severity = proto.SystemEvent_INFO
+		message = "Default route disconnected due to high availability change"
+		userMessage = "Exit node disconnected due to high availability change."
 	default:
 		severity = proto.SystemEvent_ERROR
 		message = "Default route disconnected for unknown reasons"
 		userMessage = "Exit node disconnected for unknown reasons."
 	}
 
-	c.statusRecorder.PublishEvent(
+	w.statusRecorder.PublishEvent(
 		severity,
 		proto.SystemEvent_NETWORK,
 		message,
@@ -416,86 +405,101 @@ func (c *clientNetwork) disconnectEvent(rsn reason) {
 	)
 }
 
-func (c *clientNetwork) sendUpdateToClientNetworkWatcher(update routesUpdate) {
+func (w *Watcher) SendUpdate(update RoutesUpdate) {
 	go func() {
-		c.routeUpdate <- update
+		select {
+		case w.routeUpdate <- update:
+		case <-w.ctx.Done():
+		}
 	}()
 }
 
-func (c *clientNetwork) handleUpdate(update routesUpdate) bool {
+func (w *Watcher) classifyUpdate(update RoutesUpdate) bool {
 	isUpdateMapDifferent := false
 	updateMap := make(map[route.ID]*route.Route)
 
-	for _, r := range update.routes {
+	for _, r := range update.Routes {
 		updateMap[r.ID] = r
 	}
 
-	if len(c.routes) != len(updateMap) {
+	if len(w.routes) != len(updateMap) {
 		isUpdateMapDifferent = true
 	}
 
-	for id, r := range c.routes {
+	for id, r := range w.routes {
 		_, found := updateMap[id]
 		if !found {
-			close(c.routePeersNotifiers[r.Peer])
-			delete(c.routePeersNotifiers, r.Peer)
+			close(w.routePeersNotifiers[r.Peer])
+			delete(w.routePeersNotifiers, r.Peer)
 			isUpdateMapDifferent = true
 			continue
 		}
-		if !reflect.DeepEqual(c.routes[id], updateMap[id]) {
+		if !reflect.DeepEqual(w.routes[id], updateMap[id]) {
 			isUpdateMapDifferent = true
 		}
 	}
 
-	c.routes = updateMap
+	w.routes = updateMap
 	return isUpdateMapDifferent
 }
 
-// peersStateAndUpdateWatcher is the main point of reacting on client network routing events.
+// Start is the main point of reacting on client network routing events.
 // All the processing related to the client network should be done here. Thread-safe.
-func (c *clientNetwork) peersStateAndUpdateWatcher() {
+func (w *Watcher) Start() {
 	for {
 		select {
-		case <-c.ctx.Done():
-			log.Debugf("Stopping watcher for network [%v]", c.handler)
-			if err := c.removeRouteFromPeerAndSystem(reasonShutdown); err != nil {
-				log.Errorf("Failed to remove routes for [%v]: %v", c.handler, err)
-			}
+		case <-w.ctx.Done():
 			return
-		case <-c.peerStateUpdate:
-			err := c.recalculateRouteAndUpdatePeerAndSystem(reasonPeerUpdate)
-			if err != nil {
-				log.Errorf("Failed to recalculate routes for network [%v]: %v", c.handler, err)
+		case <-w.peerStateUpdate:
+			if err := w.recalculateRoutes(reasonPeerUpdate); err != nil {
+				log.Errorf("Failed to recalculate routes for network [%v]: %v", w.handler, err)
 			}
-		case update := <-c.routeUpdate:
-			if update.updateSerial < c.updateSerial {
-				log.Warnf("Received a routes update with smaller serial number (%d -> %d), ignoring it", c.updateSerial, update.updateSerial)
+		case update := <-w.routeUpdate:
+			if update.UpdateSerial < w.updateSerial {
+				log.Warnf("Received a routes update with smaller serial number (%d -> %d), ignoring it", w.updateSerial, update.UpdateSerial)
 				continue
 			}
 
-			log.Debugf("Received a new client network route update for [%v]", c.handler)
+			w.handleRouteUpdate(update)
+		}
+	}
+}
 
-			// hash update somehow
-			isTrueRouteUpdate := c.handleUpdate(update)
+func (w *Watcher) handleRouteUpdate(update RoutesUpdate) {
+	log.Debugf("Received a new client network route update for [%v]", w.handler)
 
-			c.updateSerial = update.updateSerial
+	// hash update somehow
+	isTrueRouteUpdate := w.classifyUpdate(update)
 
-			if isTrueRouteUpdate {
-				log.Debug("Client network update contains different routes, recalculating routes")
-				err := c.recalculateRouteAndUpdatePeerAndSystem(reasonRouteUpdate)
-				if err != nil {
-					log.Errorf("Failed to recalculate routes for network [%v]: %v", c.handler, err)
-				}
-			} else {
-				log.Debug("Route update is not different, skipping route recalculation")
-			}
+	w.updateSerial = update.UpdateSerial
 
-			c.startPeersStatusChangeWatcher()
+	if isTrueRouteUpdate {
+		log.Debugf("client network update %v for [%v] contains different routes, recalculating routes", update.UpdateSerial, w.handler)
+		if err := w.recalculateRoutes(reasonRouteUpdate); err != nil {
+			log.Errorf("failed to recalculate routes for network [%v]: %v", w.handler, err)
 		}
+	} else {
+		log.Debugf("route update %v for [%v] is not different, skipping route recalculation", update.UpdateSerial, w.handler)
+	}
+
+	w.startNewPeerStatusWatchers()
+}
+
+// Stop stops the watcher and cleans up resources.
+func (w *Watcher) Stop() {
+	log.Debugf("Stopping watcher for network [%v]", w.handler)
+
+	w.cancel()
+
+	if w.currentChosen == nil {
+		return
+	}
+	if err := w.removeAllowedIPs(w.currentChosen, reasonShutdown); err != nil {
+		log.Errorf("Failed to remove routes for [%v]: %v", w.handler, err)
 	}
 }
 
-func handlerFromRoute(
+func HandlerFromRoute(
 	rt *route.Route,
 	routeRefCounter *refcounter.RouteRefCounter,
 	allowedIPsRefCounter *refcounter.AllowedIPsRefCounter,
diff --git a/client/internal/routemanager/dnsinterceptor/handler.go b/client/internal/routemanager/dnsinterceptor/handler.go
index 68d81d96..78d5e3b3 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)
@@ -264,7 +264,7 @@ func (d *DnsInterceptor) writeMsg(w dns.ResponseWriter, r *dns.Msg) error {
 				continue
 			}
 
-			prefix := netip.PrefixFrom(ip, ip.BitLen())
+			prefix := netip.PrefixFrom(ip.Unmap(), ip.BitLen())
 			newPrefixes = append(newPrefixes, prefix)
 		}
 
@@ -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/iface/iface_common.go b/client/internal/routemanager/iface/iface_common.go
index 9e1f8058..02d3f4c6 100644
--- a/client/internal/routemanager/iface/iface_common.go
+++ b/client/internal/routemanager/iface/iface_common.go
@@ -3,7 +3,6 @@ package iface
 import (
 	"net"
 
-	"github.com/netbirdio/netbird/client/iface/configurer"
 	"github.com/netbirdio/netbird/client/iface/device"
 	"github.com/netbirdio/netbird/client/iface/wgaddr"
 )
@@ -18,5 +17,4 @@ type wgIfaceBase interface {
 	IsUserspaceBind() bool
 	GetFilter() device.PacketFilter
 	GetDevice() *device.FilteredDevice
-	GetStats(peerKey string) (configurer.WGStats, error)
 }
diff --git a/client/internal/routemanager/manager.go b/client/internal/routemanager/manager.go
index ae0d1d22..8dbbb5f7 100644
--- a/client/internal/routemanager/manager.go
+++ b/client/internal/routemanager/manager.go
@@ -11,9 +11,11 @@ import (
 	"sync"
 	"time"
 
+	"github.com/hashicorp/go-multierror"
 	log "github.com/sirupsen/logrus"
 	"golang.org/x/exp/maps"
 
+	nberrors "github.com/netbirdio/netbird/client/errors"
 	firewall "github.com/netbirdio/netbird/client/firewall/manager"
 	"github.com/netbirdio/netbird/client/iface/configurer"
 	"github.com/netbirdio/netbird/client/iface/netstack"
@@ -21,9 +23,11 @@ import (
 	"github.com/netbirdio/netbird/client/internal/listener"
 	"github.com/netbirdio/netbird/client/internal/peer"
 	"github.com/netbirdio/netbird/client/internal/peerstore"
+	"github.com/netbirdio/netbird/client/internal/routemanager/client"
 	"github.com/netbirdio/netbird/client/internal/routemanager/iface"
 	"github.com/netbirdio/netbird/client/internal/routemanager/notifier"
 	"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
+	"github.com/netbirdio/netbird/client/internal/routemanager/server"
 	"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
 	"github.com/netbirdio/netbird/client/internal/routemanager/vars"
 	"github.com/netbirdio/netbird/client/internal/routeselector"
@@ -68,9 +72,9 @@ type DefaultManager struct {
 	ctx                  context.Context
 	stop                 context.CancelFunc
 	mux                  sync.Mutex
-	clientNetworks       map[route.HAUniqueID]*clientNetwork
+	clientNetworks       map[route.HAUniqueID]*client.Watcher
 	routeSelector        *routeselector.RouteSelector
-	serverRouter         *serverRouter
+	serverRouter         *server.Router
 	sysOps               *systemops.SysOps
 	statusRecorder       *peer.Status
 	relayMgr             *relayClient.Manager
@@ -88,6 +92,7 @@ type DefaultManager struct {
 	useNewDNSRoute      bool
 	disableClientRoutes bool
 	disableServerRoutes bool
+	activeRoutes        map[route.HAUniqueID]client.RouteHandler
 }
 
 func NewManager(config ManagerConfig) *DefaultManager {
@@ -99,7 +104,7 @@ func NewManager(config ManagerConfig) *DefaultManager {
 		ctx:                 mCTX,
 		stop:                cancel,
 		dnsRouteInterval:    config.DNSRouteInterval,
-		clientNetworks:      make(map[route.HAUniqueID]*clientNetwork),
+		clientNetworks:      make(map[route.HAUniqueID]*client.Watcher),
 		relayMgr:            config.RelayManager,
 		sysOps:              sysOps,
 		statusRecorder:      config.StatusRecorder,
@@ -111,6 +116,7 @@ func NewManager(config ManagerConfig) *DefaultManager {
 		peerStore:           config.PeerStore,
 		disableClientRoutes: config.DisableClientRoutes,
 		disableServerRoutes: config.DisableServerRoutes,
+		activeRoutes:        make(map[route.HAUniqueID]client.RouteHandler),
 	}
 
 	useNoop := netstack.IsEnabled() || config.DisableClientRoutes
@@ -226,7 +232,7 @@ func (m *DefaultManager) EnableServerRouter(firewall firewall.Manager) error {
 	}
 
 	var err error
-	m.serverRouter, err = newServerRouter(m.ctx, m.wgInterface, firewall, m.statusRecorder)
+	m.serverRouter, err = server.NewRouter(m.ctx, m.wgInterface, firewall, m.statusRecorder)
 	if err != nil {
 		return err
 	}
@@ -237,7 +243,7 @@ func (m *DefaultManager) EnableServerRouter(firewall firewall.Manager) error {
 func (m *DefaultManager) Stop(stateManager *statemanager.Manager) {
 	m.stop()
 	if m.serverRouter != nil {
-		m.serverRouter.cleanUp()
+		m.serverRouter.CleanUp()
 	}
 
 	if m.routeRefCounter != nil {
@@ -259,14 +265,60 @@ func (m *DefaultManager) Stop(stateManager *statemanager.Manager) {
 		}
 	}
 
-	m.ctx = nil
-
 	m.mux.Lock()
 	defer m.mux.Unlock()
 	m.clientRoutes = nil
 }
 
 // UpdateRoutes compares received routes with existing routes and removes, updates or adds them to the client and server maps
+func (m *DefaultManager) updateSystemRoutes(newRoutes route.HAMap) error {
+	toAdd := make(map[route.HAUniqueID]*route.Route)
+	toRemove := make(map[route.HAUniqueID]client.RouteHandler)
+
+	for id, routes := range newRoutes {
+		if len(routes) > 0 {
+			toAdd[id] = routes[0]
+		}
+	}
+
+	for id, activeHandler := range m.activeRoutes {
+		if _, exists := toAdd[id]; exists {
+			delete(toAdd, id)
+		} else {
+			toRemove[id] = activeHandler
+		}
+	}
+
+	var merr *multierror.Error
+	for id, handler := range toRemove {
+		if err := handler.RemoveRoute(); err != nil {
+			merr = multierror.Append(merr, fmt.Errorf("remove route %s: %w", handler.String(), err))
+		}
+		delete(m.activeRoutes, id)
+	}
+
+	for id, route := range toAdd {
+		handler := client.HandlerFromRoute(
+			route,
+			m.routeRefCounter,
+			m.allowedIPsRefCounter,
+			m.dnsRouteInterval,
+			m.statusRecorder,
+			m.wgInterface,
+			m.dnsServer,
+			m.peerStore,
+			m.useNewDNSRoute,
+		)
+		if err := handler.AddRoute(m.ctx); err != nil {
+			merr = multierror.Append(merr, fmt.Errorf("add route %s: %w", handler.String(), err))
+			continue
+		}
+		m.activeRoutes[id] = handler
+	}
+
+	return nberrors.FormatErrorOrNil(merr)
+}
+
 func (m *DefaultManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Route, useNewDNSRoute bool) error {
 	select {
 	case <-m.ctx.Done():
@@ -281,22 +333,28 @@ func (m *DefaultManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Ro
 
 	newServerRoutesMap, newClientRoutesIDMap := m.classifyRoutes(newRoutes)
 
+	var merr *multierror.Error
 	if !m.disableClientRoutes {
 		filteredClientRoutes := m.routeSelector.FilterSelected(newClientRoutesIDMap)
+
+		if err := m.updateSystemRoutes(filteredClientRoutes); err != nil {
+			merr = multierror.Append(merr, fmt.Errorf("update system routes: %w", err))
+		}
+
 		m.updateClientNetworks(updateSerial, filteredClientRoutes)
 		m.notifier.OnNewRoutes(filteredClientRoutes)
 	}
 	m.clientRoutes = newClientRoutesIDMap
 
 	if m.serverRouter == nil {
-		return nil
+		return nberrors.FormatErrorOrNil(merr)
 	}
 
-	if err := m.serverRouter.updateRoutes(newServerRoutesMap); err != nil {
-		return fmt.Errorf("update routes: %w", err)
+	if err := m.serverRouter.UpdateRoutes(newServerRoutesMap, useNewDNSRoute); err != nil {
+		merr = multierror.Append(merr, fmt.Errorf("update server routes: %w", err))
 	}
 
-	return nil
+	return nberrors.FormatErrorOrNil(merr)
 }
 
 // SetRouteChangeListener set RouteListener for route change Notifier
@@ -343,6 +401,10 @@ func (m *DefaultManager) TriggerSelection(networks route.HAMap) {
 
 	m.notifier.OnNewRoutes(networks)
 
+	if err := m.updateSystemRoutes(networks); err != nil {
+		log.Errorf("failed to update system routes during selection: %v", err)
+	}
+
 	m.stopObsoleteClients(networks)
 
 	for id, routes := range networks {
@@ -351,21 +413,24 @@ func (m *DefaultManager) TriggerSelection(networks route.HAMap) {
 			continue
 		}
 
-		clientNetworkWatcher := newClientNetworkWatcher(
-			m.ctx,
-			m.dnsRouteInterval,
-			m.wgInterface,
-			m.statusRecorder,
-			routes[0],
-			m.routeRefCounter,
-			m.allowedIPsRefCounter,
-			m.dnsServer,
-			m.peerStore,
-			m.useNewDNSRoute,
-		)
+		handler := m.activeRoutes[id]
+		if handler == nil {
+			log.Warnf("no active handler found for route %s", id)
+			continue
+		}
+
+		config := client.WatcherConfig{
+			Context:          m.ctx,
+			DNSRouteInterval: m.dnsRouteInterval,
+			WGInterface:      m.wgInterface,
+			StatusRecorder:   m.statusRecorder,
+			Route:            routes[0],
+			Handler:          handler,
+		}
+		clientNetworkWatcher := client.NewWatcher(config)
 		m.clientNetworks[id] = clientNetworkWatcher
-		go clientNetworkWatcher.peersStateAndUpdateWatcher()
-		clientNetworkWatcher.sendUpdateToClientNetworkWatcher(routesUpdate{routes: routes})
+		go clientNetworkWatcher.Start()
+		clientNetworkWatcher.SendUpdate(client.RoutesUpdate{Routes: routes})
 	}
 
 	if err := m.stateManager.UpdateState((*SelectorState)(m.routeSelector)); err != nil {
@@ -377,8 +442,7 @@ func (m *DefaultManager) TriggerSelection(networks route.HAMap) {
 func (m *DefaultManager) stopObsoleteClients(networks route.HAMap) {
 	for id, client := range m.clientNetworks {
 		if _, ok := networks[id]; !ok {
-			log.Debugf("Stopping client network watcher, %s", id)
-			client.cancel()
+			client.Stop()
 			delete(m.clientNetworks, id)
 		}
 	}
@@ -391,26 +455,29 @@ func (m *DefaultManager) updateClientNetworks(updateSerial uint64, networks rout
 	for id, routes := range networks {
 		clientNetworkWatcher, found := m.clientNetworks[id]
 		if !found {
-			clientNetworkWatcher = newClientNetworkWatcher(
-				m.ctx,
-				m.dnsRouteInterval,
-				m.wgInterface,
-				m.statusRecorder,
-				routes[0],
-				m.routeRefCounter,
-				m.allowedIPsRefCounter,
-				m.dnsServer,
-				m.peerStore,
-				m.useNewDNSRoute,
-			)
+			handler := m.activeRoutes[id]
+			if handler == nil {
+				log.Errorf("No active handler found for route %s", id)
+				continue
+			}
+
+			config := client.WatcherConfig{
+				Context:          m.ctx,
+				DNSRouteInterval: m.dnsRouteInterval,
+				WGInterface:      m.wgInterface,
+				StatusRecorder:   m.statusRecorder,
+				Route:            routes[0],
+				Handler:          handler,
+			}
+			clientNetworkWatcher = client.NewWatcher(config)
 			m.clientNetworks[id] = clientNetworkWatcher
-			go clientNetworkWatcher.peersStateAndUpdateWatcher()
+			go clientNetworkWatcher.Start()
 		}
-		update := routesUpdate{
-			updateSerial: updateSerial,
-			routes:       routes,
+		update := client.RoutesUpdate{
+			UpdateSerial: updateSerial,
+			Routes:       routes,
 		}
-		clientNetworkWatcher.sendUpdateToClientNetworkWatcher(update)
+		clientNetworkWatcher.SendUpdate(update)
 	}
 }
 
diff --git a/client/internal/routemanager/notifier/notifier.go b/client/internal/routemanager/notifier/notifier.go
index ebdd6032..25a3a71e 100644
--- a/client/internal/routemanager/notifier/notifier.go
+++ b/client/internal/routemanager/notifier/notifier.go
@@ -32,6 +32,10 @@ func (n *Notifier) SetListener(listener listener.NetworkChangeListener) {
 func (n *Notifier) SetInitialClientRoutes(clientRoutes []*route.Route) {
 	nets := make([]string, 0)
 	for _, r := range clientRoutes {
+		// filter out domain routes
+		if r.IsDynamic() {
+			continue
+		}
 		nets = append(nets, r.Network.String())
 	}
 	sort.Strings(nets)
diff --git a/client/internal/routemanager/server/server.go b/client/internal/routemanager/server/server.go
new file mode 100644
index 00000000..e674c80c
--- /dev/null
+++ b/client/internal/routemanager/server/server.go
@@ -0,0 +1,173 @@
+package server
+
+import (
+	"context"
+	"fmt"
+	"net/netip"
+	"sync"
+
+	log "github.com/sirupsen/logrus"
+
+	firewall "github.com/netbirdio/netbird/client/firewall/manager"
+	"github.com/netbirdio/netbird/client/internal/peer"
+	"github.com/netbirdio/netbird/client/internal/routemanager/iface"
+	"github.com/netbirdio/netbird/route"
+)
+
+type Router struct {
+	mux            sync.Mutex
+	ctx            context.Context
+	routes         map[route.ID]*route.Route
+	firewall       firewall.Manager
+	wgInterface    iface.WGIface
+	statusRecorder *peer.Status
+}
+
+func NewRouter(ctx context.Context, wgInterface iface.WGIface, firewall firewall.Manager, statusRecorder *peer.Status) (*Router, error) {
+	return &Router{
+		ctx:            ctx,
+		routes:         make(map[route.ID]*route.Route),
+		firewall:       firewall,
+		wgInterface:    wgInterface,
+		statusRecorder: statusRecorder,
+	}, nil
+}
+
+func (r *Router) UpdateRoutes(routesMap map[route.ID]*route.Route, useNewDNSRoute bool) error {
+	r.mux.Lock()
+	defer r.mux.Unlock()
+
+	serverRoutesToRemove := make([]route.ID, 0)
+
+	for routeID := range r.routes {
+		update, found := routesMap[routeID]
+		if !found || !update.Equal(r.routes[routeID]) {
+			serverRoutesToRemove = append(serverRoutesToRemove, routeID)
+		}
+	}
+
+	for _, routeID := range serverRoutesToRemove {
+		oldRoute := r.routes[routeID]
+		err := r.removeFromServerNetwork(oldRoute)
+		if err != nil {
+			log.Errorf("Unable to remove route id: %s, network %s, from server, got: %v",
+				oldRoute.ID, oldRoute.Network, err)
+		}
+		delete(r.routes, routeID)
+	}
+
+	// If routing is to be disabled, do it after routes have been removed
+	// If routing is to be enabled, do it before adding new routes; addToServerNetwork needs routing to be enabled
+	if len(routesMap) > 0 {
+		if err := r.firewall.EnableRouting(); err != nil {
+			return fmt.Errorf("enable routing: %w", err)
+		}
+	} else {
+		if err := r.firewall.DisableRouting(); err != nil {
+			return fmt.Errorf("disable routing: %w", err)
+		}
+	}
+
+	for id, newRoute := range routesMap {
+		_, found := r.routes[id]
+		if found {
+			continue
+		}
+
+		err := r.addToServerNetwork(newRoute, useNewDNSRoute)
+		if err != nil {
+			log.Errorf("Unable to add route %s from server, got: %v", newRoute.ID, err)
+			continue
+		}
+		r.routes[id] = newRoute
+	}
+
+	return nil
+}
+
+func (r *Router) removeFromServerNetwork(route *route.Route) error {
+	if r.ctx.Err() != nil {
+		log.Infof("Not removing from server network because context is done")
+		return r.ctx.Err()
+	}
+
+	routerPair := routeToRouterPair(route, false)
+	if err := r.firewall.RemoveNatRule(routerPair); err != nil {
+		return fmt.Errorf("remove routing rules: %w", err)
+	}
+
+	delete(r.routes, route.ID)
+	r.statusRecorder.RemoveLocalPeerStateRoute(route.NetString())
+
+	return nil
+}
+
+func (r *Router) addToServerNetwork(route *route.Route, useNewDNSRoute bool) error {
+	if r.ctx.Err() != nil {
+		log.Infof("Not adding to server network because context is done")
+		return r.ctx.Err()
+	}
+
+	routerPair := routeToRouterPair(route, useNewDNSRoute)
+	if err := r.firewall.AddNatRule(routerPair); err != nil {
+		return fmt.Errorf("insert routing rules: %w", err)
+	}
+
+	r.routes[route.ID] = route
+	r.statusRecorder.AddLocalPeerStateRoute(route.NetString(), route.GetResourceID())
+
+	return nil
+}
+
+func (r *Router) CleanUp() {
+	r.mux.Lock()
+	defer r.mux.Unlock()
+
+	for _, route := range r.routes {
+		routerPair := routeToRouterPair(route, false)
+		if err := r.firewall.RemoveNatRule(routerPair); err != nil {
+			log.Errorf("Failed to remove cleanup route: %v", err)
+		}
+	}
+
+	r.statusRecorder.CleanLocalPeerStateRoutes()
+}
+
+func (r *Router) RoutesCount() int {
+	r.mux.Lock()
+	defer r.mux.Unlock()
+	return len(r.routes)
+}
+
+func routeToRouterPair(route *route.Route, useNewDNSRoute bool) firewall.RouterPair {
+	source := getDefaultPrefix(route.Network)
+	destination := firewall.Network{}
+	if route.IsDynamic() {
+		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{
+		ID:          route.ID,
+		Source:      source,
+		Destination: destination,
+		Masquerade:  route.Masquerade,
+	}
+}
+
+func getDefaultPrefix(prefix netip.Prefix) firewall.Network {
+	if prefix.Addr().Is6() {
+		return firewall.Network{
+			Prefix: netip.PrefixFrom(netip.IPv6Unspecified(), 0),
+		}
+	}
+	return firewall.Network{
+		Prefix: netip.PrefixFrom(netip.IPv4Unspecified(), 0),
+	}
+}
diff --git a/client/internal/routemanager/server_android.go b/client/internal/routemanager/server_android.go
deleted file mode 100644
index 48bb0380..00000000
--- a/client/internal/routemanager/server_android.go
+++ /dev/null
@@ -1,27 +0,0 @@
-//go:build android
-
-package routemanager
-
-import (
-	"context"
-	"fmt"
-
-	firewall "github.com/netbirdio/netbird/client/firewall/manager"
-	"github.com/netbirdio/netbird/client/internal/peer"
-	"github.com/netbirdio/netbird/client/internal/routemanager/iface"
-	"github.com/netbirdio/netbird/route"
-)
-
-type serverRouter struct {
-}
-
-func (r serverRouter) cleanUp() {
-}
-
-func (r serverRouter) updateRoutes(map[route.ID]*route.Route) error {
-	return nil
-}
-
-func newServerRouter(context.Context, iface.WGIface, firewall.Manager, *peer.Status) (*serverRouter, error) {
-	return nil, fmt.Errorf("server route not supported on this os")
-}
diff --git a/client/internal/routemanager/server_nonandroid.go b/client/internal/routemanager/server_nonandroid.go
deleted file mode 100644
index 18713ee6..00000000
--- a/client/internal/routemanager/server_nonandroid.go
+++ /dev/null
@@ -1,191 +0,0 @@
-//go:build !android
-
-package routemanager
-
-import (
-	"context"
-	"fmt"
-	"net/netip"
-	"sync"
-
-	log "github.com/sirupsen/logrus"
-
-	firewall "github.com/netbirdio/netbird/client/firewall/manager"
-	"github.com/netbirdio/netbird/client/internal/peer"
-	"github.com/netbirdio/netbird/client/internal/routemanager/iface"
-	"github.com/netbirdio/netbird/route"
-)
-
-type serverRouter struct {
-	mux            sync.Mutex
-	ctx            context.Context
-	routes         map[route.ID]*route.Route
-	firewall       firewall.Manager
-	wgInterface    iface.WGIface
-	statusRecorder *peer.Status
-}
-
-func newServerRouter(ctx context.Context, wgInterface iface.WGIface, firewall firewall.Manager, statusRecorder *peer.Status) (*serverRouter, error) {
-	return &serverRouter{
-		ctx:            ctx,
-		routes:         make(map[route.ID]*route.Route),
-		firewall:       firewall,
-		wgInterface:    wgInterface,
-		statusRecorder: statusRecorder,
-	}, nil
-}
-
-func (m *serverRouter) updateRoutes(routesMap map[route.ID]*route.Route) error {
-	serverRoutesToRemove := make([]route.ID, 0)
-
-	for routeID := range m.routes {
-		update, found := routesMap[routeID]
-		if !found || !update.Equal(m.routes[routeID]) {
-			serverRoutesToRemove = append(serverRoutesToRemove, routeID)
-		}
-	}
-
-	for _, routeID := range serverRoutesToRemove {
-		oldRoute := m.routes[routeID]
-		err := m.removeFromServerNetwork(oldRoute)
-		if err != nil {
-			log.Errorf("Unable to remove route id: %s, network %s, from server, got: %v",
-				oldRoute.ID, oldRoute.Network, err)
-		}
-		delete(m.routes, routeID)
-	}
-
-	// If routing is to be disabled, do it after routes have been removed
-	// If routing is to be enabled, do it before adding new routes; addToServerNetwork needs routing to be enabled
-	if len(routesMap) > 0 {
-		if err := m.firewall.EnableRouting(); err != nil {
-			return fmt.Errorf("enable routing: %w", err)
-		}
-	} else {
-		if err := m.firewall.DisableRouting(); err != nil {
-			return fmt.Errorf("disable routing: %w", err)
-		}
-	}
-
-	for id, newRoute := range routesMap {
-		_, found := m.routes[id]
-		if found {
-			continue
-		}
-
-		err := m.addToServerNetwork(newRoute)
-		if err != nil {
-			log.Errorf("Unable to add route %s from server, got: %v", newRoute.ID, err)
-			continue
-		}
-		m.routes[id] = newRoute
-	}
-
-	return nil
-}
-
-func (m *serverRouter) removeFromServerNetwork(route *route.Route) error {
-	if m.ctx.Err() != nil {
-		log.Infof("Not removing from 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.RemoveNatRule(routerPair)
-	if 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)
-
-	return nil
-}
-
-func (m *serverRouter) addToServerNetwork(route *route.Route) 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 {
-		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())
-
-	return nil
-}
-
-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 {
-			log.Errorf("Failed to remove cleanup route: %v", err)
-		}
-
-	}
-
-	m.statusRecorder.CleanLocalPeerStateRoutes()
-}
-
-func routeToRouterPair(route *route.Route) (firewall.RouterPair, error) {
-	// TODO: add ipv6
-	source := getDefaultPrefix(route.Network)
-
-	destination := route.Network.Masked()
-	if route.IsDynamic() {
-		// TODO: add ipv6 additionally
-		destination = getDefaultPrefix(destination)
-	}
-
-	return firewall.RouterPair{
-		ID:          route.ID,
-		Source:      source,
-		Destination: destination,
-		Masquerade:  route.Masquerade,
-	}, nil
-}
-
-func getDefaultPrefix(prefix netip.Prefix) netip.Prefix {
-	if prefix.Addr().Is6() {
-		return netip.PrefixFrom(netip.IPv6Unspecified(), 0)
-	}
-	return netip.PrefixFrom(netip.IPv4Unspecified(), 0)
-}
diff --git a/client/internal/routemanager/static/route.go b/client/internal/routemanager/static/route.go
index 98c34dbe..c8b9338e 100644
--- a/client/internal/routemanager/static/route.go
+++ b/client/internal/routemanager/static/route.go
@@ -24,19 +24,22 @@ func NewRoute(rt *route.Route, routeRefCounter *refcounter.RouteRefCounter, allo
 	}
 }
 
-// Route route methods
 func (r *Route) String() string {
 	return r.route.Network.String()
 }
 
 func (r *Route) AddRoute(context.Context) error {
-	_, err := r.routeRefCounter.Increment(r.route.Network, struct{}{})
-	return err
+	if _, err := r.routeRefCounter.Increment(r.route.Network, struct{}{}); err != nil {
+		return err
+	}
+	return nil
 }
 
 func (r *Route) RemoveRoute() error {
-	_, err := r.routeRefCounter.Decrement(r.route.Network)
-	return err
+	if _, err := r.routeRefCounter.Decrement(r.route.Network); err != nil {
+		return err
+	}
+	return nil
 }
 
 func (r *Route) AddAllowedIPs(peerKey string) error {
@@ -52,6 +55,8 @@ func (r *Route) AddAllowedIPs(peerKey string) error {
 }
 
 func (r *Route) RemoveAllowedIPs() error {
-	_, err := r.allowedIPsRefcounter.Decrement(r.route.Network)
-	return err
+	if _, err := r.allowedIPsRefcounter.Decrement(r.route.Network); err != nil {
+		return err
+	}
+	return nil
 }
diff --git a/client/internal/routemanager/sysctl/sysctl_linux.go b/client/internal/routemanager/sysctl/sysctl_linux.go
index ea63f02f..f96a57f3 100644
--- a/client/internal/routemanager/sysctl/sysctl_linux.go
+++ b/client/internal/routemanager/sysctl/sysctl_linux.go
@@ -13,7 +13,7 @@ import (
 	log "github.com/sirupsen/logrus"
 
 	nberrors "github.com/netbirdio/netbird/client/errors"
-	"github.com/netbirdio/netbird/client/internal/routemanager/iface"
+	"github.com/netbirdio/netbird/client/iface/wgaddr"
 )
 
 const (
@@ -22,8 +22,13 @@ const (
 	srcValidMarkPath      = "net.ipv4.conf.all.src_valid_mark"
 )
 
+type iface interface {
+	Address() wgaddr.Address
+	Name() string
+}
+
 // Setup configures sysctl settings for RP filtering and source validation.
-func Setup(wgIface iface.WGIface) (map[string]int, error) {
+func Setup(wgIface iface) (map[string]int, error) {
 	keys := map[string]int{}
 	var result *multierror.Error
 
diff --git a/client/internal/routemanager/systemops/systemops.go b/client/internal/routemanager/systemops/systemops.go
index 5c117b94..261567dc 100644
--- a/client/internal/routemanager/systemops/systemops.go
+++ b/client/internal/routemanager/systemops/systemops.go
@@ -1,13 +1,15 @@
 package systemops
 
 import (
+	"fmt"
 	"net"
 	"net/netip"
 	"sync"
 
-	"github.com/netbirdio/netbird/client/internal/routemanager/iface"
+	"github.com/netbirdio/netbird/client/iface/wgaddr"
 	"github.com/netbirdio/netbird/client/internal/routemanager/notifier"
 	"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
+	"github.com/netbirdio/netbird/client/internal/routemanager/vars"
 )
 
 type Nexthop struct {
@@ -15,11 +17,30 @@ type Nexthop struct {
 	Intf *net.Interface
 }
 
+// Equal checks if two nexthops are equal.
+func (n Nexthop) Equal(other Nexthop) bool {
+	return n.IP == other.IP && (n.Intf == nil && other.Intf == nil ||
+		n.Intf != nil && other.Intf != nil && n.Intf.Index == other.Intf.Index)
+}
+
+// String returns a string representation of the nexthop.
+func (n Nexthop) String() string {
+	if n.Intf == nil {
+		return n.IP.String()
+	}
+	return fmt.Sprintf("%s @ %d (%s)", n.IP.String(), n.Intf.Index, n.Intf.Name)
+}
+
+type wgIface interface {
+	Address() wgaddr.Address
+	Name() string
+}
+
 type ExclusionCounter = refcounter.Counter[netip.Prefix, struct{}, Nexthop]
 
 type SysOps struct {
 	refCounter  *ExclusionCounter
-	wgInterface iface.WGIface
+	wgInterface wgIface
 	// prefixes is tracking all the current added prefixes im memory
 	// (this is used in iOS as all route updates require a full table update)
 	//nolint
@@ -30,9 +51,27 @@ type SysOps struct {
 	notifier *notifier.Notifier
 }
 
-func NewSysOps(wgInterface iface.WGIface, notifier *notifier.Notifier) *SysOps {
+func NewSysOps(wgInterface wgIface, notifier *notifier.Notifier) *SysOps {
 	return &SysOps{
 		wgInterface: wgInterface,
 		notifier:    notifier,
 	}
 }
+
+func (r *SysOps) validateRoute(prefix netip.Prefix) error {
+	addr := prefix.Addr()
+
+	switch {
+	case
+		!addr.IsValid(),
+		addr.IsLoopback(),
+		addr.IsLinkLocalUnicast(),
+		addr.IsLinkLocalMulticast(),
+		addr.IsInterfaceLocalMulticast(),
+		addr.IsMulticast(),
+		addr.IsUnspecified() && prefix.Bits() != 0,
+		r.wgInterface.Address().Network.Contains(addr):
+		return vars.ErrRouteNotAllowed
+	}
+	return nil
+}
diff --git a/client/internal/routemanager/systemops/systemops_generic.go b/client/internal/routemanager/systemops/systemops_generic.go
index eaef0181..d223a27b 100644
--- a/client/internal/routemanager/systemops/systemops_generic.go
+++ b/client/internal/routemanager/systemops/systemops_generic.go
@@ -17,7 +17,6 @@ import (
 
 	nberrors "github.com/netbirdio/netbird/client/errors"
 	"github.com/netbirdio/netbird/client/iface/netstack"
-	"github.com/netbirdio/netbird/client/internal/routemanager/iface"
 	"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
 	"github.com/netbirdio/netbird/client/internal/routemanager/util"
 	"github.com/netbirdio/netbird/client/internal/routemanager/vars"
@@ -106,59 +105,15 @@ func (r *SysOps) cleanupRefCounter(stateManager *statemanager.Manager) error {
 	return nil
 }
 
-// TODO: fix: for default our wg address now appears as the default gw
-func (r *SysOps) addRouteForCurrentDefaultGateway(prefix netip.Prefix) error {
-	addr := netip.IPv4Unspecified()
-	if prefix.Addr().Is6() {
-		addr = netip.IPv6Unspecified()
-	}
-
-	nexthop, err := GetNextHop(addr)
-	if err != nil && !errors.Is(err, vars.ErrRouteNotFound) {
-		return fmt.Errorf("get existing route gateway: %s", err)
-	}
-
-	if !prefix.Contains(nexthop.IP) {
-		log.Debugf("Skipping adding a new route for gateway %s because it is not in the network %s", nexthop.IP, prefix)
-		return nil
-	}
-
-	gatewayPrefix := netip.PrefixFrom(nexthop.IP, 32)
-	if nexthop.IP.Is6() {
-		gatewayPrefix = netip.PrefixFrom(nexthop.IP, 128)
-	}
-
-	ok, err := existsInRouteTable(gatewayPrefix)
-	if err != nil {
-		return fmt.Errorf("unable to check if there is an existing route for gateway %s. error: %s", gatewayPrefix, err)
-	}
-
-	if ok {
-		log.Debugf("Skipping adding a new route for gateway %s because it already exists", gatewayPrefix)
-		return nil
-	}
-
-	nexthop, err = GetNextHop(nexthop.IP)
-	if err != nil && !errors.Is(err, vars.ErrRouteNotFound) {
-		return fmt.Errorf("unable to get the next hop for the default gateway address. error: %s", err)
-	}
-
-	log.Debugf("Adding a new route for gateway %s with next hop %s", gatewayPrefix, nexthop.IP)
-	return r.addToRouteTable(gatewayPrefix, nexthop)
-}
-
 // addRouteToNonVPNIntf adds a new route to the routing table for the given prefix and returns the next hop and interface.
 // If the next hop or interface is pointing to the VPN interface, it will return the initial values.
-func (r *SysOps) addRouteToNonVPNIntf(prefix netip.Prefix, vpnIntf iface.WGIface, initialNextHop Nexthop) (Nexthop, error) {
-	addr := prefix.Addr()
-	switch {
-	case addr.IsLoopback(),
-		addr.IsLinkLocalUnicast(),
-		addr.IsLinkLocalMulticast(),
-		addr.IsInterfaceLocalMulticast(),
-		addr.IsUnspecified(),
-		addr.IsMulticast():
+func (r *SysOps) addRouteToNonVPNIntf(prefix netip.Prefix, vpnIntf wgIface, initialNextHop Nexthop) (Nexthop, error) {
+	if err := r.validateRoute(prefix); err != nil {
+		return Nexthop{}, err
+	}
 
+	addr := prefix.Addr()
+	if addr.IsUnspecified() {
 		return Nexthop{}, vars.ErrRouteNotAllowed
 	}
 
@@ -179,10 +134,7 @@ func (r *SysOps) addRouteToNonVPNIntf(prefix netip.Prefix, vpnIntf iface.WGIface
 		Intf: nexthop.Intf,
 	}
 
-	vpnAddr, ok := netip.AddrFromSlice(vpnIntf.Address().IP)
-	if !ok {
-		return Nexthop{}, fmt.Errorf("failed to convert vpn address to netip.Addr")
-	}
+	vpnAddr := vpnIntf.Address().IP
 
 	// if next hop is the VPN address or the interface is the VPN interface, we should use the initial values
 	if exitNextHop.IP == vpnAddr || exitNextHop.Intf != nil && exitNextHop.Intf.Name == vpnIntf.Name() {
@@ -271,32 +223,7 @@ func (r *SysOps) genericAddVPNRoute(prefix netip.Prefix, intf *net.Interface) er
 		return nil
 	}
 
-	return r.addNonExistingRoute(prefix, intf)
-}
-
-// addNonExistingRoute adds a new route to the vpn interface if it doesn't exist in the current routing table
-func (r *SysOps) addNonExistingRoute(prefix netip.Prefix, intf *net.Interface) error {
-	ok, err := existsInRouteTable(prefix)
-	if err != nil {
-		return fmt.Errorf("exists in route table: %w", err)
-	}
-	if ok {
-		log.Warnf("Skipping adding a new route for network %s because it already exists", prefix)
-		return nil
-	}
-
-	ok, err = isSubRange(prefix)
-	if err != nil {
-		return fmt.Errorf("sub range: %w", err)
-	}
-
-	if ok {
-		if err := r.addRouteForCurrentDefaultGateway(prefix); err != nil {
-			log.Warnf("Unable to add route for current default gateway route. Will proceed without it. error: %s", err)
-		}
-	}
-
-	return r.addToRouteTable(prefix, Nexthop{netip.Addr{}, intf})
+	return r.addToRouteTable(prefix, nextHop)
 }
 
 // genericRemoveVPNRoute removes the route from the vpn interface. If a default prefix is given,
@@ -408,12 +335,8 @@ func GetNextHop(ip netip.Addr) (Nexthop, error) {
 
 	log.Debugf("Route for %s: interface %v nexthop %v, preferred source %v", ip, intf, gateway, preferredSrc)
 	if gateway == nil {
-		if runtime.GOOS == "freebsd" {
-			return Nexthop{Intf: intf}, nil
-		}
-
 		if preferredSrc == nil {
-			return Nexthop{}, vars.ErrRouteNotFound
+			return Nexthop{Intf: intf}, nil
 		}
 		log.Debugf("No next hop found for IP %s, using preferred source %s", ip, preferredSrc)
 
@@ -457,32 +380,6 @@ func ipToAddr(ip net.IP, intf *net.Interface) (netip.Addr, error) {
 	return addr.Unmap(), nil
 }
 
-func existsInRouteTable(prefix netip.Prefix) (bool, error) {
-	routes, err := GetRoutesFromTable()
-	if err != nil {
-		return false, fmt.Errorf("get routes from table: %w", err)
-	}
-	for _, tableRoute := range routes {
-		if tableRoute == prefix {
-			return true, nil
-		}
-	}
-	return false, nil
-}
-
-func isSubRange(prefix netip.Prefix) (bool, error) {
-	routes, err := GetRoutesFromTable()
-	if err != nil {
-		return false, fmt.Errorf("get routes from table: %w", err)
-	}
-	for _, tableRoute := range routes {
-		if tableRoute.Bits() > vars.MinRangeBits && tableRoute.Contains(prefix.Addr()) && tableRoute.Bits() < prefix.Bits() {
-			return true, nil
-		}
-	}
-	return false, nil
-}
-
 // IsAddrRouted checks if the candidate address would route to the vpn, in which case it returns true and the matched prefix.
 func IsAddrRouted(addr netip.Addr, vpnRoutes []netip.Prefix) (bool, netip.Prefix) {
 	localRoutes, err := hasSeparateRouting()
diff --git a/client/internal/routemanager/systemops/systemops_linux.go b/client/internal/routemanager/systemops/systemops_linux.go
index cf3c2f0a..b48cfa24 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"},
 	}
@@ -149,6 +149,10 @@ func (r *SysOps) removeFromRouteTable(prefix netip.Prefix, nexthop Nexthop) erro
 }
 
 func (r *SysOps) AddVPNRoute(prefix netip.Prefix, intf *net.Interface) error {
+	if err := r.validateRoute(prefix); err != nil {
+		return err
+	}
+
 	if !nbnet.AdvancedRouting() {
 		return r.genericAddVPNRoute(prefix, intf)
 	}
@@ -172,6 +176,10 @@ func (r *SysOps) AddVPNRoute(prefix netip.Prefix, intf *net.Interface) error {
 }
 
 func (r *SysOps) RemoveVPNRoute(prefix netip.Prefix, intf *net.Interface) error {
+	if err := r.validateRoute(prefix); err != nil {
+		return err
+	}
+
 	if !nbnet.AdvancedRouting() {
 		return r.genericRemoveVPNRoute(prefix, intf)
 	}
@@ -219,7 +227,7 @@ func getRoutes(tableID, family int) ([]netip.Prefix, error) {
 
 			ones, _ := route.Dst.Mask.Size()
 
-			prefix := netip.PrefixFrom(addr, ones)
+			prefix := netip.PrefixFrom(addr.Unmap(), ones)
 			if prefix.IsValid() {
 				prefixList = append(prefixList, prefix)
 			}
@@ -247,7 +255,7 @@ func addRoute(prefix netip.Prefix, nexthop Nexthop, tableID int) error {
 		return fmt.Errorf("add gateway and device: %w", err)
 	}
 
-	if err := netlink.RouteAdd(route); err != nil && !errors.Is(err, syscall.EEXIST) && !isOpErr(err) {
+	if err := netlink.RouteAdd(route); err != nil && !isOpErr(err) {
 		return fmt.Errorf("netlink add route: %w", err)
 	}
 
@@ -270,7 +278,7 @@ func addUnreachableRoute(prefix netip.Prefix, tableID int) error {
 		Dst:    ipNet,
 	}
 
-	if err := netlink.RouteAdd(route); err != nil && !errors.Is(err, syscall.EEXIST) && !isOpErr(err) {
+	if err := netlink.RouteAdd(route); err != nil && !isOpErr(err) {
 		return fmt.Errorf("netlink add unreachable route: %w", err)
 	}
 
diff --git a/client/internal/routemanager/systemops/systemops_nonlinux.go b/client/internal/routemanager/systemops/systemops_nonlinux.go
index 3b52fc7a..59581255 100644
--- a/client/internal/routemanager/systemops/systemops_nonlinux.go
+++ b/client/internal/routemanager/systemops/systemops_nonlinux.go
@@ -11,10 +11,16 @@ import (
 )
 
 func (r *SysOps) AddVPNRoute(prefix netip.Prefix, intf *net.Interface) error {
+	if err := r.validateRoute(prefix); err != nil {
+		return err
+	}
 	return r.genericAddVPNRoute(prefix, intf)
 }
 
 func (r *SysOps) RemoveVPNRoute(prefix netip.Prefix, intf *net.Interface) error {
+	if err := r.validateRoute(prefix); err != nil {
+		return err
+	}
 	return r.genericRemoveVPNRoute(prefix, intf)
 }
 
diff --git a/client/internal/routemanager/systemops/systemops_unix.go b/client/internal/routemanager/systemops/systemops_unix.go
index 0f8f2a34..f284e131 100644
--- a/client/internal/routemanager/systemops/systemops_unix.go
+++ b/client/internal/routemanager/systemops/systemops_unix.go
@@ -3,15 +3,19 @@
 package systemops
 
 import (
+	"errors"
 	"fmt"
 	"net"
 	"net/netip"
-	"os/exec"
-	"strings"
+	"strconv"
+	"syscall"
 	"time"
+	"unsafe"
 
 	"github.com/cenkalti/backoff/v4"
 	log "github.com/sirupsen/logrus"
+	"golang.org/x/net/route"
+	"golang.org/x/sys/unix"
 
 	"github.com/netbirdio/netbird/client/internal/statemanager"
 	nbnet "github.com/netbirdio/netbird/util/net"
@@ -26,58 +30,174 @@ func (r *SysOps) CleanupRouting(stateManager *statemanager.Manager) error {
 }
 
 func (r *SysOps) addToRouteTable(prefix netip.Prefix, nexthop Nexthop) error {
-	return r.routeCmd("add", prefix, nexthop)
+	return r.routeSocket(unix.RTM_ADD, prefix, nexthop)
 }
 
 func (r *SysOps) removeFromRouteTable(prefix netip.Prefix, nexthop Nexthop) error {
-	return r.routeCmd("delete", prefix, nexthop)
+	return r.routeSocket(unix.RTM_DELETE, prefix, nexthop)
 }
 
-func (r *SysOps) routeCmd(action string, prefix netip.Prefix, nexthop Nexthop) error {
-	inet := "-inet"
-	if prefix.Addr().Is6() {
-		inet = "-inet6"
+func (r *SysOps) routeSocket(action int, prefix netip.Prefix, nexthop Nexthop) error {
+	if !prefix.IsValid() {
+		return fmt.Errorf("invalid prefix: %s", prefix)
+	}
+
+	expBackOff := backoff.NewExponentialBackOff()
+	expBackOff.InitialInterval = 50 * time.Millisecond
+	expBackOff.MaxInterval = 500 * time.Millisecond
+	expBackOff.MaxElapsedTime = 1 * time.Second
+
+	if err := backoff.Retry(r.routeOp(action, prefix, nexthop), expBackOff); err != nil {
+		a := "add"
+		if action == unix.RTM_DELETE {
+			a = "remove"
+		}
+		return fmt.Errorf("%s route for %s: %w", a, prefix, err)
+	}
+	return nil
+}
+
+func (r *SysOps) routeOp(action int, prefix netip.Prefix, nexthop Nexthop) func() error {
+	operation := func() error {
+		fd, err := unix.Socket(syscall.AF_ROUTE, syscall.SOCK_RAW, syscall.AF_UNSPEC)
+		if err != nil {
+			return fmt.Errorf("open routing socket: %w", err)
+		}
+		defer func() {
+			if err := unix.Close(fd); err != nil && !errors.Is(err, unix.EBADF) {
+				log.Warnf("failed to close routing socket: %v", err)
+			}
+		}()
+
+		msg, err := r.buildRouteMessage(action, prefix, nexthop)
+		if err != nil {
+			return backoff.Permanent(fmt.Errorf("build route message: %w", err))
+		}
+
+		msgBytes, err := msg.Marshal()
+		if err != nil {
+			return backoff.Permanent(fmt.Errorf("marshal route message: %w", err))
+		}
+
+		if _, err = unix.Write(fd, msgBytes); err != nil {
+			if errors.Is(err, unix.ENOBUFS) || errors.Is(err, unix.EAGAIN) {
+				return fmt.Errorf("write: %w", err)
+			}
+			return backoff.Permanent(fmt.Errorf("write: %w", err))
+		}
+
+		respBuf := make([]byte, 2048)
+		n, err := unix.Read(fd, respBuf)
+		if err != nil {
+			return backoff.Permanent(fmt.Errorf("read route response: %w", err))
+		}
+
+		if n > 0 {
+			if err := r.parseRouteResponse(respBuf[:n]); err != nil {
+				return backoff.Permanent(err)
+			}
+		}
+
+		return nil
+	}
+	return operation
+}
+
+func (r *SysOps) buildRouteMessage(action int, prefix netip.Prefix, nexthop Nexthop) (msg *route.RouteMessage, err error) {
+	msg = &route.RouteMessage{
+		Type:    action,
+		Flags:   unix.RTF_UP,
+		Version: unix.RTM_VERSION,
+		Seq:     1,
+	}
+
+	const numAddrs = unix.RTAX_NETMASK + 1
+	addrs := make([]route.Addr, numAddrs)
+
+	addrs[unix.RTAX_DST], err = addrToRouteAddr(prefix.Addr())
+	if err != nil {
+		return nil, fmt.Errorf("build destination address for %s: %w", prefix.Addr(), err)
 	}
 
-	network := prefix.String()
 	if prefix.IsSingleIP() {
-		network = prefix.Addr().String()
+		msg.Flags |= unix.RTF_HOST
+	} else {
+		addrs[unix.RTAX_NETMASK], err = prefixToRouteNetmask(prefix)
+		if err != nil {
+			return nil, fmt.Errorf("build netmask for %s: %w", prefix, err)
+		}
 	}
 
-	args := []string{"-n", action, inet, network}
 	if nexthop.IP.IsValid() {
-		args = append(args, nexthop.IP.Unmap().String())
+		msg.Flags |= unix.RTF_GATEWAY
+		addrs[unix.RTAX_GATEWAY], err = addrToRouteAddr(nexthop.IP.Unmap())
+		if err != nil {
+			return nil, fmt.Errorf("build gateway IP address for %s: %w", nexthop.IP, err)
+		}
 	} else if nexthop.Intf != nil {
-		args = append(args, "-interface", nexthop.Intf.Name)
+		msg.Index = nexthop.Intf.Index
+		addrs[unix.RTAX_GATEWAY] = &route.LinkAddr{
+			Index: nexthop.Intf.Index,
+			Name:  nexthop.Intf.Name,
+		}
 	}
 
-	if err := retryRouteCmd(args); err != nil {
-		return fmt.Errorf("failed to %s route for %s: %w", action, prefix, err)
+	msg.Addrs = addrs
+	return msg, nil
+}
+
+func (r *SysOps) parseRouteResponse(buf []byte) error {
+	if len(buf) < int(unsafe.Sizeof(unix.RtMsghdr{})) {
+		return nil
+	}
+
+	rtMsg := (*unix.RtMsghdr)(unsafe.Pointer(&buf[0]))
+	if rtMsg.Errno != 0 {
+		return fmt.Errorf("parse: %d", rtMsg.Errno)
 	}
+
 	return nil
 }
 
-func retryRouteCmd(args []string) error {
-	operation := func() error {
-		out, err := exec.Command("route", args...).CombinedOutput()
-		log.Tracef("route %s: %s", strings.Join(args, " "), out)
-		// https://github.com/golang/go/issues/45736
-		if err != nil && strings.Contains(string(out), "sysctl: cannot allocate memory") {
-			return err
-		} else if err != nil {
-			return backoff.Permanent(err)
+// addrToRouteAddr converts a netip.Addr to the appropriate route.Addr (*route.Inet4Addr or *route.Inet6Addr).
+func addrToRouteAddr(addr netip.Addr) (route.Addr, error) {
+	if addr.Is4() {
+		return &route.Inet4Addr{IP: addr.As4()}, nil
+	}
+
+	if addr.Zone() == "" {
+		return &route.Inet6Addr{IP: addr.As16()}, nil
+	}
+
+	var zone int
+	// zone can be either a numeric zone ID or an interface name.
+	if z, err := strconv.Atoi(addr.Zone()); err == nil {
+		zone = z
+	} else {
+		iface, err := net.InterfaceByName(addr.Zone())
+		if err != nil {
+			return nil, fmt.Errorf("resolve zone '%s': %w", addr.Zone(), err)
 		}
-		return nil
+		zone = iface.Index
 	}
+	return &route.Inet6Addr{IP: addr.As16(), ZoneID: zone}, nil
+}
 
-	expBackOff := backoff.NewExponentialBackOff()
-	expBackOff.InitialInterval = 50 * time.Millisecond
-	expBackOff.MaxInterval = 500 * time.Millisecond
-	expBackOff.MaxElapsedTime = 1 * time.Second
+func prefixToRouteNetmask(prefix netip.Prefix) (route.Addr, error) {
+	bits := prefix.Bits()
+	if prefix.Addr().Is4() {
+		m := net.CIDRMask(bits, 32)
+		var maskBytes [4]byte
+		copy(maskBytes[:], m)
+		return &route.Inet4Addr{IP: maskBytes}, nil
+	}
 
-	err := backoff.Retry(operation, expBackOff)
-	if err != nil {
-		return fmt.Errorf("route cmd retry failed: %w", err)
+	if prefix.Addr().Is6() {
+		m := net.CIDRMask(bits, 128)
+		var maskBytes [16]byte
+		copy(maskBytes[:], m)
+		return &route.Inet6Addr{IP: maskBytes}, nil
 	}
-	return nil
+
+	return nil, fmt.Errorf("unknown IP version in prefix: %s", prefix.Addr().String())
 }
diff --git a/client/internal/routemanager/systemops/systemops_windows.go b/client/internal/routemanager/systemops/systemops_windows.go
index ad325e12..11eaa435 100644
--- a/client/internal/routemanager/systemops/systemops_windows.go
+++ b/client/internal/routemanager/systemops/systemops_windows.go
@@ -1,5 +1,3 @@
-//go:build windows
-
 package systemops
 
 import (
@@ -9,9 +7,8 @@ import (
 	"net"
 	"net/netip"
 	"os"
-	"os/exec"
+	"runtime/debug"
 	"strconv"
-	"strings"
 	"sync"
 	"syscall"
 	"time"
@@ -21,11 +18,12 @@ import (
 	"github.com/yusufpapurcu/wmi"
 	"golang.org/x/sys/windows"
 
-	"github.com/netbirdio/netbird/client/firewall/uspfilter"
 	"github.com/netbirdio/netbird/client/internal/statemanager"
 	nbnet "github.com/netbirdio/netbird/util/net"
 )
 
+const InfiniteLifetime = 0xffffffff
+
 type RouteUpdateType int
 
 // RouteUpdate represents a change in the routing table.
@@ -33,8 +31,7 @@ type RouteUpdateType int
 type RouteUpdate struct {
 	Type        RouteUpdateType
 	Destination netip.Prefix
-	NextHop     netip.Addr
-	Interface   *net.Interface
+	NextHop     Nexthop
 }
 
 // RouteMonitor provides a way to monitor changes in the routing table.
@@ -59,9 +56,13 @@ type MSFT_NetRoute struct {
 	AddressFamily     uint16
 }
 
-// MIB_IPFORWARD_ROW2 is defined in https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ns-netioapi-mib_ipforward_row2
+// luid represents a locally unique identifier for network interfaces
+type luid uint64
+
+// MIB_IPFORWARD_ROW2 represents a route entry in the routing table.
+// It is defined in https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ns-netioapi-mib_ipforward_row2
 type MIB_IPFORWARD_ROW2 struct {
-	InterfaceLuid        uint64
+	InterfaceLuid        luid
 	InterfaceIndex       uint32
 	DestinationPrefix    IP_ADDRESS_PREFIX
 	NextHop              SOCKADDR_INET_NEXTHOP
@@ -109,9 +110,14 @@ type SOCKADDR_INET_NEXTHOP struct {
 type MIB_NOTIFICATION_TYPE int32
 
 var (
-	modiphlpapi                = windows.NewLazyDLL("iphlpapi.dll")
-	procNotifyRouteChange2     = modiphlpapi.NewProc("NotifyRouteChange2")
-	procCancelMibChangeNotify2 = modiphlpapi.NewProc("CancelMibChangeNotify2")
+	modiphlpapi                     = windows.NewLazyDLL("iphlpapi.dll")
+	procNotifyRouteChange2          = modiphlpapi.NewProc("NotifyRouteChange2")
+	procCancelMibChangeNotify2      = modiphlpapi.NewProc("CancelMibChangeNotify2")
+	procCreateIpForwardEntry2       = modiphlpapi.NewProc("CreateIpForwardEntry2")
+	procDeleteIpForwardEntry2       = modiphlpapi.NewProc("DeleteIpForwardEntry2")
+	procGetIpForwardEntry2          = modiphlpapi.NewProc("GetIpForwardEntry2")
+	procInitializeIpForwardEntry    = modiphlpapi.NewProc("InitializeIpForwardEntry")
+	procConvertInterfaceIndexToLuid = modiphlpapi.NewProc("ConvertInterfaceIndexToLuid")
 
 	prefixList []netip.Prefix
 	lastUpdate time.Time
@@ -140,6 +146,8 @@ func (r *SysOps) CleanupRouting(stateManager *statemanager.Manager) error {
 }
 
 func (r *SysOps) addToRouteTable(prefix netip.Prefix, nexthop Nexthop) error {
+	log.Debugf("Adding route to %s via %s", prefix, nexthop)
+	// if we don't have an interface but a zone, extract the interface index from the zone
 	if nexthop.IP.Zone() != "" && nexthop.Intf == nil {
 		zone, err := strconv.Atoi(nexthop.IP.Zone())
 		if err != nil {
@@ -148,23 +156,187 @@ func (r *SysOps) addToRouteTable(prefix netip.Prefix, nexthop Nexthop) error {
 		nexthop.Intf = &net.Interface{Index: zone}
 	}
 
-	return addRouteCmd(prefix, nexthop)
+	return addRoute(prefix, nexthop)
 }
 
 func (r *SysOps) removeFromRouteTable(prefix netip.Prefix, nexthop Nexthop) error {
-	args := []string{"delete", prefix.String()}
+	log.Debugf("Removing route to %s via %s", prefix, nexthop)
+	return deleteRoute(prefix, nexthop)
+}
+
+// setupRouteEntry prepares a route entry with common configuration
+func setupRouteEntry(prefix netip.Prefix, nexthop Nexthop) (*MIB_IPFORWARD_ROW2, error) {
+	route := &MIB_IPFORWARD_ROW2{}
+
+	initializeIPForwardEntry(route)
+
+	// Convert interface index to luid if interface is specified
+	if nexthop.Intf != nil {
+		var luid luid
+		if err := convertInterfaceIndexToLUID(uint32(nexthop.Intf.Index), &luid); err != nil {
+			return nil, fmt.Errorf("convert interface index to luid: %w", err)
+		}
+		route.InterfaceLuid = luid
+		route.InterfaceIndex = uint32(nexthop.Intf.Index)
+	}
+
+	if err := setDestinationPrefix(&route.DestinationPrefix, prefix); err != nil {
+		return nil, fmt.Errorf("set destination prefix: %w", err)
+	}
+
 	if nexthop.IP.IsValid() {
-		ip := nexthop.IP.WithZone("")
-		args = append(args, ip.Unmap().String())
+		if err := setNextHop(&route.NextHop, nexthop.IP); err != nil {
+			return nil, fmt.Errorf("set next hop: %w", err)
+		}
 	}
 
-	routeCmd := uspfilter.GetSystem32Command("route")
+	return route, nil
+}
 
-	out, err := exec.Command(routeCmd, args...).CombinedOutput()
-	log.Tracef("route %s: %s", strings.Join(args, " "), out)
+// addRoute adds a route using Windows iphelper APIs
+func addRoute(prefix netip.Prefix, nexthop Nexthop) (err error) {
+	defer func() {
+		if r := recover(); r != nil {
+			err = fmt.Errorf("panic in addRoute: %v, stack trace: %s", r, debug.Stack())
+		}
+	}()
 
-	if err != nil {
-		return fmt.Errorf("remove route: %w", err)
+	route, setupErr := setupRouteEntry(prefix, nexthop)
+	if setupErr != nil {
+		return fmt.Errorf("setup route entry: %w", setupErr)
+	}
+
+	route.Metric = 1
+	route.ValidLifetime = InfiniteLifetime
+	route.PreferredLifetime = InfiniteLifetime
+
+	return createIPForwardEntry2(route)
+}
+
+// deleteRoute deletes a route using Windows iphelper APIs
+func deleteRoute(prefix netip.Prefix, nexthop Nexthop) (err error) {
+	defer func() {
+		if r := recover(); r != nil {
+			err = fmt.Errorf("panic in deleteRoute: %v, stack trace: %s", r, debug.Stack())
+		}
+	}()
+
+	route, setupErr := setupRouteEntry(prefix, nexthop)
+	if setupErr != nil {
+		return fmt.Errorf("setup route entry: %w", setupErr)
+	}
+
+	if err := getIPForwardEntry2(route); err != nil {
+		return fmt.Errorf("get route entry: %w", err)
+	}
+
+	return deleteIPForwardEntry2(route)
+}
+
+// setDestinationPrefix sets the destination prefix in the route structure
+func setDestinationPrefix(prefix *IP_ADDRESS_PREFIX, dest netip.Prefix) error {
+	addr := dest.Addr()
+	prefix.PrefixLength = uint8(dest.Bits())
+
+	if addr.Is4() {
+		prefix.Prefix.sin6_family = windows.AF_INET
+		ip4 := addr.As4()
+		binary.BigEndian.PutUint32(prefix.Prefix.data[:4],
+			uint32(ip4[0])<<24|uint32(ip4[1])<<16|uint32(ip4[2])<<8|uint32(ip4[3]))
+		return nil
+	}
+
+	if addr.Is6() {
+		prefix.Prefix.sin6_family = windows.AF_INET6
+		ip6 := addr.As16()
+		copy(prefix.Prefix.data[4:20], ip6[:])
+
+		if zone := addr.Zone(); zone != "" {
+			if scopeID, err := strconv.ParseUint(zone, 10, 32); err == nil {
+				binary.BigEndian.PutUint32(prefix.Prefix.data[20:24], uint32(scopeID))
+			}
+		}
+		return nil
+	}
+
+	return fmt.Errorf("invalid address family")
+}
+
+// setNextHop sets the next hop address in the route structure
+func setNextHop(nextHop *SOCKADDR_INET_NEXTHOP, addr netip.Addr) error {
+	if addr.Is4() {
+		nextHop.sin6_family = windows.AF_INET
+		ip4 := addr.As4()
+		binary.BigEndian.PutUint32(nextHop.data[:4],
+			uint32(ip4[0])<<24|uint32(ip4[1])<<16|uint32(ip4[2])<<8|uint32(ip4[3]))
+		return nil
+	}
+
+	if addr.Is6() {
+		nextHop.sin6_family = windows.AF_INET6
+		ip6 := addr.As16()
+		copy(nextHop.data[4:20], ip6[:])
+
+		// Handle zone if present
+		if zone := addr.Zone(); zone != "" {
+			if scopeID, err := strconv.ParseUint(zone, 10, 32); err == nil {
+				binary.BigEndian.PutUint32(nextHop.data[20:24], uint32(scopeID))
+			}
+		}
+		return nil
+	}
+
+	return fmt.Errorf("invalid address family")
+}
+
+// Windows API wrappers
+func createIPForwardEntry2(route *MIB_IPFORWARD_ROW2) error {
+	r1, _, e1 := syscall.SyscallN(procCreateIpForwardEntry2.Addr(), uintptr(unsafe.Pointer(route)))
+	if r1 != 0 {
+		if e1 != 0 {
+			return fmt.Errorf("CreateIpForwardEntry2: %w", e1)
+		}
+		return fmt.Errorf("CreateIpForwardEntry2: code %d", r1)
+	}
+	return nil
+}
+
+func deleteIPForwardEntry2(route *MIB_IPFORWARD_ROW2) error {
+	r1, _, e1 := syscall.SyscallN(procDeleteIpForwardEntry2.Addr(), uintptr(unsafe.Pointer(route)))
+	if r1 != 0 {
+		if e1 != 0 {
+			return fmt.Errorf("DeleteIpForwardEntry2: %w", e1)
+		}
+		return fmt.Errorf("DeleteIpForwardEntry2: code %d", r1)
+	}
+	return nil
+}
+
+func getIPForwardEntry2(route *MIB_IPFORWARD_ROW2) error {
+	r1, _, e1 := syscall.SyscallN(procGetIpForwardEntry2.Addr(), uintptr(unsafe.Pointer(route)))
+	if r1 != 0 {
+		if e1 != 0 {
+			return fmt.Errorf("GetIpForwardEntry2: %w", e1)
+		}
+		return fmt.Errorf("GetIpForwardEntry2: code %d", r1)
+	}
+	return nil
+}
+
+// https://learn.microsoft.com/en-us/windows/win32/api/netioapi/nf-netioapi-initializeipforwardentry
+func initializeIPForwardEntry(route *MIB_IPFORWARD_ROW2) {
+	// Does not return anything. Trying to handle the error might return an uninitialized value.
+	_, _, _ = syscall.SyscallN(procInitializeIpForwardEntry.Addr(), uintptr(unsafe.Pointer(route)))
+}
+
+func convertInterfaceIndexToLUID(interfaceIndex uint32, interfaceLUID *luid) error {
+	r1, _, e1 := syscall.SyscallN(procConvertInterfaceIndexToLuid.Addr(),
+		uintptr(interfaceIndex), uintptr(unsafe.Pointer(interfaceLUID)))
+	if r1 != 0 {
+		if e1 != 0 {
+			return fmt.Errorf("ConvertInterfaceIndexToLuid: %w", e1)
+		}
+		return fmt.Errorf("ConvertInterfaceIndexToLuid: code %d", r1)
 	}
 	return nil
 }
@@ -231,15 +403,15 @@ func (rm *RouteMonitor) parseUpdate(row *MIB_IPFORWARD_ROW2, notificationType MI
 		intf, err := net.InterfaceByIndex(idx)
 		if err != nil {
 			log.Warnf("failed to get interface name for index %d: %v", idx, err)
-			update.Interface = &net.Interface{
+			update.NextHop.Intf = &net.Interface{
 				Index: idx,
 			}
 		} else {
-			update.Interface = intf
+			update.NextHop.Intf = intf
 		}
 	}
 
-	log.Tracef("Received route update with destination %v, next hop %v, interface %v", row.DestinationPrefix, row.NextHop, update.Interface)
+	log.Tracef("Received route update with destination %v, next hop %v, interface %v", row.DestinationPrefix, row.NextHop, update.NextHop.Intf)
 	dest := parseIPPrefix(row.DestinationPrefix, idx)
 	if !dest.Addr().IsValid() {
 		return RouteUpdate{}, fmt.Errorf("invalid destination: %v", row)
@@ -262,7 +434,7 @@ func (rm *RouteMonitor) parseUpdate(row *MIB_IPFORWARD_ROW2, notificationType MI
 
 	update.Type = updateType
 	update.Destination = dest
-	update.NextHop = nexthop
+	update.NextHop.IP = nexthop
 
 	return update, nil
 }
@@ -320,7 +492,7 @@ func cancelMibChangeNotify2(handle windows.Handle) error {
 }
 
 // GetRoutesFromTable returns the current routing table from with prefixes only.
-// It ccaches the result for 2 seconds to avoid blocking the caller.
+// It caches the result for 2 seconds to avoid blocking the caller.
 func GetRoutesFromTable() ([]netip.Prefix, error) {
 	mux.Lock()
 	defer mux.Unlock()
@@ -389,35 +561,6 @@ func GetRoutes() ([]Route, error) {
 	return routes, nil
 }
 
-func addRouteCmd(prefix netip.Prefix, nexthop Nexthop) error {
-	args := []string{"add", prefix.String()}
-
-	if nexthop.IP.IsValid() {
-		ip := nexthop.IP.WithZone("")
-		args = append(args, ip.Unmap().String())
-	} else {
-		addr := "0.0.0.0"
-		if prefix.Addr().Is6() {
-			addr = "::"
-		}
-		args = append(args, addr)
-	}
-
-	if nexthop.Intf != nil {
-		args = append(args, "if", strconv.Itoa(nexthop.Intf.Index))
-	}
-
-	routeCmd := uspfilter.GetSystem32Command("route")
-
-	out, err := exec.Command(routeCmd, args...).CombinedOutput()
-	log.Tracef("route %s: %s", strings.Join(args, " "), out)
-	if err != nil {
-		return fmt.Errorf("route add: %w", err)
-	}
-
-	return nil
-}
-
 func isCacheDisabled() bool {
 	return os.Getenv("NB_DISABLE_ROUTE_CACHE") == "true"
 }
diff --git a/client/internal/routeselector/routeselector.go b/client/internal/routeselector/routeselector.go
index 72c4758f..8ebdc63e 100644
--- a/client/internal/routeselector/routeselector.go
+++ b/client/internal/routeselector/routeselector.go
@@ -14,23 +14,15 @@ import (
 )
 
 type RouteSelector struct {
-	mu             sync.RWMutex
-	selectedRoutes map[route.NetID]struct{}
-	selectAll      bool
-
-	// Indicates if new routes should be automatically selected
-	includeNewRoutes bool
-
-	// All known routes at the time of deselection
-	knownRoutes []route.NetID
+	mu               sync.RWMutex
+	deselectedRoutes map[route.NetID]struct{}
+	deselectAll      bool
 }
 
 func NewRouteSelector() *RouteSelector {
 	return &RouteSelector{
-		selectedRoutes:   map[route.NetID]struct{}{},
-		selectAll:        true,
-		includeNewRoutes: false,
-		knownRoutes:      []route.NetID{},
+		deselectedRoutes: map[route.NetID]struct{}{},
+		deselectAll:      false,
 	}
 }
 
@@ -39,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
@@ -49,11 +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.includeNewRoutes = false
+
+	rs.deselectAll = false
 
 	return errors.FormatErrorOrNil(err)
 }
@@ -63,38 +57,26 @@ func (rs *RouteSelector) SelectAllRoutes() {
 	rs.mu.Lock()
 	defer rs.mu.Unlock()
 
-	rs.selectAll = true
-	rs.selectedRoutes = map[route.NetID]struct{}{}
-	rs.includeNewRoutes = false
+	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
-// but will keep new routes selected.
 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.includeNewRoutes = true
-		rs.knownRoutes = make([]route.NetID, len(allRoutes))
-		copy(rs.knownRoutes, allRoutes)
-
-		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)
@@ -105,9 +87,8 @@ func (rs *RouteSelector) DeselectAllRoutes() {
 	rs.mu.Lock()
 	defer rs.mu.Unlock()
 
-	rs.selectAll = false
-	rs.includeNewRoutes = false
-	rs.selectedRoutes = map[route.NetID]struct{}{}
+	rs.deselectAll = true
+	maps.Clear(rs.deselectedRoutes)
 }
 
 // IsSelected checks if a specific route is selected.
@@ -115,23 +96,12 @@ func (rs *RouteSelector) IsSelected(routeID route.NetID) bool {
 	rs.mu.RLock()
 	defer rs.mu.RUnlock()
 
-	if rs.selectAll {
-		return true
-	}
-
-	// Check if the route exists in selectedRoutes
-	_, selected := rs.selectedRoutes[routeID]
-	if selected {
-		return true
+	if rs.deselectAll {
+		return false
 	}
 
-	// If includeNewRoutes is true and this is a new route (not in knownRoutes),
-	// then it should be selected
-	if rs.includeNewRoutes && !slices.Contains(rs.knownRoutes, routeID) {
-		return true
-	}
-
-	return false
+	_, deselected := rs.deselectedRoutes[routeID]
+	return !deselected
 }
 
 // FilterSelected removes unselected routes from the provided map.
@@ -139,17 +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 {
 		netID := id.NetID()
-		_, selected := rs.selectedRoutes[netID]
-
-		// Include if directly selected or if it's a new route and includeNewRoutes is true
-		if selected || (rs.includeNewRoutes && !slices.Contains(rs.knownRoutes, netID)) {
+		_, deselected := rs.deselectedRoutes[netID]
+		if !deselected {
 			filtered[id] = rt
 		}
 	}
@@ -162,15 +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"`
-		IncludeNewRoutes bool                     `json:"include_new_routes"`
-		KnownRoutes      []route.NetID            `json:"known_routes"`
+		DeselectedRoutes map[route.NetID]struct{} `json:"deselected_routes"`
+		DeselectAll      bool                     `json:"deselect_all"`
 	}{
-		SelectAll:        rs.selectAll,
-		SelectedRoutes:   rs.selectedRoutes,
-		IncludeNewRoutes: rs.includeNewRoutes,
-		KnownRoutes:      rs.knownRoutes,
+		DeselectedRoutes: rs.deselectedRoutes,
+		DeselectAll:      rs.deselectAll,
 	})
 }
 
@@ -182,34 +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.includeNewRoutes = false
-		rs.knownRoutes = []route.NetID{}
+		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"`
-		IncludeNewRoutes bool                     `json:"include_new_routes"`
-		KnownRoutes      []route.NetID            `json:"known_routes"`
+		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.includeNewRoutes = temp.IncludeNewRoutes
-	rs.knownRoutes = temp.KnownRoutes
+	rs.deselectedRoutes = temp.DeselectedRoutes
+	rs.deselectAll = temp.DeselectAll
 
-	if rs.selectedRoutes == nil {
-		rs.selectedRoutes = map[route.NetID]struct{}{}
-	}
-	if rs.knownRoutes == nil {
-		rs.knownRoutes = []route.NetID{}
+	if rs.deselectedRoutes == nil {
+		rs.deselectedRoutes = map[route.NetID]struct{}{}
 	}
 
 	return nil
diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go
index d04d7a9c..b88d0aa3 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.12
 // source: daemon.proto
 
 package proto
@@ -255,7 +255,7 @@ type LoginRequest struct {
 	// omits initialized empty slices due to omitempty tags
 	CleanNATExternalIPs  bool                 `protobuf:"varint,6,opt,name=cleanNATExternalIPs,proto3" json:"cleanNATExternalIPs,omitempty"`
 	CustomDNSAddress     []byte               `protobuf:"bytes,7,opt,name=customDNSAddress,proto3" json:"customDNSAddress,omitempty"`
-	IsLinuxDesktopClient bool                 `protobuf:"varint,8,opt,name=isLinuxDesktopClient,proto3" json:"isLinuxDesktopClient,omitempty"`
+	IsUnixDesktopClient  bool                 `protobuf:"varint,8,opt,name=isUnixDesktopClient,proto3" json:"isUnixDesktopClient,omitempty"`
 	Hostname             string               `protobuf:"bytes,9,opt,name=hostname,proto3" json:"hostname,omitempty"`
 	RosenpassEnabled     *bool                `protobuf:"varint,10,opt,name=rosenpassEnabled,proto3,oneof" json:"rosenpassEnabled,omitempty"`
 	InterfaceName        *string              `protobuf:"bytes,11,opt,name=interfaceName,proto3,oneof" json:"interfaceName,omitempty"`
@@ -277,7 +277,9 @@ type LoginRequest struct {
 	// cleanDNSLabels clean map list of DNS labels.
 	// This is needed because the generated code
 	// omits initialized empty slices due to omitempty tags
-	CleanDNSLabels bool `protobuf:"varint,27,opt,name=cleanDNSLabels,proto3" json:"cleanDNSLabels,omitempty"`
+	CleanDNSLabels        bool  `protobuf:"varint,27,opt,name=cleanDNSLabels,proto3" json:"cleanDNSLabels,omitempty"`
+	LazyConnectionEnabled *bool `protobuf:"varint,28,opt,name=lazyConnectionEnabled,proto3,oneof" json:"lazyConnectionEnabled,omitempty"`
+	BlockInbound          *bool `protobuf:"varint,29,opt,name=block_inbound,json=blockInbound,proto3,oneof" json:"block_inbound,omitempty"`
 }
 
 func (x *LoginRequest) Reset() {
@@ -362,9 +364,9 @@ func (x *LoginRequest) GetCustomDNSAddress() []byte {
 	return nil
 }
 
-func (x *LoginRequest) GetIsLinuxDesktopClient() bool {
+func (x *LoginRequest) GetIsUnixDesktopClient() bool {
 	if x != nil {
-		return x.IsLinuxDesktopClient
+		return x.IsUnixDesktopClient
 	}
 	return false
 }
@@ -502,6 +504,20 @@ func (x *LoginRequest) GetCleanDNSLabels() bool {
 	return false
 }
 
+func (x *LoginRequest) GetLazyConnectionEnabled() bool {
+	if x != nil && x.LazyConnectionEnabled != nil {
+		return *x.LazyConnectionEnabled
+	}
+	return false
+}
+
+func (x *LoginRequest) GetBlockInbound() bool {
+	if x != nil && x.BlockInbound != nil {
+		return *x.BlockInbound
+	}
+	return false
+}
+
 type LoginResponse struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -982,14 +998,16 @@ type GetConfigResponse struct {
 	// preSharedKey settings value.
 	PreSharedKey string `protobuf:"bytes,4,opt,name=preSharedKey,proto3" json:"preSharedKey,omitempty"`
 	// adminURL settings value.
-	AdminURL             string `protobuf:"bytes,5,opt,name=adminURL,proto3" json:"adminURL,omitempty"`
-	InterfaceName        string `protobuf:"bytes,6,opt,name=interfaceName,proto3" json:"interfaceName,omitempty"`
-	WireguardPort        int64  `protobuf:"varint,7,opt,name=wireguardPort,proto3" json:"wireguardPort,omitempty"`
-	DisableAutoConnect   bool   `protobuf:"varint,9,opt,name=disableAutoConnect,proto3" json:"disableAutoConnect,omitempty"`
-	ServerSSHAllowed     bool   `protobuf:"varint,10,opt,name=serverSSHAllowed,proto3" json:"serverSSHAllowed,omitempty"`
-	RosenpassEnabled     bool   `protobuf:"varint,11,opt,name=rosenpassEnabled,proto3" json:"rosenpassEnabled,omitempty"`
-	RosenpassPermissive  bool   `protobuf:"varint,12,opt,name=rosenpassPermissive,proto3" json:"rosenpassPermissive,omitempty"`
-	DisableNotifications bool   `protobuf:"varint,13,opt,name=disable_notifications,json=disableNotifications,proto3" json:"disable_notifications,omitempty"`
+	AdminURL              string `protobuf:"bytes,5,opt,name=adminURL,proto3" json:"adminURL,omitempty"`
+	InterfaceName         string `protobuf:"bytes,6,opt,name=interfaceName,proto3" json:"interfaceName,omitempty"`
+	WireguardPort         int64  `protobuf:"varint,7,opt,name=wireguardPort,proto3" json:"wireguardPort,omitempty"`
+	DisableAutoConnect    bool   `protobuf:"varint,9,opt,name=disableAutoConnect,proto3" json:"disableAutoConnect,omitempty"`
+	ServerSSHAllowed      bool   `protobuf:"varint,10,opt,name=serverSSHAllowed,proto3" json:"serverSSHAllowed,omitempty"`
+	RosenpassEnabled      bool   `protobuf:"varint,11,opt,name=rosenpassEnabled,proto3" json:"rosenpassEnabled,omitempty"`
+	RosenpassPermissive   bool   `protobuf:"varint,12,opt,name=rosenpassPermissive,proto3" json:"rosenpassPermissive,omitempty"`
+	DisableNotifications  bool   `protobuf:"varint,13,opt,name=disable_notifications,json=disableNotifications,proto3" json:"disable_notifications,omitempty"`
+	LazyConnectionEnabled bool   `protobuf:"varint,14,opt,name=lazyConnectionEnabled,proto3" json:"lazyConnectionEnabled,omitempty"`
+	BlockInbound          bool   `protobuf:"varint,15,opt,name=blockInbound,proto3" json:"blockInbound,omitempty"`
 }
 
 func (x *GetConfigResponse) Reset() {
@@ -1108,6 +1126,20 @@ func (x *GetConfigResponse) GetDisableNotifications() bool {
 	return false
 }
 
+func (x *GetConfigResponse) GetLazyConnectionEnabled() bool {
+	if x != nil {
+		return x.LazyConnectionEnabled
+	}
+	return false
+}
+
+func (x *GetConfigResponse) GetBlockInbound() bool {
+	if x != nil {
+		return x.BlockInbound
+	}
+	return false
+}
+
 // PeerState contains the latest state of a peer
 type PeerState struct {
 	state         protoimpl.MessageState
@@ -1657,6 +1689,7 @@ type FullStatus struct {
 	DnsServers              []*NSGroupState  `protobuf:"bytes,6,rep,name=dns_servers,json=dnsServers,proto3" json:"dns_servers,omitempty"`
 	NumberOfForwardingRules int32            `protobuf:"varint,8,opt,name=NumberOfForwardingRules,proto3" json:"NumberOfForwardingRules,omitempty"`
 	Events                  []*SystemEvent   `protobuf:"bytes,7,rep,name=events,proto3" json:"events,omitempty"`
+	LazyConnectionEnabled   bool             `protobuf:"varint,9,opt,name=lazyConnectionEnabled,proto3" json:"lazyConnectionEnabled,omitempty"`
 }
 
 func (x *FullStatus) Reset() {
@@ -1747,6 +1780,13 @@ func (x *FullStatus) GetEvents() []*SystemEvent {
 	return nil
 }
 
+func (x *FullStatus) GetLazyConnectionEnabled() bool {
+	if x != nil {
+		return x.LazyConnectionEnabled
+	}
+	return false
+}
+
 // Networks
 type ListNetworksRequest struct {
 	state         protoimpl.MessageState
@@ -2277,6 +2317,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 +2373,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 +2429,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
@@ -3585,7 +3649,7 @@ var file_daemon_proto_rawDesc = []byte{
 	0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
 	0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74,
 	0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x0e, 0x0a, 0x0c, 0x45, 0x6d, 0x70,
-	0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xb0, 0x0c, 0x0a, 0x0c, 0x4c, 0x6f,
+	0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xbf, 0x0d, 0x0a, 0x0c, 0x4c, 0x6f,
 	0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65,
 	0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65,
 	0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x12, 0x26, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61,
@@ -3603,565 +3667,590 @@ var file_daemon_proto_rawDesc = []byte{
 	0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x49, 0x50, 0x73, 0x12, 0x2a, 0x0a, 0x10, 0x63, 0x75,
 	0x73, 0x74, 0x6f, 0x6d, 0x44, 0x4e, 0x53, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x07,
 	0x20, 0x01, 0x28, 0x0c, 0x52, 0x10, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x44, 0x4e, 0x53, 0x41,
-	0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x69, 0x73, 0x4c, 0x69, 0x6e, 0x75,
-	0x78, 0x44, 0x65, 0x73, 0x6b, 0x74, 0x6f, 0x70, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x18, 0x08,
-	0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x69, 0x73, 0x4c, 0x69, 0x6e, 0x75, 0x78, 0x44, 0x65, 0x73,
-	0x6b, 0x74, 0x6f, 0x70, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f,
-	0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f,
-	0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2f, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70,
-	0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08,
-	0x48, 0x00, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61,
-	0x62, 0x6c, 0x65, 0x64, 0x88, 0x01, 0x01, 0x12, 0x29, 0x0a, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72,
-	0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01,
-	0x52, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x88,
-	0x01, 0x01, 0x12, 0x29, 0x0a, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50,
-	0x6f, 0x72, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x48, 0x02, 0x52, 0x0d, 0x77, 0x69, 0x72,
-	0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x88, 0x01, 0x01, 0x12, 0x37, 0x0a,
-	0x14, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72,
-	0x65, 0x64, 0x4b, 0x65, 0x79, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x48, 0x03, 0x52, 0x14, 0x6f,
+	0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x30, 0x0a, 0x13, 0x69, 0x73, 0x55, 0x6e, 0x69, 0x78,
+	0x44, 0x65, 0x73, 0x6b, 0x74, 0x6f, 0x70, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x18, 0x08, 0x20,
+	0x01, 0x28, 0x08, 0x52, 0x13, 0x69, 0x73, 0x55, 0x6e, 0x69, 0x78, 0x44, 0x65, 0x73, 0x6b, 0x74,
+	0x6f, 0x70, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74,
+	0x6e, 0x61, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74,
+	0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2f, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73,
+	0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00,
+	0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c,
+	0x65, 0x64, 0x88, 0x01, 0x01, 0x12, 0x29, 0x0a, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61,
+	0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x0d,
+	0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x88, 0x01, 0x01,
+	0x12, 0x29, 0x0a, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72,
+	0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x48, 0x02, 0x52, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67,
+	0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x88, 0x01, 0x01, 0x12, 0x37, 0x0a, 0x14, 0x6f,
 	0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64,
-	0x4b, 0x65, 0x79, 0x88, 0x01, 0x01, 0x12, 0x33, 0x0a, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c,
-	0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x18, 0x0e, 0x20, 0x01,
-	0x28, 0x08, 0x48, 0x04, 0x52, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74,
-	0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x88, 0x01, 0x01, 0x12, 0x2f, 0x0a, 0x10, 0x73,
-	0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18,
-	0x0f, 0x20, 0x01, 0x28, 0x08, 0x48, 0x05, 0x52, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53,
-	0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x88, 0x01, 0x01, 0x12, 0x35, 0x0a, 0x13,
-	0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73,
-	0x69, 0x76, 0x65, 0x18, 0x10, 0x20, 0x01, 0x28, 0x08, 0x48, 0x06, 0x52, 0x13, 0x72, 0x6f, 0x73,
-	0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65,
-	0x88, 0x01, 0x01, 0x12, 0x30, 0x0a, 0x13, 0x65, 0x78, 0x74, 0x72, 0x61, 0x49, 0x46, 0x61, 0x63,
-	0x65, 0x42, 0x6c, 0x61, 0x63, 0x6b, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x11, 0x20, 0x03, 0x28, 0x09,
-	0x52, 0x13, 0x65, 0x78, 0x74, 0x72, 0x61, 0x49, 0x46, 0x61, 0x63, 0x65, 0x42, 0x6c, 0x61, 0x63,
-	0x6b, 0x6c, 0x69, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x0e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
-	0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x18, 0x12, 0x20, 0x01, 0x28, 0x08, 0x48, 0x07, 0x52,
-	0x0e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x88,
-	0x01, 0x01, 0x12, 0x4a, 0x0a, 0x10, 0x64, 0x6e, 0x73, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x6e,
-	0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x13, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67,
-	0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44,
-	0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x08, 0x52, 0x10, 0x64, 0x6e, 0x73, 0x52, 0x6f,
-	0x75, 0x74, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x37,
-	0x0a, 0x15, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74,
-	0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x14, 0x20, 0x01, 0x28, 0x08, 0x48, 0x09, 0x52,
-	0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x6f,
-	0x75, 0x74, 0x65, 0x73, 0x88, 0x01, 0x01, 0x12, 0x37, 0x0a, 0x15, 0x64, 0x69, 0x73, 0x61, 0x62,
-	0x6c, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73,
-	0x18, 0x15, 0x20, 0x01, 0x28, 0x08, 0x48, 0x0a, 0x52, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c,
-	0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x88, 0x01, 0x01,
-	0x12, 0x24, 0x0a, 0x0b, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x6e, 0x73, 0x18,
-	0x16, 0x20, 0x01, 0x28, 0x08, 0x48, 0x0b, 0x52, 0x0a, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65,
-	0x44, 0x6e, 0x73, 0x88, 0x01, 0x01, 0x12, 0x2e, 0x0a, 0x10, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c,
-	0x65, 0x5f, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x18, 0x17, 0x20, 0x01, 0x28, 0x08,
-	0x48, 0x0c, 0x52, 0x0f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77,
-	0x61, 0x6c, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x2d, 0x0a, 0x10, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f,
-	0x6c, 0x61, 0x6e, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x18, 0x20, 0x01, 0x28, 0x08,
-	0x48, 0x0d, 0x52, 0x0e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x4c, 0x61, 0x6e, 0x41, 0x63, 0x63, 0x65,
-	0x73, 0x73, 0x88, 0x01, 0x01, 0x12, 0x38, 0x0a, 0x15, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65,
-	0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x19,
-	0x20, 0x01, 0x28, 0x08, 0x48, 0x0e, 0x52, 0x14, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x4e,
-	0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x88, 0x01, 0x01, 0x12,
-	0x1d, 0x0a, 0x0a, 0x64, 0x6e, 0x73, 0x5f, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x1a, 0x20,
-	0x03, 0x28, 0x09, 0x52, 0x09, 0x64, 0x6e, 0x73, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x26,
-	0x0a, 0x0e, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x44, 0x4e, 0x53, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73,
-	0x18, 0x1b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x44, 0x4e, 0x53,
-	0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e,
-	0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x42, 0x10, 0x0a, 0x0e, 0x5f,
-	0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x42, 0x10, 0x0a,
-	0x0e, 0x5f, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x42,
-	0x17, 0x0a, 0x15, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x53,
-	0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x64, 0x69, 0x73,
-	0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x42,
-	0x13, 0x0a, 0x11, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c,
-	0x6f, 0x77, 0x65, 0x64, 0x42, 0x16, 0x0a, 0x14, 0x5f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61,
-	0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x42, 0x11, 0x0a, 0x0f,
-	0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x42,
-	0x13, 0x0a, 0x11, 0x5f, 0x64, 0x6e, 0x73, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x74, 0x65,
-	0x72, 0x76, 0x61, 0x6c, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65,
-	0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x42, 0x18,
-	0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65,
-	0x72, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x69, 0x73,
-	0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x6e, 0x73, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x64, 0x69, 0x73,
-	0x61, 0x62, 0x6c, 0x65, 0x5f, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x42, 0x13, 0x0a,
-	0x11, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x6c, 0x61, 0x6e, 0x5f, 0x61, 0x63, 0x63, 0x65,
-	0x73, 0x73, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e,
-	0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xb5, 0x01, 0x0a,
-	0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24,
-	0x0a, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18,
-	0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c,
-	0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65,
-	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65,
-	0x12, 0x28, 0x0a, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
-	0x55, 0x52, 0x49, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66,
-	0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x12, 0x38, 0x0a, 0x17, 0x76, 0x65,
-	0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d,
-	0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x76, 0x65, 0x72,
+	0x4b, 0x65, 0x79, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x48, 0x03, 0x52, 0x14, 0x6f, 0x70, 0x74,
+	0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65,
+	0x79, 0x88, 0x01, 0x01, 0x12, 0x33, 0x0a, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41,
+	0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x08,
+	0x48, 0x04, 0x52, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43,
+	0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x88, 0x01, 0x01, 0x12, 0x2f, 0x0a, 0x10, 0x73, 0x65, 0x72,
+	0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x0f, 0x20,
+	0x01, 0x28, 0x08, 0x48, 0x05, 0x52, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48,
+	0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x88, 0x01, 0x01, 0x12, 0x35, 0x0a, 0x13, 0x72, 0x6f,
+	0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76,
+	0x65, 0x18, 0x10, 0x20, 0x01, 0x28, 0x08, 0x48, 0x06, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e,
+	0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x88, 0x01,
+	0x01, 0x12, 0x30, 0x0a, 0x13, 0x65, 0x78, 0x74, 0x72, 0x61, 0x49, 0x46, 0x61, 0x63, 0x65, 0x42,
+	0x6c, 0x61, 0x63, 0x6b, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x11, 0x20, 0x03, 0x28, 0x09, 0x52, 0x13,
+	0x65, 0x78, 0x74, 0x72, 0x61, 0x49, 0x46, 0x61, 0x63, 0x65, 0x42, 0x6c, 0x61, 0x63, 0x6b, 0x6c,
+	0x69, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x0e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f,
+	0x6e, 0x69, 0x74, 0x6f, 0x72, 0x18, 0x12, 0x20, 0x01, 0x28, 0x08, 0x48, 0x07, 0x52, 0x0e, 0x6e,
+	0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x88, 0x01, 0x01,
+	0x12, 0x4a, 0x0a, 0x10, 0x64, 0x6e, 0x73, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x74, 0x65,
+	0x72, 0x76, 0x61, 0x6c, 0x18, 0x13, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f,
+	0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72,
+	0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x08, 0x52, 0x10, 0x64, 0x6e, 0x73, 0x52, 0x6f, 0x75, 0x74,
+	0x65, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x37, 0x0a, 0x15,
+	0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x72,
+	0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x14, 0x20, 0x01, 0x28, 0x08, 0x48, 0x09, 0x52, 0x13, 0x64,
+	0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x6f, 0x75, 0x74,
+	0x65, 0x73, 0x88, 0x01, 0x01, 0x12, 0x37, 0x0a, 0x15, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65,
+	0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x15,
+	0x20, 0x01, 0x28, 0x08, 0x48, 0x0a, 0x52, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53,
+	0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x88, 0x01, 0x01, 0x12, 0x24,
+	0x0a, 0x0b, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x6e, 0x73, 0x18, 0x16, 0x20,
+	0x01, 0x28, 0x08, 0x48, 0x0b, 0x52, 0x0a, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x44, 0x6e,
+	0x73, 0x88, 0x01, 0x01, 0x12, 0x2e, 0x0a, 0x10, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f,
+	0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x18, 0x17, 0x20, 0x01, 0x28, 0x08, 0x48, 0x0c,
+	0x52, 0x0f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c,
+	0x6c, 0x88, 0x01, 0x01, 0x12, 0x2d, 0x0a, 0x10, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x6c, 0x61,
+	0x6e, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x18, 0x20, 0x01, 0x28, 0x08, 0x48, 0x0d,
+	0x52, 0x0e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x4c, 0x61, 0x6e, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73,
+	0x88, 0x01, 0x01, 0x12, 0x38, 0x0a, 0x15, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e,
+	0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x19, 0x20, 0x01,
+	0x28, 0x08, 0x48, 0x0e, 0x52, 0x14, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x4e, 0x6f, 0x74,
+	0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a,
+	0x0a, 0x64, 0x6e, 0x73, 0x5f, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x1a, 0x20, 0x03, 0x28,
+	0x09, 0x52, 0x09, 0x64, 0x6e, 0x73, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x26, 0x0a, 0x0e,
+	0x63, 0x6c, 0x65, 0x61, 0x6e, 0x44, 0x4e, 0x53, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x1b,
+	0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x44, 0x4e, 0x53, 0x4c, 0x61,
+	0x62, 0x65, 0x6c, 0x73, 0x12, 0x39, 0x0a, 0x15, 0x6c, 0x61, 0x7a, 0x79, 0x43, 0x6f, 0x6e, 0x6e,
+	0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x1c, 0x20,
+	0x01, 0x28, 0x08, 0x48, 0x0f, 0x52, 0x15, 0x6c, 0x61, 0x7a, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65,
+	0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x88, 0x01, 0x01, 0x12,
+	0x28, 0x0a, 0x0d, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64,
+	0x18, 0x1d, 0x20, 0x01, 0x28, 0x08, 0x48, 0x10, 0x52, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x49,
+	0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x88, 0x01, 0x01, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x72, 0x6f,
+	0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x42, 0x10,
+	0x0a, 0x0e, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65,
+	0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f,
+	0x72, 0x74, 0x42, 0x17, 0x0a, 0x15, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x50,
+	0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x42, 0x15, 0x0a, 0x13, 0x5f,
+	0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65,
+	0x63, 0x74, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48,
+	0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x42, 0x16, 0x0a, 0x14, 0x5f, 0x72, 0x6f, 0x73, 0x65,
+	0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x42,
+	0x11, 0x0a, 0x0f, 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, 0x6e, 0x69, 0x74,
+	0x6f, 0x72, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x64, 0x6e, 0x73, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x49,
+	0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61,
+	0x62, 0x6c, 0x65, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65,
+	0x73, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x73, 0x65,
+	0x72, 0x76, 0x65, 0x72, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x42, 0x0e, 0x0a, 0x0c, 0x5f,
+	0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x6e, 0x73, 0x42, 0x13, 0x0a, 0x11, 0x5f,
+	0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c,
+	0x42, 0x13, 0x0a, 0x11, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x6c, 0x61, 0x6e, 0x5f, 0x61,
+	0x63, 0x63, 0x65, 0x73, 0x73, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c,
+	0x65, 0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42,
+	0x18, 0x0a, 0x16, 0x5f, 0x6c, 0x61, 0x7a, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69,
+	0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x62, 0x6c,
+	0x6f, 0x63, 0x6b, 0x5f, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0xb5, 0x01, 0x0a, 0x0d,
+	0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a,
+	0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x01,
+	0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f,
+	0x67, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18,
+	0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12,
+	0x28, 0x0a, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55,
+	0x52, 0x49, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69,
+	0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x12, 0x38, 0x0a, 0x17, 0x76, 0x65, 0x72,
 	0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70,
-	0x6c, 0x65, 0x74, 0x65, 0x22, 0x4d, 0x0a, 0x13, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c,
-	0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75,
-	0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75,
-	0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e,
-	0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e,
-	0x61, 0x6d, 0x65, 0x22, 0x16, 0x0a, 0x14, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f,
-	0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x0b, 0x0a, 0x09, 0x55,
-	0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0c, 0x0a, 0x0a, 0x55, 0x70, 0x52, 0x65,
-	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3d, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
-	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75,
-	0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01,
-	0x28, 0x08, 0x52, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53,
-	0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x82, 0x01, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
-	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74,
-	0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73,
-	0x12, 0x32, 0x0a, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02,
-	0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x75,
-	0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74,
-	0x61, 0x74, 0x75, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65,
-	0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x61, 0x65,
-	0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x0d, 0x0a, 0x0b, 0x44, 0x6f,
-	0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0e, 0x0a, 0x0c, 0x44, 0x6f, 0x77,
-	0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74,
-	0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xee, 0x03,
-	0x0a, 0x11, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f,
-	0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
-	0x74, 0x55, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6d, 0x61, 0x6e, 0x61,
-	0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e,
-	0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63,
-	0x6f, 0x6e, 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6c, 0x6f, 0x67,
-	0x46, 0x69, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x46,
-	0x69, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64,
-	0x4b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68,
-	0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e,
-	0x55, 0x52, 0x4c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e,
-	0x55, 0x52, 0x4c, 0x12, 0x24, 0x0a, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65,
-	0x4e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x69, 0x6e, 0x74, 0x65,
-	0x72, 0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x77, 0x69, 0x72,
-	0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03,
-	0x52, 0x0d, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x12,
-	0x2e, 0x0a, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f,
-	0x6e, 0x6e, 0x65, 0x63, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x64, 0x69, 0x73,
-	0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x12,
-	0x2a, 0x0a, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f,
-	0x77, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65,
-	0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x2a, 0x0a, 0x10, 0x72,
-	0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18,
-	0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73,
-	0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e,
-	0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x0c,
-	0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50,
-	0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x12, 0x33, 0x0a, 0x15, 0x64, 0x69, 0x73,
-	0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f,
-	0x6e, 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c,
-	0x65, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xde,
-	0x05, 0x0a, 0x09, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02,
-	0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06,
-	0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75,
-	0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74,
-	0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74,
-	0x61, 0x74, 0x75, 0x73, 0x12, 0x46, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74,
-	0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a,
-	0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
-	0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x6e,
-	0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07,
-	0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72,
-	0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49,
+	0x6c, 0x65, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x76, 0x65, 0x72, 0x69,
+	0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70, 0x6c,
+	0x65, 0x74, 0x65, 0x22, 0x4d, 0x0a, 0x13, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f,
+	0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73,
+	0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73,
+	0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61,
+	0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61,
+	0x6d, 0x65, 0x22, 0x16, 0x0a, 0x14, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67,
+	0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x0b, 0x0a, 0x09, 0x55, 0x70,
+	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0c, 0x0a, 0x0a, 0x55, 0x70, 0x52, 0x65, 0x73,
+	0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3d, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52,
+	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c,
+	0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28,
+	0x08, 0x52, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74,
+	0x61, 0x74, 0x75, 0x73, 0x22, 0x82, 0x01, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52,
+	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75,
+	0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12,
+	0x32, 0x0a, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20,
+	0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x75, 0x6c,
+	0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61,
+	0x74, 0x75, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72,
+	0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x61, 0x65, 0x6d,
+	0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x0d, 0x0a, 0x0b, 0x44, 0x6f, 0x77,
+	0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0e, 0x0a, 0x0c, 0x44, 0x6f, 0x77, 0x6e,
+	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x43,
+	0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xc8, 0x04, 0x0a,
+	0x11, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
+	0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
+	0x55, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67,
+	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x66,
+	0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f,
+	0x6e, 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6c, 0x6f, 0x67, 0x46,
+	0x69, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x46, 0x69,
+	0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b,
+	0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61,
+	0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55,
+	0x52, 0x4c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55,
+	0x52, 0x4c, 0x12, 0x24, 0x0a, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x4e,
+	0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x69, 0x6e, 0x74, 0x65, 0x72,
+	0x66, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x77, 0x69, 0x72, 0x65,
+	0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52,
+	0x0d, 0x77, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x2e,
+	0x0a, 0x12, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e,
+	0x6e, 0x65, 0x63, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x64, 0x69, 0x73, 0x61,
+	0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, 0x6f, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x12, 0x2a,
+	0x0a, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77,
+	0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72,
+	0x53, 0x53, 0x48, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f,
+	0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0b,
+	0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45,
+	0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70,
+	0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x0c, 0x20,
+	0x01, 0x28, 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65,
+	0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x12, 0x33, 0x0a, 0x15, 0x64, 0x69, 0x73, 0x61,
+	0x62, 0x6c, 0x65, 0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
+	0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65,
+	0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x34, 0x0a,
+	0x15, 0x6c, 0x61, 0x7a, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45,
+	0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x08, 0x52, 0x15, 0x6c, 0x61,
+	0x7a, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62,
+	0x6c, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x49, 0x6e, 0x62, 0x6f,
+	0x75, 0x6e, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b,
+	0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0xde, 0x05, 0x0a, 0x09, 0x50, 0x65, 0x65, 0x72,
+	0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28,
+	0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18,
+	0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a,
+	0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28,
+	0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x46, 0x0a,
+	0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74,
+	0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
+	0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74,
+	0x61, 0x6d, 0x70, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55,
+	0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64,
+	0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x12,
+	0x34, 0x0a, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69,
+	0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15,
+	0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74,
+	0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x36, 0x0a, 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49,
 	0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18,
-	0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43,
-	0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x36, 0x0a, 0x16,
-	0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61,
-	0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x72, 0x65,
-	0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65,
-	0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x09, 0x20, 0x01,
-	0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x3c, 0x0a, 0x19, 0x6c, 0x6f, 0x63, 0x61,
-	0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64,
-	0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x19, 0x6c, 0x6f, 0x63,
-	0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e,
-	0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x3e, 0x0a, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65,
-	0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70,
-	0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, 0x72, 0x65, 0x6d, 0x6f,
-	0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e,
-	0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x52, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69,
-	0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65,
-	0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
-	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
-	0x6d, 0x70, 0x52, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72,
-	0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79,
-	0x74, 0x65, 0x73, 0x52, 0x78, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74,
-	0x65, 0x73, 0x52, 0x78, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x18,
-	0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x12, 0x2a,
-	0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c,
-	0x65, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70,
-	0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65,
-	0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65,
-	0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x33, 0x0a, 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63,
-	0x79, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
-	0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69,
-	0x6f, 0x6e, 0x52, 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x72,
-	0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28,
-	0x09, 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22,
-	0xf0, 0x01, 0x0a, 0x0e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61,
-	0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02,
-	0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01,
-	0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x6b, 0x65,
-	0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20,
-	0x01, 0x28, 0x08, 0x52, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72,
-	0x66, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01,
-	0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65,
-	0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01,
-	0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61,
-	0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73,
-	0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28,
-	0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d,
-	0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
-	0x6b, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
-	0x6b, 0x73, 0x22, 0x53, 0x0a, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74,
-	0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03,
-	0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64,
-	0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65,
-	0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
-	0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x57, 0x0a, 0x0f, 0x4d, 0x61, 0x6e, 0x61, 0x67,
-	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52,
+	0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65,
+	0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a,
+	0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64,
+	0x6e, 0x12, 0x3c, 0x0a, 0x19, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e,
+	0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0a,
+	0x20, 0x01, 0x28, 0x09, 0x52, 0x19, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61,
+	0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12,
+	0x3e, 0x0a, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64,
+	0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x0b, 0x20,
+	0x01, 0x28, 0x09, 0x52, 0x1a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61,
+	0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12,
+	0x52, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64,
+	0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32,
+	0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
+	0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x16, 0x6c, 0x61, 0x73,
+	0x74, 0x57, 0x69, 0x72, 0x65, 0x67, 0x75, 0x61, 0x72, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68,
+	0x61, 0x6b, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x52, 0x78, 0x18, 0x0d,
+	0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x52, 0x78, 0x12, 0x18, 0x0a,
+	0x07, 0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07,
+	0x62, 0x79, 0x74, 0x65, 0x73, 0x54, 0x78, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e,
+	0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x0f, 0x20, 0x01, 0x28,
+	0x08, 0x52, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62,
+	0x6c, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18,
+	0x10, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12,
+	0x33, 0x0a, 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b,
+	0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
+	0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x6c, 0x61, 0x74,
+	0x65, 0x6e, 0x63, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x41, 0x64, 0x64,
+	0x72, 0x65, 0x73, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x61,
+	0x79, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0xf0, 0x01, 0x0a, 0x0e, 0x4c, 0x6f, 0x63,
+	0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49,
+	0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70,
+	0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62,
+	0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74,
+	0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6b, 0x65,
+	0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a,
+	0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64,
+	0x6e, 0x12, 0x2a, 0x0a, 0x10, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e,
+	0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x6f, 0x73,
+	0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x30, 0x0a,
+	0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73,
+	0x73, 0x69, 0x76, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65,
+	0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x76, 0x65, 0x12,
+	0x1a, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28,
+	0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x22, 0x53, 0x0a, 0x0b, 0x53,
+	0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52,
 	0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09,
 	0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52,
 	0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72,
 	0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72,
-	0x22, 0x52, 0x0a, 0x0a, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10,
-	0x0a, 0x03, 0x55, 0x52, 0x49, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x49,
-	0x12, 0x1c, 0x0a, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20,
-	0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x14,
-	0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65,
-	0x72, 0x72, 0x6f, 0x72, 0x22, 0x72, 0x0a, 0x0c, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53,
-	0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18,
-	0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18,
-	0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52,
-	0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62,
-	0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c,
-	0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28,
-	0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0xb9, 0x03, 0x0a, 0x0a, 0x46, 0x75, 0x6c,
-	0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x41, 0x0a, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67,
-	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
-	0x32, 0x17, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65,
-	0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67,
-	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, 0x0b, 0x73, 0x69,
-	0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
-	0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53,
-	0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74,
-	0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74,
-	0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d,
-	0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74,
-	0x65, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74,
-	0x65, 0x12, 0x27, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b,
-	0x32, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74,
-	0x61, 0x74, 0x65, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x06, 0x72, 0x65,
-	0x6c, 0x61, 0x79, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65,
-	0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06,
-	0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x12, 0x35, 0x0a, 0x0b, 0x64, 0x6e, 0x73, 0x5f, 0x73, 0x65,
-	0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x64, 0x61,
-	0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74,
-	0x65, 0x52, 0x0a, 0x64, 0x6e, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x38, 0x0a,
-	0x17, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x4f, 0x66, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64,
-	0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x17,
-	0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x4f, 0x66, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69,
-	0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x2b, 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74,
-	0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e,
-	0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76,
-	0x65, 0x6e, 0x74, 0x73, 0x22, 0x15, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77,
-	0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3f, 0x0a, 0x14, 0x4c,
-	0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
-	0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20,
-	0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74,
-	0x77, 0x6f, 0x72, 0x6b, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x61, 0x0a, 0x15,
-	0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65,
-	0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
-	0x49, 0x44, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f,
-	0x72, 0x6b, 0x49, 0x44, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x18,
-	0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x12, 0x10, 0x0a,
-	0x03, 0x61, 0x6c, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22,
-	0x18, 0x0a, 0x16, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
-	0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1a, 0x0a, 0x06, 0x49, 0x50, 0x4c,
-	0x69, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09,
-	0x52, 0x03, 0x69, 0x70, 0x73, 0x22, 0xf9, 0x01, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72,
-	0x6b, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49,
-	0x44, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
-	0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63,
-	0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63,
-	0x74, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x04,
-	0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x42, 0x0a,
-	0x0b, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x18, 0x05, 0x20, 0x03,
-	0x28, 0x0b, 0x32, 0x20, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77,
-	0x6f, 0x72, 0x6b, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45,
-	0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50,
-	0x73, 0x1a, 0x4e, 0x0a, 0x10, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73,
-	0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01,
-	0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
-	0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
-	0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38,
-	0x01, 0x22, 0x92, 0x01, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14,
-	0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x04,
-	0x70, 0x6f, 0x72, 0x74, 0x12, 0x2e, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20,
-	0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x6f, 0x72,
-	0x74, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x05, 0x72,
-	0x61, 0x6e, 0x67, 0x65, 0x1a, 0x2f, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a,
-	0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, 0x74,
-	0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d,
-	0x52, 0x03, 0x65, 0x6e, 0x64, 0x42, 0x0f, 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, 0x6c,
-	0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x80, 0x02, 0x0a, 0x0e, 0x46, 0x6f, 0x72, 0x77, 0x61,
-	0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f,
-	0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f,
-	0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x3a, 0x0a, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61,
-	0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10,
-	0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 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, 0x09, 0x52, 0x11, 0x74, 0x72,
-	0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12,
-	0x2e, 0x0a, 0x12, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73,
-	0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x74, 0x72, 0x61,
-	0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12,
-	0x38, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72,
-	0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e,
-	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, 0x22, 0x47, 0x0a, 0x17, 0x46, 0x6f, 0x72,
-	0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70,
-	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,
+	0x22, 0x57, 0x0a, 0x0f, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74,
+	0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74,
+	0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63,
+	0x74, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x52, 0x0a, 0x0a, 0x52, 0x65, 0x6c,
+	0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x49, 0x18, 0x01,
+	0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x49, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x76, 0x61,
+	0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x76,
+	0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72,
+	0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x72, 0x0a,
+	0x0c, 0x4e, 0x53, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a,
+	0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07,
+	0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69,
+	0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
+	0x73, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01,
+	0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65,
+	0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f,
+	0x72, 0x22, 0xef, 0x03, 0x0a, 0x0a, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
+	0x12, 0x41, 0x0a, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74,
+	0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x64, 0x61, 0x65, 0x6d,
+	0x6f, 0x6e, 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61,
+	0x74, 0x65, 0x52, 0x0f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74,
+	0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61,
+	0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
+	0x6e, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x73,
+	0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x6c, 0x6f,
+	0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01,
+	0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61,
+	0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61,
+	0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, 0x0a, 0x05, 0x70, 0x65,
+	0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d,
+	0x6f, 0x6e, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x70, 0x65,
+	0x65, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x18, 0x05, 0x20,
+	0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x6c,
+	0x61, 0x79, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x12,
+	0x35, 0x0a, 0x0b, 0x64, 0x6e, 0x73, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x06,
+	0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x53,
+	0x47, 0x72, 0x6f, 0x75, 0x70, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0a, 0x64, 0x6e, 0x73, 0x53,
+	0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x38, 0x0a, 0x17, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72,
+	0x4f, 0x66, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65,
+	0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x17, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x4f,
+	0x66, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73,
+	0x12, 0x2b, 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b,
+	0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d,
+	0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x34, 0x0a,
+	0x15, 0x6c, 0x61, 0x7a, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45,
+	0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x15, 0x6c, 0x61,
+	0x7a, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62,
+	0x6c, 0x65, 0x64, 0x22, 0x15, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f,
+	0x72, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x3f, 0x0a, 0x14, 0x4c, 0x69,
+	0x73, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
+	0x73, 0x65, 0x12, 0x27, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03,
+	0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77,
+	0x6f, 0x72, 0x6b, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x61, 0x0a, 0x15, 0x53,
+	0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x52, 0x65, 0x71,
+	0x75, 0x65, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49,
+	0x44, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
+	0x6b, 0x49, 0x44, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x18, 0x02,
+	0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x61, 0x70, 0x70, 0x65, 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03,
+	0x61, 0x6c, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x18,
+	0x0a, 0x16, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73,
+	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1a, 0x0a, 0x06, 0x49, 0x50, 0x4c, 0x69,
+	0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52,
+	0x03, 0x69, 0x70, 0x73, 0x22, 0xf9, 0x01, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
+	0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44,
+	0x12, 0x14, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
+	0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74,
+	0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74,
+	0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x04, 0x20,
+	0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x42, 0x0a, 0x0b,
+	0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28,
+	0x0b, 0x32, 0x20, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f,
+	0x72, 0x6b, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45, 0x6e,
+	0x74, 0x72, 0x79, 0x52, 0x0b, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73,
+	0x1a, 0x4e, 0x0a, 0x10, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x64, 0x49, 0x50, 0x73, 0x45,
+	0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28,
+	0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18,
+	0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x49,
+	0x50, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01,
+	0x22, 0x92, 0x01, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a,
+	0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x04, 0x70,
+	0x6f, 0x72, 0x74, 0x12, 0x2e, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01,
+	0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x6f, 0x72, 0x74,
+	0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x05, 0x72, 0x61,
+	0x6e, 0x67, 0x65, 0x1a, 0x2f, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05,
+	0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, 0x74, 0x61,
+	0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52,
+	0x03, 0x65, 0x6e, 0x64, 0x42, 0x0f, 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, 0x6c, 0x65,
+	0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x80, 0x02, 0x0a, 0x0e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72,
+	0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74,
+	0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74,
+	0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x3a, 0x0a, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74,
+	0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e,
+	0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 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, 0x09, 0x52, 0x11, 0x74, 0x72, 0x61,
+	0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x2e,
+	0x0a, 0x12, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74,
+	0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x74, 0x72, 0x61, 0x6e,
+	0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x38,
+	0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, 0x74,
+	0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 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, 0x22, 0x47, 0x0a, 0x17, 0x46, 0x6f, 0x72, 0x77,
+	0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 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, 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, 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,
+	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, 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,
+	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, 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, 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,
+	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 49e57785..a46ba554 100644
--- a/client/proto/daemon.proto
+++ b/client/proto/daemon.proto
@@ -94,7 +94,7 @@ message LoginRequest {
 
   bytes customDNSAddress = 7;
 
-  bool isLinuxDesktopClient = 8;
+  bool isUnixDesktopClient = 8;
 
   string hostname = 9;
 
@@ -122,7 +122,6 @@ message LoginRequest {
   optional bool disable_server_routes = 21;
   optional bool disable_dns = 22;
   optional bool disable_firewall = 23;
-
   optional bool block_lan_access = 24;
 
   optional bool disable_notifications = 25;
@@ -134,6 +133,9 @@ message LoginRequest {
   // omits initialized empty slices due to omitempty tags
   bool cleanDNSLabels = 27;
 
+  optional bool lazyConnectionEnabled = 28;
+
+  optional bool block_inbound = 29;
 }
 
 message LoginResponse {
@@ -201,6 +203,10 @@ message GetConfigResponse {
   bool rosenpassPermissive = 12;
 
   bool disable_notifications = 13;
+
+  bool lazyConnectionEnabled = 14;
+
+  bool blockInbound = 15;
 }
 
 // PeerState contains the latest state of a peer
@@ -274,6 +280,8 @@ message FullStatus {
   int32 NumberOfForwardingRules = 8;
 
   repeated SystemEvent events = 7;
+
+  bool lazyConnectionEnabled = 9;
 }
 
 // Networks
@@ -336,10 +344,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 9ccfb13f..7de3e860 100644
--- a/client/server/debug.go
+++ b/client/server/debug.go
@@ -4,16 +4,24 @@ package server
 
 import (
 	"context"
+	"crypto/sha256"
+	"encoding/json"
 	"errors"
 	"fmt"
+	"io"
+	"net/http"
+	"os"
 
 	log "github.com/sirupsen/logrus"
 
 	"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 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()
@@ -42,7 +50,104 @@ func (s *Server) DebugBundle(_ context.Context, req *proto.DebugBundleRequest) (
 		return nil, fmt.Errorf("generate debug bundle: %w", err)
 	}
 
-	return &proto.DebugBundleResponse{Path: path}, nil
+	if req.GetUploadURL() == "" {
+		return &proto.DebugBundleResponse{Path: path}, nil
+	}
+	key, err := uploadDebugBundle(context.Background(), req.GetUploadURL(), s.config.ManagementURL.String(), path)
+	if err != nil {
+		log.Errorf("failed to upload debug bundle to %s: %v", req.GetUploadURL(), err)
+		return &proto.DebugBundleResponse{Path: path, UploadFailureReason: err.Error()}, nil
+	}
+
+	log.Infof("debug bundle uploaded to %s with key %s", req.GetUploadURL(), key)
+
+	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 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 {
+		return nil, fmt.Errorf("create GET request: %w", err)
+	}
+
+	getReq.Header.Set(types.ClientHeader, types.ClientHeaderValue)
+
+	resp, err := http.DefaultClient.Do(getReq)
+	if err != nil {
+		return nil, fmt.Errorf("get presigned URL: %w", err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		body, _ := io.ReadAll(resp.Body)
+		return nil, fmt.Errorf("get presigned URL status %d: %s", resp.StatusCode, string(body))
+	}
+
+	urlBytes, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, fmt.Errorf("read response body: %w", err)
+	}
+	var response types.GetURLResponse
+	if err := json.Unmarshal(urlBytes, &response); err != nil {
+		return nil, fmt.Errorf("unmarshal response: %w", err)
+	}
+	return &response, nil
+}
+
+func getURLHash(url string) string {
+	return fmt.Sprintf("%x", sha256.Sum256([]byte(url)))
 }
 
 // GetLogLevel gets the current logging level for the server.
diff --git a/client/server/network.go b/client/server/network.go
index e0b01f76..93b7caa4 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/server/server.go b/client/server/server.go
index cba09a8b..2025a89e 100644
--- a/client/server/server.go
+++ b/client/server/server.go
@@ -139,6 +139,7 @@ func (s *Server) Start() error {
 
 	s.statusRecorder.UpdateManagementAddress(config.ManagementURL.String())
 	s.statusRecorder.UpdateRosenpass(config.RosenpassEnabled, config.RosenpassPermissive)
+	s.statusRecorder.UpdateLazyConnection(config.LazyConnectionEnabled)
 
 	if s.sessionWatcher == nil {
 		s.sessionWatcher = internal.NewSessionWatcher(s.rootCtx, s.statusRecorder)
@@ -397,11 +398,14 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
 		inputConfig.DisableFirewall = msg.DisableFirewall
 		s.latestConfigInput.DisableFirewall = msg.DisableFirewall
 	}
-
 	if msg.BlockLanAccess != nil {
 		inputConfig.BlockLANAccess = msg.BlockLanAccess
 		s.latestConfigInput.BlockLANAccess = msg.BlockLanAccess
 	}
+	if msg.BlockInbound != nil {
+		inputConfig.BlockInbound = msg.BlockInbound
+		s.latestConfigInput.BlockInbound = msg.BlockInbound
+	}
 
 	if msg.CleanDNSLabels {
 		inputConfig.DNSLabels = domain.List{}
@@ -417,6 +421,11 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
 		s.latestConfigInput.DisableNotifications = msg.DisableNotifications
 	}
 
+	if msg.LazyConnectionEnabled != nil {
+		inputConfig.LazyConnectionEnabled = msg.LazyConnectionEnabled
+		s.latestConfigInput.LazyConnectionEnabled = msg.LazyConnectionEnabled
+	}
+
 	s.mutex.Unlock()
 
 	if msg.OptionalPreSharedKey != nil {
@@ -446,7 +455,7 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
 	state.Set(internal.StatusConnecting)
 
 	if msg.SetupKey == "" {
-		oAuthFlow, err := auth.NewOAuthFlow(ctx, config, msg.IsLinuxDesktopClient)
+		oAuthFlow, err := auth.NewOAuthFlow(ctx, config, msg.IsUnixDesktopClient)
 		if err != nil {
 			state.Set(internal.StatusLoginFailed)
 			return nil, err
@@ -750,18 +759,20 @@ func (s *Server) GetConfig(_ context.Context, _ *proto.GetConfigRequest) (*proto
 	}
 
 	return &proto.GetConfigResponse{
-		ManagementUrl:        managementURL,
-		ConfigFile:           s.latestConfigInput.ConfigPath,
-		LogFile:              s.logFile,
-		PreSharedKey:         preSharedKey,
-		AdminURL:             adminURL,
-		InterfaceName:        s.config.WgIface,
-		WireguardPort:        int64(s.config.WgPort),
-		DisableAutoConnect:   s.config.DisableAutoConnect,
-		ServerSSHAllowed:     *s.config.ServerSSHAllowed,
-		RosenpassEnabled:     s.config.RosenpassEnabled,
-		RosenpassPermissive:  s.config.RosenpassPermissive,
-		DisableNotifications: disableNotifications,
+		ManagementUrl:         managementURL,
+		ConfigFile:            s.latestConfigInput.ConfigPath,
+		LogFile:               s.logFile,
+		PreSharedKey:          preSharedKey,
+		AdminURL:              adminURL,
+		InterfaceName:         s.config.WgIface,
+		WireguardPort:         int64(s.config.WgPort),
+		DisableAutoConnect:    s.config.DisableAutoConnect,
+		ServerSSHAllowed:      *s.config.ServerSSHAllowed,
+		RosenpassEnabled:      s.config.RosenpassEnabled,
+		RosenpassPermissive:   s.config.RosenpassPermissive,
+		LazyConnectionEnabled: s.config.LazyConnectionEnabled,
+		BlockInbound:          s.config.BlockInbound,
+		DisableNotifications:  disableNotifications,
 	}, nil
 }
 
@@ -804,6 +815,7 @@ func toProtoFullStatus(fullStatus peer.FullStatus) *proto.FullStatus {
 	pbFullStatus.LocalPeerState.RosenpassEnabled = fullStatus.RosenpassState.Enabled
 	pbFullStatus.LocalPeerState.Networks = maps.Keys(fullStatus.LocalPeerState.Routes)
 	pbFullStatus.NumberOfForwardingRules = int32(fullStatus.NumOfForwardingRules)
+	pbFullStatus.LazyConnectionEnabled = fullStatus.LazyConnectionEnabled
 
 	for _, peerState := range fullStatus.Peers {
 		pbPeerState := &proto.PeerState{
diff --git a/client/server/trace.go b/client/server/trace.go
index 8b9d375f..e4ac9148 100644
--- a/client/server/trace.go
+++ b/client/server/trace.go
@@ -3,11 +3,11 @@ package server
 import (
 	"context"
 	"fmt"
-	"net"
 	"net/netip"
 
 	fw "github.com/netbirdio/netbird/client/firewall/manager"
 	"github.com/netbirdio/netbird/client/firewall/uspfilter"
+	"github.com/netbirdio/netbird/client/internal"
 	"github.com/netbirdio/netbird/client/proto"
 )
 
@@ -19,98 +19,129 @@ func (s *Server) TracePacket(_ context.Context, req *proto.TracePacketRequest) (
 	s.mutex.Lock()
 	defer s.mutex.Unlock()
 
+	tracer, engine, err := s.getPacketTracer()
+	if err != nil {
+		return nil, err
+	}
+
+	srcAddr, err := s.parseAddress(req.GetSourceIp(), engine)
+	if err != nil {
+		return nil, fmt.Errorf("invalid source IP address: %w", err)
+	}
+
+	dstAddr, err := s.parseAddress(req.GetDestinationIp(), engine)
+	if err != nil {
+		return nil, fmt.Errorf("invalid destination IP address: %w", err)
+	}
+
+	protocol, err := s.parseProtocol(req.GetProtocol())
+	if err != nil {
+		return nil, err
+	}
+
+	direction, err := s.parseDirection(req.GetDirection())
+	if err != nil {
+		return nil, err
+	}
+
+	tcpState := s.parseTCPFlags(req.GetTcpFlags())
+
+	builder := &uspfilter.PacketBuilder{
+		SrcIP:     srcAddr,
+		DstIP:     dstAddr,
+		Protocol:  protocol,
+		SrcPort:   uint16(req.GetSourcePort()),
+		DstPort:   uint16(req.GetDestinationPort()),
+		Direction: direction,
+		TCPState:  tcpState,
+		ICMPType:  uint8(req.GetIcmpType()),
+		ICMPCode:  uint8(req.GetIcmpCode()),
+	}
+
+	trace, err := tracer.TracePacketFromBuilder(builder)
+	if err != nil {
+		return nil, fmt.Errorf("trace packet: %w", err)
+	}
+
+	return s.buildTraceResponse(trace), nil
+}
+
+func (s *Server) getPacketTracer() (packetTracer, *internal.Engine, error) {
 	if s.connectClient == nil {
-		return nil, fmt.Errorf("connect client not initialized")
+		return nil, nil, fmt.Errorf("connect client not initialized")
 	}
+
 	engine := s.connectClient.Engine()
 	if engine == nil {
-		return nil, fmt.Errorf("engine not initialized")
+		return nil, nil, fmt.Errorf("engine not initialized")
 	}
 
 	fwManager := engine.GetFirewallManager()
 	if fwManager == nil {
-		return nil, fmt.Errorf("firewall manager not initialized")
+		return nil, nil, fmt.Errorf("firewall manager not initialized")
 	}
 
 	tracer, ok := fwManager.(packetTracer)
 	if !ok {
-		return nil, fmt.Errorf("firewall manager does not support packet tracing")
+		return nil, nil, fmt.Errorf("firewall manager does not support packet tracing")
 	}
 
-	srcIP := net.ParseIP(req.GetSourceIp())
-	if req.GetSourceIp() == "self" {
-		srcIP = engine.GetWgAddr()
-	}
-
-	srcAddr, ok := netip.AddrFromSlice(srcIP)
-	if !ok {
-		return nil, fmt.Errorf("invalid source IP address")
-	}
+	return tracer, engine, nil
+}
 
-	dstIP := net.ParseIP(req.GetDestinationIp())
-	if req.GetDestinationIp() == "self" {
-		dstIP = engine.GetWgAddr()
+func (s *Server) parseAddress(addr string, engine *internal.Engine) (netip.Addr, error) {
+	if addr == "self" {
+		return engine.GetWgAddr(), nil
 	}
 
-	dstAddr, ok := netip.AddrFromSlice(dstIP)
-	if !ok {
-		return nil, fmt.Errorf("invalid source IP address")
+	a, err := netip.ParseAddr(addr)
+	if err != nil {
+		return netip.Addr{}, err
 	}
 
-	if srcIP == nil || dstIP == nil {
-		return nil, fmt.Errorf("invalid IP address")
-	}
+	return a.Unmap(), nil
+}
 
-	var tcpState *uspfilter.TCPState
-	if flags := req.GetTcpFlags(); flags != nil {
-		tcpState = &uspfilter.TCPState{
-			SYN: flags.GetSyn(),
-			ACK: flags.GetAck(),
-			FIN: flags.GetFin(),
-			RST: flags.GetRst(),
-			PSH: flags.GetPsh(),
-			URG: flags.GetUrg(),
-		}
+func (s *Server) parseProtocol(protocol string) (fw.Protocol, error) {
+	switch protocol {
+	case "tcp":
+		return fw.ProtocolTCP, nil
+	case "udp":
+		return fw.ProtocolUDP, nil
+	case "icmp":
+		return fw.ProtocolICMP, nil
+	default:
+		return "", fmt.Errorf("invalid protocol")
 	}
+}
 
-	var dir fw.RuleDirection
-	switch req.GetDirection() {
+func (s *Server) parseDirection(direction string) (fw.RuleDirection, error) {
+	switch direction {
 	case "in":
-		dir = fw.RuleDirectionIN
+		return fw.RuleDirectionIN, nil
 	case "out":
-		dir = fw.RuleDirectionOUT
+		return fw.RuleDirectionOUT, nil
 	default:
-		return nil, fmt.Errorf("invalid direction")
+		return 0, fmt.Errorf("invalid direction")
 	}
+}
 
-	var protocol fw.Protocol
-	switch req.GetProtocol() {
-	case "tcp":
-		protocol = fw.ProtocolTCP
-	case "udp":
-		protocol = fw.ProtocolUDP
-	case "icmp":
-		protocol = fw.ProtocolICMP
-	default:
-		return nil, fmt.Errorf("invalid protocolcol")
+func (s *Server) parseTCPFlags(flags *proto.TCPFlags) *uspfilter.TCPState {
+	if flags == nil {
+		return nil
 	}
 
-	builder := &uspfilter.PacketBuilder{
-		SrcIP:     srcAddr,
-		DstIP:     dstAddr,
-		Protocol:  protocol,
-		SrcPort:   uint16(req.GetSourcePort()),
-		DstPort:   uint16(req.GetDestinationPort()),
-		Direction: dir,
-		TCPState:  tcpState,
-		ICMPType:  uint8(req.GetIcmpType()),
-		ICMPCode:  uint8(req.GetIcmpCode()),
-	}
-	trace, err := tracer.TracePacketFromBuilder(builder)
-	if err != nil {
-		return nil, fmt.Errorf("trace packet: %w", err)
+	return &uspfilter.TCPState{
+		SYN: flags.GetSyn(),
+		ACK: flags.GetAck(),
+		FIN: flags.GetFin(),
+		RST: flags.GetRst(),
+		PSH: flags.GetPsh(),
+		URG: flags.GetUrg(),
 	}
+}
 
+func (s *Server) buildTraceResponse(trace *uspfilter.PacketTrace) *proto.TracePacketResponse {
 	resp := &proto.TracePacketResponse{}
 
 	for _, result := range trace.Results {
@@ -119,10 +150,12 @@ func (s *Server) TracePacket(_ context.Context, req *proto.TracePacketRequest) (
 			Message: result.Message,
 			Allowed: result.Allowed,
 		}
+
 		if result.ForwarderAction != nil {
 			details := fmt.Sprintf("%s to %s", result.ForwarderAction.Action, result.ForwarderAction.RemoteAddr)
 			stage.ForwardingDetails = &details
 		}
+
 		resp.Stages = append(resp.Stages, stage)
 	}
 
@@ -130,5 +163,5 @@ func (s *Server) TracePacket(_ context.Context, req *proto.TracePacketRequest) (
 		resp.FinalDisposition = trace.Results[len(trace.Results)-1].Allowed
 	}
 
-	return resp, nil
+	return resp
 }
diff --git a/client/status/status.go b/client/status/status.go
index 43acc919..18056e36 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"
 )
 
@@ -96,6 +97,7 @@ type OutputOverview struct {
 	NumberOfForwardingRules int                        `json:"forwardingRules" yaml:"forwardingRules"`
 	NSServerGroups          []NsServerGroupStateOutput `json:"dnsServers" yaml:"dnsServers"`
 	Events                  []SystemEventOutput        `json:"events" yaml:"events"`
+	LazyConnectionEnabled   bool                       `json:"lazyConnectionEnabled" yaml:"lazyConnectionEnabled"`
 }
 
 func ConvertToStatusOutputOverview(resp *proto.StatusResponse, anon bool, statusFilter string, prefixNamesFilter []string, prefixNamesFilterMap map[string]struct{}, ipsFilter map[string]struct{}) OutputOverview {
@@ -135,6 +137,7 @@ func ConvertToStatusOutputOverview(resp *proto.StatusResponse, anon bool, status
 		NumberOfForwardingRules: int(pbFullStatus.GetNumberOfForwardingRules()),
 		NSServerGroups:          mapNSGroups(pbFullStatus.GetDnsServers()),
 		Events:                  mapEvents(pbFullStatus.GetEvents()),
+		LazyConnectionEnabled:   pbFullStatus.GetLazyConnectionEnabled(),
 	}
 
 	if anon {
@@ -205,7 +208,7 @@ func mapPeers(
 		transferSent := int64(0)
 
 		isPeerConnected := pbPeerState.ConnStatus == peer.StatusConnected.String()
-		if skipDetailByFilters(pbPeerState, isPeerConnected, statusFilter, prefixNamesFilter, prefixNamesFilterMap, ipsFilter) {
+		if skipDetailByFilters(pbPeerState, pbPeerState.ConnStatus, statusFilter, prefixNamesFilter, prefixNamesFilterMap, ipsFilter) {
 			continue
 		}
 		if isPeerConnected {
@@ -383,6 +386,11 @@ func ParseGeneralSummary(overview OutputOverview, showURL bool, showRelays bool,
 		}
 	}
 
+	lazyConnectionEnabledStatus := "false"
+	if overview.LazyConnectionEnabled {
+		lazyConnectionEnabledStatus = "true"
+	}
+
 	peersCountString := fmt.Sprintf("%d/%d Connected", overview.Peers.Connected, overview.Peers.Total)
 
 	goos := runtime.GOOS
@@ -404,6 +412,7 @@ func ParseGeneralSummary(overview OutputOverview, showURL bool, showRelays bool,
 			"NetBird IP: %s\n"+
 			"Interface type: %s\n"+
 			"Quantum resistance: %s\n"+
+			"Lazy connection: %s\n"+
 			"Networks: %s\n"+
 			"Forwarding rules: %d\n"+
 			"Peers count: %s\n",
@@ -414,10 +423,11 @@ func ParseGeneralSummary(overview OutputOverview, showURL bool, showRelays bool,
 		signalConnString,
 		relaysString,
 		dnsServersString,
-		overview.FQDN,
+		domain.Domain(overview.FQDN).SafeString(),
 		interfaceIP,
 		interfaceTypeString,
 		rosenpassEnabledStatus,
+		lazyConnectionEnabledStatus,
 		networks,
 		overview.NumberOfForwardingRules,
 		peersCountString,
@@ -508,7 +518,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,
@@ -532,23 +542,13 @@ func parsePeers(peers PeersStateOutput, rosenpassEnabled, rosenpassPermissive bo
 	return peersString
 }
 
-func skipDetailByFilters(
-	peerState *proto.PeerState,
-	isConnected bool,
-	statusFilter string,
-	prefixNamesFilter []string,
-	prefixNamesFilterMap map[string]struct{},
-	ipsFilter map[string]struct{},
-) bool {
+func skipDetailByFilters(peerState *proto.PeerState, peerStatus string, statusFilter string, prefixNamesFilter []string, prefixNamesFilterMap map[string]struct{}, ipsFilter map[string]struct{}) bool {
 	statusEval := false
 	ipEval := false
 	nameEval := true
 
 	if statusFilter != "" {
-		lowerStatusFilter := strings.ToLower(statusFilter)
-		if lowerStatusFilter == "disconnected" && isConnected {
-			statusEval = true
-		} else if lowerStatusFilter == "connected" && !isConnected {
+		if !strings.EqualFold(peerStatus, statusFilter) {
 			statusEval = true
 		}
 	}
diff --git a/client/system/info.go b/client/system/info.go
index 2a0343ca..3a0c5715 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 fabe65a6..f178ec93 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 00000000..faa3e700
--- /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 b2a6404b..f0202b8e 100644
--- a/client/ui/client_ui.go
+++ b/client/ui/client_ui.go
@@ -51,14 +51,19 @@ const (
 )
 
 func main() {
-	daemonAddr, showSettings, showNetworks, errorMsg, saveLogsInFile := parseFlags()
+	daemonAddr, showSettings, showNetworks, showDebug, errorMsg, saveLogsInFile := parseFlags()
 
 	// Initialize file logging if needed.
+	var logFile string
 	if saveLogsInFile {
-		if err := initLogFile(); err != nil {
+		file, err := initLogFile()
+		if err != nil {
 			log.Errorf("error while initializing log: %v", err)
 			return
 		}
+		logFile = file
+	} else {
+		_ = util.InitLog("trace", "console")
 	}
 
 	// Create the Fyne application.
@@ -72,25 +77,25 @@ func main() {
 	}
 
 	// Create the service client (this also builds the settings or networks UI if requested).
-	client := newServiceClient(daemonAddr, a, showSettings, showNetworks)
+	client := newServiceClient(daemonAddr, logFile, a, showSettings, showNetworks, showDebug)
 
 	// Watch for theme/settings changes to update the icon.
 	go watchSettingsChanges(a, client)
 
 	// Run in window mode if any UI flag was set.
-	if showSettings || showNetworks {
+	if showSettings || showNetworks || showDebug {
 		a.Run()
 		return
 	}
 
 	// Check for another running process.
-	running, err := process.IsAnotherProcessRunning()
+	pid, running, err := process.IsAnotherProcessRunning()
 	if err != nil {
 		log.Errorf("error while checking process: %v", err)
 		return
 	}
 	if running {
-		log.Warn("another process is running")
+		log.Warnf("another process is running with pid %d, exiting", pid)
 		return
 	}
 
@@ -99,7 +104,7 @@ func main() {
 }
 
 // parseFlags reads and returns all needed command-line flags.
-func parseFlags() (daemonAddr string, showSettings, showNetworks bool, errorMsg string, saveLogsInFile bool) {
+func parseFlags() (daemonAddr string, showSettings, showNetworks, showDebug bool, errorMsg string, saveLogsInFile bool) {
 	defaultDaemonAddr := "unix:///var/run/netbird.sock"
 	if runtime.GOOS == "windows" {
 		defaultDaemonAddr = "tcp://127.0.0.1:41731"
@@ -107,25 +112,17 @@ func parseFlags() (daemonAddr string, showSettings, showNetworks bool, errorMsg
 	flag.StringVar(&daemonAddr, "daemon-addr", defaultDaemonAddr, "Daemon service address to serve CLI requests [unix|tcp]://[path|host:port]")
 	flag.BoolVar(&showSettings, "settings", false, "run settings window")
 	flag.BoolVar(&showNetworks, "networks", false, "run networks window")
+	flag.BoolVar(&showDebug, "debug", false, "run debug window")
 	flag.StringVar(&errorMsg, "error-msg", "", "displays an error message window")
-
-	tmpDir := "/tmp"
-	if runtime.GOOS == "windows" {
-		tmpDir = os.TempDir()
-	}
-	flag.BoolVar(&saveLogsInFile, "use-log-file", false, fmt.Sprintf("save logs in a file: %s/netbird-ui-PID.log", tmpDir))
+	flag.BoolVar(&saveLogsInFile, "use-log-file", false, fmt.Sprintf("save logs in a file: %s/netbird-ui-PID.log", os.TempDir()))
 	flag.Parse()
 	return
 }
 
 // initLogFile initializes logging into a file.
-func initLogFile() error {
-	tmpDir := "/tmp"
-	if runtime.GOOS == "windows" {
-		tmpDir = os.TempDir()
-	}
-	logFile := path.Join(tmpDir, fmt.Sprintf("netbird-ui-%d.log", os.Getpid()))
-	return util.InitLog("trace", logFile)
+func initLogFile() (string, error) {
+	logFile := path.Join(os.TempDir(), fmt.Sprintf("netbird-ui-%d.log", os.Getpid()))
+	return logFile, util.InitLog("trace", logFile)
 }
 
 // watchSettingsChanges listens for Fyne theme/settings changes and updates the client icon.
@@ -168,9 +165,10 @@ var iconConnectingMacOS []byte
 var iconErrorMacOS []byte
 
 type serviceClient struct {
-	ctx  context.Context
-	addr string
-	conn proto.DaemonServiceClient
+	ctx    context.Context
+	cancel context.CancelFunc
+	addr   string
+	conn   proto.DaemonServiceClient
 
 	icAbout              []byte
 	icConnected          []byte
@@ -195,6 +193,8 @@ type serviceClient struct {
 	mAllowSSH          *systray.MenuItem
 	mAutoConnect       *systray.MenuItem
 	mEnableRosenpass   *systray.MenuItem
+	mLazyConnEnabled   *systray.MenuItem
+	mBlockInbound      *systray.MenuItem
 	mNotifications     *systray.MenuItem
 	mAdvancedSettings  *systray.MenuItem
 	mCreateDebugBundle *systray.MenuItem
@@ -231,13 +231,16 @@ type serviceClient struct {
 	daemonVersion        string
 	updateIndicationLock sync.Mutex
 	isUpdateIconActive   bool
-	showRoutes           bool
-	wRoutes              fyne.Window
+	showNetworks         bool
+	wNetworks            fyne.Window
 
 	eventManager *event.Manager
 
-	exitNodeMu     sync.Mutex
-	mExitNodeItems []menuHandler
+	exitNodeMu           sync.Mutex
+	mExitNodeItems       []menuHandler
+	exitNodeStates       []exitNodeState
+	mExitNodeDeselectAll *systray.MenuItem
+	logFile              string
 }
 
 type menuHandler struct {
@@ -248,25 +251,30 @@ type menuHandler struct {
 // newServiceClient instance constructor
 //
 // This constructor also builds the UI elements for the settings window.
-func newServiceClient(addr string, a fyne.App, showSettings bool, showRoutes bool) *serviceClient {
+func newServiceClient(addr string, logFile string, a fyne.App, showSettings bool, showNetworks bool, showDebug bool) *serviceClient {
+	ctx, cancel := context.WithCancel(context.Background())
 	s := &serviceClient{
-		ctx:              context.Background(),
+		ctx:              ctx,
+		cancel:           cancel,
 		addr:             addr,
 		app:              a,
+		logFile:          logFile,
 		sendNotification: false,
 
 		showAdvancedSettings: showSettings,
-		showRoutes:           showRoutes,
+		showNetworks:         showNetworks,
 		update:               version.NewUpdate(),
 	}
 
 	s.setNewIcons()
 
-	if showSettings {
+	switch {
+	case showSettings:
 		s.showSettingsUI()
-		return s
-	} else if showRoutes {
+	case showNetworks:
 		s.showNetworksUI()
+	case showDebug:
+		s.showDebugUI()
 	}
 
 	return s
@@ -313,6 +321,8 @@ func (s *serviceClient) updateIcon() {
 func (s *serviceClient) showSettingsUI() {
 	// add settings window UI elements.
 	s.wSettings = s.app.NewWindow("NetBird Settings")
+	s.wSettings.SetOnClosed(s.cancel)
+
 	s.iMngURL = widget.NewEntry()
 	s.iAdminURL = widget.NewEntry()
 	s.iConfigFile = widget.NewEntry()
@@ -378,12 +388,12 @@ func (s *serviceClient) getSettingsForm() *widget.Form {
 				s.adminURL = iAdminURL
 
 				loginRequest := proto.LoginRequest{
-					ManagementUrl:        iMngURL,
-					AdminURL:             iAdminURL,
-					IsLinuxDesktopClient: runtime.GOOS == "linux",
-					RosenpassPermissive:  &s.sRosenpassPermissive.Checked,
-					InterfaceName:        &s.iInterfaceName.Text,
-					WireguardPort:        &port,
+					ManagementUrl:       iMngURL,
+					AdminURL:            iAdminURL,
+					IsUnixDesktopClient: runtime.GOOS == "linux" || runtime.GOOS == "freebsd",
+					RosenpassPermissive: &s.sRosenpassPermissive.Checked,
+					InterfaceName:       &s.iInterfaceName.Text,
+					WireguardPort:       &port,
 				}
 
 				if s.iPreSharedKey.Text != censoredPreSharedKey {
@@ -410,7 +420,7 @@ func (s *serviceClient) login() error {
 	}
 
 	loginResp, err := conn.Login(s.ctx, &proto.LoginRequest{
-		IsLinuxDesktopClient: runtime.GOOS == "linux",
+		IsUnixDesktopClient: runtime.GOOS == "linux" || runtime.GOOS == "freebsd",
 	})
 	if err != nil {
 		log.Errorf("login to management URL with: %v", err)
@@ -457,7 +467,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 +492,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 +530,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 +547,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 +608,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")
@@ -611,6 +636,8 @@ func (s *serviceClient) onTrayReady() {
 	s.mAllowSSH = s.mSettings.AddSubMenuItemCheckbox("Allow SSH", allowSSHMenuDescr, false)
 	s.mAutoConnect = s.mSettings.AddSubMenuItemCheckbox("Connect on Startup", autoConnectMenuDescr, false)
 	s.mEnableRosenpass = s.mSettings.AddSubMenuItemCheckbox("Enable Quantum-Resistance", quantumResistanceMenuDescr, false)
+	s.mLazyConnEnabled = s.mSettings.AddSubMenuItemCheckbox("Enable Lazy Connections", lazyConnMenuDescr, false)
+	s.mBlockInbound = s.mSettings.AddSubMenuItemCheckbox("Block Inbound Connections", blockInboundMenuDescr, false)
 	s.mNotifications = s.mSettings.AddSubMenuItemCheckbox("Notifications", notificationsMenuDescr, false)
 	s.mAdvancedSettings = s.mSettings.AddSubMenuItem("Advanced Settings", advancedSettingsMenuDescr)
 	s.mCreateDebugBundle = s.mSettings.AddSubMenuItem("Create Debug Bundle", debugBundleMenuDescr)
@@ -670,111 +697,129 @@ func (s *serviceClient) onTrayReady() {
 
 	go s.eventManager.Start(s.ctx)
 
-	go func() {
-		for {
-			select {
-			case <-s.mUp.ClickedCh:
-				s.mUp.Disable()
-				go func() {
-					defer s.mUp.Enable()
-					err := s.menuUpClick()
-					if err != nil {
-						s.app.SendNotification(fyne.NewNotification("Error", "Failed to connect to NetBird service"))
-						return
-					}
-				}()
-			case <-s.mDown.ClickedCh:
-				s.mDown.Disable()
-				go func() {
-					defer s.mDown.Enable()
-					err := s.menuDownClick()
-					if err != nil {
-						s.app.SendNotification(fyne.NewNotification("Error", "Failed to connect to NetBird service"))
-						return
-					}
-				}()
-			case <-s.mAllowSSH.ClickedCh:
-				if s.mAllowSSH.Checked() {
-					s.mAllowSSH.Uncheck()
-				} else {
-					s.mAllowSSH.Check()
-				}
-				if err := s.updateConfig(); err != nil {
-					log.Errorf("failed to update config: %v", err)
-				}
-			case <-s.mAutoConnect.ClickedCh:
-				if s.mAutoConnect.Checked() {
-					s.mAutoConnect.Uncheck()
-				} else {
-					s.mAutoConnect.Check()
-				}
-				if err := s.updateConfig(); err != nil {
-					log.Errorf("failed to update config: %v", err)
-				}
-			case <-s.mEnableRosenpass.ClickedCh:
-				if s.mEnableRosenpass.Checked() {
-					s.mEnableRosenpass.Uncheck()
-				} else {
-					s.mEnableRosenpass.Check()
-				}
-				if err := s.updateConfig(); err != nil {
-					log.Errorf("failed to update config: %v", err)
-				}
-			case <-s.mAdvancedSettings.ClickedCh:
-				s.mAdvancedSettings.Disable()
-				go func() {
-					defer s.mAdvancedSettings.Enable()
-					defer s.getSrvConfig()
-					s.runSelfCommand("settings", "true")
-				}()
-			case <-s.mCreateDebugBundle.ClickedCh:
-				go func() {
-					if err := s.createAndOpenDebugBundle(); err != nil {
-						log.Errorf("Failed to create debug bundle: %v", err)
-						s.app.SendNotification(fyne.NewNotification("Error", "Failed to create debug bundle"))
-					}
-				}()
-			case <-s.mQuit.ClickedCh:
-				systray.Quit()
-				return
-			case <-s.mGitHub.ClickedCh:
-				err := openURL("https://github.com/netbirdio/netbird")
+	go s.listenEvents()
+}
+
+func (s *serviceClient) listenEvents() {
+	for {
+		select {
+		case <-s.mUp.ClickedCh:
+			s.mUp.Disable()
+			go func() {
+				defer s.mUp.Enable()
+				err := s.menuUpClick()
 				if err != nil {
-					log.Errorf("%s", err)
+					s.app.SendNotification(fyne.NewNotification("Error", "Failed to connect to NetBird service"))
+					return
 				}
-			case <-s.mUpdate.ClickedCh:
-				err := openURL(version.DownloadUrl())
+			}()
+		case <-s.mDown.ClickedCh:
+			s.mDown.Disable()
+			go func() {
+				defer s.mDown.Enable()
+				err := s.menuDownClick()
 				if err != nil {
-					log.Errorf("%s", err)
-				}
-			case <-s.mNetworks.ClickedCh:
-				s.mNetworks.Disable()
-				go func() {
-					defer s.mNetworks.Enable()
-					s.runSelfCommand("networks", "true")
-				}()
-			case <-s.mNotifications.ClickedCh:
-				if s.mNotifications.Checked() {
-					s.mNotifications.Uncheck()
-				} else {
-					s.mNotifications.Check()
-				}
-				if s.eventManager != nil {
-					s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked())
-				}
-				if err := s.updateConfig(); err != nil {
-					log.Errorf("failed to update config: %v", err)
+					s.app.SendNotification(fyne.NewNotification("Error", "Failed to connect to NetBird service"))
+					return
 				}
+			}()
+		case <-s.mAllowSSH.ClickedCh:
+			if s.mAllowSSH.Checked() {
+				s.mAllowSSH.Uncheck()
+			} else {
+				s.mAllowSSH.Check()
+			}
+			if err := s.updateConfig(); err != nil {
+				log.Errorf("failed to update config: %v", err)
+			}
+		case <-s.mAutoConnect.ClickedCh:
+			if s.mAutoConnect.Checked() {
+				s.mAutoConnect.Uncheck()
+			} else {
+				s.mAutoConnect.Check()
+			}
+			if err := s.updateConfig(); err != nil {
+				log.Errorf("failed to update config: %v", err)
+			}
+		case <-s.mEnableRosenpass.ClickedCh:
+			if s.mEnableRosenpass.Checked() {
+				s.mEnableRosenpass.Uncheck()
+			} else {
+				s.mEnableRosenpass.Check()
+			}
+			if err := s.updateConfig(); err != nil {
+				log.Errorf("failed to update config: %v", err)
+			}
+		case <-s.mLazyConnEnabled.ClickedCh:
+			if s.mLazyConnEnabled.Checked() {
+				s.mLazyConnEnabled.Uncheck()
+			} else {
+				s.mLazyConnEnabled.Check()
+			}
+			if err := s.updateConfig(); err != nil {
+				log.Errorf("failed to update config: %v", err)
+			}
+		case <-s.mBlockInbound.ClickedCh:
+			if s.mBlockInbound.Checked() {
+				s.mBlockInbound.Uncheck()
+			} else {
+				s.mBlockInbound.Check()
+			}
+			if err := s.updateConfig(); err != nil {
+				log.Errorf("failed to update config: %v", err)
+			}
+		case <-s.mAdvancedSettings.ClickedCh:
+			s.mAdvancedSettings.Disable()
+			go func() {
+				defer s.mAdvancedSettings.Enable()
+				defer s.getSrvConfig()
+				s.runSelfCommand("settings", "true")
+			}()
+		case <-s.mCreateDebugBundle.ClickedCh:
+			s.mCreateDebugBundle.Disable()
+			go func() {
+				defer s.mCreateDebugBundle.Enable()
+				s.runSelfCommand("debug", "true")
+			}()
+		case <-s.mQuit.ClickedCh:
+			systray.Quit()
+			return
+		case <-s.mGitHub.ClickedCh:
+			err := openURL("https://github.com/netbirdio/netbird")
+			if err != nil {
+				log.Errorf("%s", err)
+			}
+		case <-s.mUpdate.ClickedCh:
+			err := openURL(version.DownloadUrl())
+			if err != nil {
+				log.Errorf("%s", err)
+			}
+		case <-s.mNetworks.ClickedCh:
+			s.mNetworks.Disable()
+			go func() {
+				defer s.mNetworks.Enable()
+				s.runSelfCommand("networks", "true")
+			}()
+		case <-s.mNotifications.ClickedCh:
+			if s.mNotifications.Checked() {
+				s.mNotifications.Uncheck()
+			} else {
+				s.mNotifications.Check()
+			}
+			if s.eventManager != nil {
+				s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked())
+			}
+			if err := s.updateConfig(); err != nil {
+				log.Errorf("failed to update config: %v", err)
 			}
-
 		}
-	}()
+	}
 }
 
 func (s *serviceClient) runSelfCommand(command, arg string) {
 	proc, err := os.Executable()
 	if err != nil {
-		log.Errorf("show %s failed with error: %v", command, err)
+		log.Errorf("Error getting executable path: %v", err)
 		return
 	}
 
@@ -783,14 +828,48 @@ func (s *serviceClient) runSelfCommand(command, arg string) {
 		fmt.Sprintf("--daemon-addr=%s", s.addr),
 	)
 
-	out, err := cmd.CombinedOutput()
-	if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
-		log.Errorf("start %s UI: %v, %s", command, err, string(out))
+	if out := s.attachOutput(cmd); out != nil {
+		defer func() {
+			if err := out.Close(); err != nil {
+				log.Errorf("Error closing log file %s: %v", s.logFile, err)
+			}
+		}()
+	}
+
+	log.Printf("Running command: %s --%s=%s --daemon-addr=%s", proc, command, arg, s.addr)
+
+	err = cmd.Run()
+
+	if err != nil {
+		var exitErr *exec.ExitError
+		if errors.As(err, &exitErr) {
+			log.Printf("Command '%s %s' failed with exit code %d", command, arg, exitErr.ExitCode())
+		} else {
+			log.Printf("Failed to start/run command '%s %s': %v", command, arg, err)
+		}
 		return
 	}
-	if len(out) != 0 {
-		log.Infof("command %s executed: %s", command, string(out))
+
+	log.Printf("Command '%s %s' completed successfully.", command, arg)
+}
+
+func (s *serviceClient) attachOutput(cmd *exec.Cmd) *os.File {
+	if s.logFile == "" {
+		// attach child's streams to parent's streams
+		cmd.Stdout = os.Stdout
+		cmd.Stderr = os.Stderr
+
+		return nil
+	}
+
+	out, err := os.OpenFile(s.logFile, os.O_WRONLY|os.O_APPEND, 0)
+	if err != nil {
+		log.Errorf("Failed to open log file %s: %v", s.logFile, err)
+		return nil
 	}
+	cmd.Stdout = out
+	cmd.Stderr = out
+	return out
 }
 
 func normalizedVersion(version string) string {
@@ -803,9 +882,7 @@ func normalizedVersion(version string) string {
 
 // onTrayExit is called when the tray icon is closed.
 func (s *serviceClient) onTrayExit() {
-	for _, item := range s.mExitNodeItems {
-		item.cancel()
-	}
+	s.cancel()
 }
 
 // getSrvClient connection to the service.
@@ -814,7 +891,7 @@ func (s *serviceClient) getSrvClient(timeout time.Duration) (proto.DaemonService
 		return s.conn, nil
 	}
 
-	ctx, cancel := context.WithTimeout(context.Background(), timeout)
+	ctx, cancel := context.WithTimeout(s.ctx, timeout)
 	defer cancel()
 
 	conn, err := grpc.DialContext(
@@ -951,6 +1028,18 @@ func (s *serviceClient) loadSettings() {
 		s.mEnableRosenpass.Uncheck()
 	}
 
+	if cfg.LazyConnectionEnabled {
+		s.mLazyConnEnabled.Check()
+	} else {
+		s.mLazyConnEnabled.Uncheck()
+	}
+
+	if cfg.BlockInbound {
+		s.mBlockInbound.Check()
+	} else {
+		s.mBlockInbound.Uncheck()
+	}
+
 	if cfg.DisableNotifications {
 		s.mNotifications.Uncheck()
 	} else {
@@ -967,14 +1056,18 @@ func (s *serviceClient) updateConfig() error {
 	disableAutoStart := !s.mAutoConnect.Checked()
 	sshAllowed := s.mAllowSSH.Checked()
 	rosenpassEnabled := s.mEnableRosenpass.Checked()
+	lazyConnectionEnabled := s.mLazyConnEnabled.Checked()
+	blockInbound := s.mBlockInbound.Checked()
 	notificationsDisabled := !s.mNotifications.Checked()
 
 	loginRequest := proto.LoginRequest{
-		IsLinuxDesktopClient: runtime.GOOS == "linux",
-		ServerSSHAllowed:     &sshAllowed,
-		RosenpassEnabled:     &rosenpassEnabled,
-		DisableAutoConnect:   &disableAutoStart,
-		DisableNotifications: &notificationsDisabled,
+		IsUnixDesktopClient:   runtime.GOOS == "linux" || runtime.GOOS == "freebsd",
+		ServerSSHAllowed:      &sshAllowed,
+		RosenpassEnabled:      &rosenpassEnabled,
+		DisableAutoConnect:    &disableAutoStart,
+		DisableNotifications:  &notificationsDisabled,
+		LazyConnectionEnabled: &lazyConnectionEnabled,
+		BlockInbound:          &blockInbound,
 	}
 
 	if err := s.restartClient(&loginRequest); err != nil {
diff --git a/client/ui/const.go b/client/ui/const.go
index 0253750d..5a4b27f3 100644
--- a/client/ui/const.go
+++ b/client/ui/const.go
@@ -5,6 +5,8 @@ const (
 	allowSSHMenuDescr          = "Allow SSH connections"
 	autoConnectMenuDescr       = "Connect automatically when the service starts"
 	quantumResistanceMenuDescr = "Enable post-quantum security via Rosenpass"
+	lazyConnMenuDescr          = "[Experimental] Enable lazy connections"
+	blockInboundMenuDescr      = "Block inbound connections to the local machine and routed networks"
 	notificationsMenuDescr     = "Enable notifications"
 	advancedSettingsMenuDescr  = "Advanced settings of the application"
 	debugBundleMenuDescr       = "Create and open debug information bundle"
diff --git a/client/ui/debug.go b/client/ui/debug.go
index 845ea284..ab7dba37 100644
--- a/client/ui/debug.go
+++ b/client/ui/debug.go
@@ -3,48 +3,721 @@
 package main
 
 import (
+	"context"
 	"fmt"
 	"path/filepath"
+	"strconv"
+	"sync"
+	"time"
 
 	"fyne.io/fyne/v2"
+	"fyne.io/fyne/v2/container"
+	"fyne.io/fyne/v2/dialog"
+	"fyne.io/fyne/v2/widget"
+	log "github.com/sirupsen/logrus"
 	"github.com/skratchdot/open-golang/open"
 
+	"github.com/netbirdio/netbird/client/internal"
 	"github.com/netbirdio/netbird/client/proto"
 	nbstatus "github.com/netbirdio/netbird/client/status"
+	uptypes "github.com/netbirdio/netbird/upload-server/types"
 )
 
-func (s *serviceClient) createAndOpenDebugBundle() error {
+// Initial state for the debug collection
+type debugInitialState struct {
+	wasDown      bool
+	logLevel     proto.LogLevel
+	isLevelTrace bool
+}
+
+// Debug collection parameters
+type debugCollectionParams struct {
+	duration          time.Duration
+	anonymize         bool
+	systemInfo        bool
+	upload            bool
+	uploadURL         string
+	enablePersistence bool
+}
+
+// UI components for progress tracking
+type progressUI struct {
+	statusLabel *widget.Label
+	progressBar *widget.ProgressBar
+	uiControls  []fyne.Disableable
+	window      fyne.Window
+}
+
+func (s *serviceClient) showDebugUI() {
+	w := s.app.NewWindow("NetBird Debug")
+	w.SetOnClosed(s.cancel)
+
+	w.Resize(fyne.NewSize(600, 500))
+	w.SetFixedSize(true)
+
+	anonymizeCheck := widget.NewCheck("Anonymize sensitive information (public IPs, domains, ...)", nil)
+	systemInfoCheck := widget.NewCheck("Include system information (routes, interfaces, ...)", nil)
+	systemInfoCheck.SetChecked(true)
+	uploadCheck := widget.NewCheck("Upload bundle automatically after creation", nil)
+	uploadCheck.SetChecked(true)
+
+	uploadURLLabel := widget.NewLabel("Debug upload URL:")
+	uploadURL := widget.NewEntry()
+	uploadURL.SetText(uptypes.DefaultBundleURL)
+	uploadURL.SetPlaceHolder("Enter upload URL")
+
+	uploadURLContainer := container.NewVBox(
+		uploadURLLabel,
+		uploadURL,
+	)
+
+	uploadCheck.OnChanged = func(checked bool) {
+		if checked {
+			uploadURLContainer.Show()
+		} else {
+			uploadURLContainer.Hide()
+		}
+	}
+
+	debugModeContainer := container.NewHBox()
+	runForDurationCheck := widget.NewCheck("Run with trace logs before creating bundle", nil)
+	runForDurationCheck.SetChecked(true)
+
+	forLabel := widget.NewLabel("for")
+
+	durationInput := widget.NewEntry()
+	durationInput.SetText("1")
+	minutesLabel := widget.NewLabel("minute")
+	durationInput.Validator = func(s string) error {
+		return validateMinute(s, minutesLabel)
+	}
+
+	noteLabel := widget.NewLabel("Note: NetBird will be brought up and down during collection")
+
+	runForDurationCheck.OnChanged = func(checked bool) {
+		if checked {
+			forLabel.Show()
+			durationInput.Show()
+			minutesLabel.Show()
+			noteLabel.Show()
+		} else {
+			forLabel.Hide()
+			durationInput.Hide()
+			minutesLabel.Hide()
+			noteLabel.Hide()
+		}
+	}
+
+	debugModeContainer.Add(runForDurationCheck)
+	debugModeContainer.Add(forLabel)
+	debugModeContainer.Add(durationInput)
+	debugModeContainer.Add(minutesLabel)
+
+	statusLabel := widget.NewLabel("")
+	statusLabel.Hide()
+
+	progressBar := widget.NewProgressBar()
+	progressBar.Hide()
+
+	createButton := widget.NewButton("Create Debug Bundle", nil)
+
+	// UI controls that should be disabled during debug collection
+	uiControls := []fyne.Disableable{
+		anonymizeCheck,
+		systemInfoCheck,
+		uploadCheck,
+		uploadURL,
+		runForDurationCheck,
+		durationInput,
+		createButton,
+	}
+
+	createButton.OnTapped = s.getCreateHandler(
+		statusLabel,
+		progressBar,
+		uploadCheck,
+		uploadURL,
+		anonymizeCheck,
+		systemInfoCheck,
+		runForDurationCheck,
+		durationInput,
+		uiControls,
+		w,
+	)
+
+	content := container.NewVBox(
+		widget.NewLabel("Create a debug bundle to help troubleshoot issues with NetBird"),
+		widget.NewLabel(""),
+		anonymizeCheck,
+		systemInfoCheck,
+		uploadCheck,
+		uploadURLContainer,
+		widget.NewLabel(""),
+		debugModeContainer,
+		noteLabel,
+		widget.NewLabel(""),
+		statusLabel,
+		progressBar,
+		createButton,
+	)
+
+	paddedContent := container.NewPadded(content)
+	w.SetContent(paddedContent)
+
+	w.Show()
+}
+
+func validateMinute(s string, minutesLabel *widget.Label) error {
+	if val, err := strconv.Atoi(s); err != nil || val < 1 {
+		return fmt.Errorf("must be a number ≥ 1")
+	}
+	if s == "1" {
+		minutesLabel.SetText("minute")
+	} else {
+		minutesLabel.SetText("minutes")
+	}
+	return nil
+}
+
+// disableUIControls disables the provided UI controls
+func disableUIControls(controls []fyne.Disableable) {
+	for _, control := range controls {
+		control.Disable()
+	}
+}
+
+// enableUIControls enables the provided UI controls
+func enableUIControls(controls []fyne.Disableable) {
+	for _, control := range controls {
+		control.Enable()
+	}
+}
+
+func (s *serviceClient) getCreateHandler(
+	statusLabel *widget.Label,
+	progressBar *widget.ProgressBar,
+	uploadCheck *widget.Check,
+	uploadURL *widget.Entry,
+	anonymizeCheck *widget.Check,
+	systemInfoCheck *widget.Check,
+	runForDurationCheck *widget.Check,
+	duration *widget.Entry,
+	uiControls []fyne.Disableable,
+	w fyne.Window,
+) func() {
+	return func() {
+		disableUIControls(uiControls)
+		statusLabel.Show()
+
+		var url string
+		if uploadCheck.Checked {
+			url = uploadURL.Text
+			if url == "" {
+				statusLabel.SetText("Error: Upload URL is required when upload is enabled")
+				enableUIControls(uiControls)
+				return
+			}
+		}
+
+		params := &debugCollectionParams{
+			anonymize:         anonymizeCheck.Checked,
+			systemInfo:        systemInfoCheck.Checked,
+			upload:            uploadCheck.Checked,
+			uploadURL:         url,
+			enablePersistence: true,
+		}
+
+		runForDuration := runForDurationCheck.Checked
+		if runForDuration {
+			minutes, err := time.ParseDuration(duration.Text + "m")
+			if err != nil {
+				statusLabel.SetText(fmt.Sprintf("Error: Invalid duration: %v", err))
+				enableUIControls(uiControls)
+				return
+			}
+			params.duration = minutes
+
+			statusLabel.SetText(fmt.Sprintf("Running in debug mode for %d minutes...", int(minutes.Minutes())))
+			progressBar.Show()
+			progressBar.SetValue(0)
+
+			go s.handleRunForDuration(
+				statusLabel,
+				progressBar,
+				uiControls,
+				w,
+				params,
+			)
+			return
+		}
+
+		statusLabel.SetText("Creating debug bundle...")
+		go s.handleDebugCreation(
+			anonymizeCheck.Checked,
+			systemInfoCheck.Checked,
+			uploadCheck.Checked,
+			url,
+			statusLabel,
+			uiControls,
+			w,
+		)
+	}
+}
+
+func (s *serviceClient) handleRunForDuration(
+	statusLabel *widget.Label,
+	progressBar *widget.ProgressBar,
+	uiControls []fyne.Disableable,
+	w fyne.Window,
+	params *debugCollectionParams,
+) {
+	progressUI := &progressUI{
+		statusLabel: statusLabel,
+		progressBar: progressBar,
+		uiControls:  uiControls,
+		window:      w,
+	}
+
 	conn, err := s.getSrvClient(failFastTimeout)
 	if err != nil {
-		return fmt.Errorf("get client: %v", err)
+		handleError(progressUI, fmt.Sprintf("Failed to get client for debug: %v", err))
+		return
+	}
+
+	initialState, err := s.getInitialState(conn)
+	if err != nil {
+		handleError(progressUI, err.Error())
+		return
+	}
+
+	statusOutput, err := s.collectDebugData(conn, initialState, params, progressUI)
+	if err != nil {
+		handleError(progressUI, err.Error())
+		return
+	}
+
+	if err := s.createDebugBundleFromCollection(conn, params, statusOutput, progressUI); err != nil {
+		handleError(progressUI, err.Error())
+		return
+	}
+
+	s.restoreServiceState(conn, initialState)
+
+	progressUI.statusLabel.SetText("Bundle created successfully")
+}
+
+// Get initial state of the service
+func (s *serviceClient) getInitialState(conn proto.DaemonServiceClient) (*debugInitialState, error) {
+	statusResp, err := conn.Status(s.ctx, &proto.StatusRequest{})
+	if err != nil {
+		return nil, fmt.Errorf(" get status: %v", err)
+	}
+
+	logLevelResp, err := conn.GetLogLevel(s.ctx, &proto.GetLogLevelRequest{})
+	if err != nil {
+		return nil, fmt.Errorf("get log level: %v", err)
+	}
+
+	wasDown := statusResp.Status != string(internal.StatusConnected) &&
+		statusResp.Status != string(internal.StatusConnecting)
+
+	initialLogLevel := logLevelResp.GetLevel()
+	initialLevelTrace := initialLogLevel >= proto.LogLevel_TRACE
+
+	return &debugInitialState{
+		wasDown:      wasDown,
+		logLevel:     initialLogLevel,
+		isLevelTrace: initialLevelTrace,
+	}, nil
+}
+
+// Handle progress tracking during collection
+func startProgressTracker(ctx context.Context, wg *sync.WaitGroup, duration time.Duration, progress *progressUI) {
+	progress.progressBar.Show()
+	progress.progressBar.SetValue(0)
+
+	startTime := time.Now()
+	endTime := startTime.Add(duration)
+	wg.Add(1)
+
+	go func() {
+		defer wg.Done()
+		ticker := time.NewTicker(500 * time.Millisecond)
+		defer ticker.Stop()
+
+		for {
+			select {
+			case <-ctx.Done():
+				return
+			case <-ticker.C:
+				remaining := time.Until(endTime)
+				if remaining <= 0 {
+					remaining = 0
+				}
+
+				elapsed := time.Since(startTime)
+				progressVal := float64(elapsed) / float64(duration)
+				if progressVal > 1.0 {
+					progressVal = 1.0
+				}
+
+				progress.progressBar.SetValue(progressVal)
+				progress.statusLabel.SetText(fmt.Sprintf("Running with trace logs... %s remaining", formatDuration(remaining)))
+			}
+		}
+	}()
+
+}
+
+func (s *serviceClient) configureServiceForDebug(
+	conn proto.DaemonServiceClient,
+	state *debugInitialState,
+	enablePersistence bool,
+) error {
+	if state.wasDown {
+		if _, err := conn.Up(s.ctx, &proto.UpRequest{}); err != nil {
+			return fmt.Errorf("bring service up: %v", err)
+		}
+		log.Info("Service brought up for debug")
+		time.Sleep(time.Second * 10)
+	}
+
+	if !state.isLevelTrace {
+		if _, err := conn.SetLogLevel(s.ctx, &proto.SetLogLevelRequest{Level: proto.LogLevel_TRACE}); err != nil {
+			return fmt.Errorf("set log level to TRACE: %v", err)
+		}
+		log.Info("Log level set to TRACE for debug")
+	}
+
+	if _, err := conn.Down(s.ctx, &proto.DownRequest{}); err != nil {
+		return fmt.Errorf("bring service down: %v", err)
+	}
+	time.Sleep(time.Second)
+
+	if enablePersistence {
+		if _, err := conn.SetNetworkMapPersistence(s.ctx, &proto.SetNetworkMapPersistenceRequest{
+			Enabled: true,
+		}); err != nil {
+			return fmt.Errorf("enable network map persistence: %v", err)
+		}
+		log.Info("Network map persistence enabled for debug")
+	}
+
+	if _, err := conn.Up(s.ctx, &proto.UpRequest{}); err != nil {
+		return fmt.Errorf("bring service back up: %v", err)
+	}
+	time.Sleep(time.Second * 3)
+
+	return nil
+}
+
+func (s *serviceClient) collectDebugData(
+	conn proto.DaemonServiceClient,
+	state *debugInitialState,
+	params *debugCollectionParams,
+	progress *progressUI,
+) (string, error) {
+	ctx, cancel := context.WithTimeout(s.ctx, params.duration)
+	defer cancel()
+	var wg sync.WaitGroup
+	startProgressTracker(ctx, &wg, params.duration, progress)
+
+	if err := s.configureServiceForDebug(conn, state, params.enablePersistence); err != nil {
+		return "", err
+	}
+
+	postUpStatus, err := conn.Status(s.ctx, &proto.StatusRequest{GetFullPeerStatus: true})
+	if err != nil {
+		log.Warnf("Failed to get post-up status: %v", err)
+	}
+
+	var postUpStatusOutput string
+	if postUpStatus != nil {
+		overview := nbstatus.ConvertToStatusOutputOverview(postUpStatus, params.anonymize, "", nil, nil, nil)
+		postUpStatusOutput = nbstatus.ParseToFullDetailSummary(overview)
+	}
+	headerPostUp := fmt.Sprintf("----- NetBird post-up - Timestamp: %s", time.Now().Format(time.RFC3339))
+	statusOutput := fmt.Sprintf("%s\n%s", headerPostUp, postUpStatusOutput)
+
+	wg.Wait()
+	progress.progressBar.Hide()
+	progress.statusLabel.SetText("Collecting debug data...")
+
+	preDownStatus, err := conn.Status(s.ctx, &proto.StatusRequest{GetFullPeerStatus: true})
+	if err != nil {
+		log.Warnf("Failed to get pre-down status: %v", err)
+	}
+
+	var preDownStatusOutput string
+	if preDownStatus != nil {
+		overview := nbstatus.ConvertToStatusOutputOverview(preDownStatus, params.anonymize, "", nil, nil, nil)
+		preDownStatusOutput = nbstatus.ParseToFullDetailSummary(overview)
+	}
+	headerPreDown := fmt.Sprintf("----- NetBird pre-down - Timestamp: %s - Duration: %s",
+		time.Now().Format(time.RFC3339), params.duration)
+	statusOutput = fmt.Sprintf("%s\n%s\n%s", statusOutput, headerPreDown, preDownStatusOutput)
+
+	return statusOutput, nil
+}
+
+// Create the debug bundle with collected data
+func (s *serviceClient) createDebugBundleFromCollection(
+	conn proto.DaemonServiceClient,
+	params *debugCollectionParams,
+	statusOutput string,
+	progress *progressUI,
+) error {
+	progress.statusLabel.SetText("Creating debug bundle with collected logs...")
+
+	request := &proto.DebugBundleRequest{
+		Anonymize:  params.anonymize,
+		Status:     statusOutput,
+		SystemInfo: params.systemInfo,
+	}
+
+	if params.upload {
+		request.UploadURL = params.uploadURL
+	}
+
+	resp, err := conn.DebugBundle(s.ctx, request)
+	if err != nil {
+		return fmt.Errorf("create debug bundle: %v", err)
+	}
+
+	// Show appropriate dialog based on upload status
+	localPath := resp.GetPath()
+	uploadFailureReason := resp.GetUploadFailureReason()
+	uploadedKey := resp.GetUploadedKey()
+
+	if params.upload {
+		if uploadFailureReason != "" {
+			showUploadFailedDialog(progress.window, localPath, uploadFailureReason)
+		} else {
+			showUploadSuccessDialog(progress.window, localPath, uploadedKey)
+		}
+	} else {
+		showBundleCreatedDialog(progress.window, localPath)
+	}
+
+	enableUIControls(progress.uiControls)
+	return nil
+}
+
+// Restore service to original state
+func (s *serviceClient) restoreServiceState(conn proto.DaemonServiceClient, state *debugInitialState) {
+	if state.wasDown {
+		if _, err := conn.Down(s.ctx, &proto.DownRequest{}); err != nil {
+			log.Errorf("Failed to restore down state: %v", err)
+		} else {
+			log.Info("Service state restored to down")
+		}
+	}
+
+	if !state.isLevelTrace {
+		if _, err := conn.SetLogLevel(s.ctx, &proto.SetLogLevelRequest{Level: state.logLevel}); err != nil {
+			log.Errorf("Failed to restore log level: %v", err)
+		} else {
+			log.Info("Log level restored to original setting")
+		}
+	}
+}
+
+// Handle errors during debug collection
+func handleError(progress *progressUI, errMsg string) {
+	log.Errorf("%s", errMsg)
+	progress.statusLabel.SetText(errMsg)
+	progress.progressBar.Hide()
+	enableUIControls(progress.uiControls)
+}
+
+func (s *serviceClient) handleDebugCreation(
+	anonymize bool,
+	systemInfo bool,
+	upload bool,
+	uploadURL string,
+	statusLabel *widget.Label,
+	uiControls []fyne.Disableable,
+	w fyne.Window,
+) {
+	log.Infof("Creating debug bundle (Anonymized: %v, System Info: %v, Upload Attempt: %v)...",
+		anonymize, systemInfo, upload)
+
+	resp, err := s.createDebugBundle(anonymize, systemInfo, uploadURL)
+	if err != nil {
+		log.Errorf("Failed to create debug bundle: %v", err)
+		statusLabel.SetText(fmt.Sprintf("Error creating bundle: %v", err))
+		enableUIControls(uiControls)
+		return
+	}
+
+	localPath := resp.GetPath()
+	uploadFailureReason := resp.GetUploadFailureReason()
+	uploadedKey := resp.GetUploadedKey()
+
+	if upload {
+		if uploadFailureReason != "" {
+			showUploadFailedDialog(w, localPath, uploadFailureReason)
+		} else {
+			showUploadSuccessDialog(w, localPath, uploadedKey)
+		}
+	} else {
+		showBundleCreatedDialog(w, localPath)
+	}
+
+	enableUIControls(uiControls)
+	statusLabel.SetText("Bundle created successfully")
+}
+
+func (s *serviceClient) createDebugBundle(anonymize bool, systemInfo bool, uploadURL string) (*proto.DebugBundleResponse, error) {
+	conn, err := s.getSrvClient(failFastTimeout)
+	if err != nil {
+		return nil, fmt.Errorf("get client: %v", err)
 	}
 
 	statusResp, err := conn.Status(s.ctx, &proto.StatusRequest{GetFullPeerStatus: true})
 	if err != nil {
-		return fmt.Errorf("failed to get status: %v", err)
+		log.Warnf("failed to get status for debug bundle: %v", err)
 	}
 
-	overview := nbstatus.ConvertToStatusOutputOverview(statusResp, true, "", nil, nil, nil)
-	statusOutput := nbstatus.ParseToFullDetailSummary(overview)
+	var statusOutput string
+	if statusResp != nil {
+		overview := nbstatus.ConvertToStatusOutputOverview(statusResp, anonymize, "", nil, nil, nil)
+		statusOutput = nbstatus.ParseToFullDetailSummary(overview)
+	}
 
-	resp, err := conn.DebugBundle(s.ctx, &proto.DebugBundleRequest{
-		Anonymize:  true,
+	request := &proto.DebugBundleRequest{
+		Anonymize:  anonymize,
 		Status:     statusOutput,
-		SystemInfo: true,
-	})
+		SystemInfo: systemInfo,
+	}
+
+	if uploadURL != "" {
+		request.UploadURL = uploadURL
+	}
+
+	resp, err := conn.DebugBundle(s.ctx, request)
 	if err != nil {
-		return fmt.Errorf("failed to create debug bundle: %v", err)
+		return nil, fmt.Errorf("failed to create debug bundle via daemon: %v", err)
 	}
 
-	bundleDir := filepath.Dir(resp.GetPath())
-	if err := open.Start(bundleDir); err != nil {
-		return fmt.Errorf("failed to open debug bundle directory: %v", err)
+	return resp, nil
+}
+
+// formatDuration formats a duration in HH:MM:SS format
+func formatDuration(d time.Duration) string {
+	d = d.Round(time.Second)
+	h := d / time.Hour
+	d %= time.Hour
+	m := d / time.Minute
+	d %= time.Minute
+	s := d / time.Second
+	return fmt.Sprintf("%02d:%02d:%02d", h, m, s)
+}
+
+// createButtonWithAction creates a button with the given label and action
+func createButtonWithAction(label string, action func()) *widget.Button {
+	button := widget.NewButton(label, action)
+	return button
+}
+
+// showUploadFailedDialog displays a dialog when upload fails
+func showUploadFailedDialog(w fyne.Window, localPath, failureReason string) {
+	content := container.NewVBox(
+		widget.NewLabel(fmt.Sprintf("Bundle upload failed:\n%s\n\n"+
+			"A local copy was saved at:\n%s", failureReason, localPath)),
+	)
+
+	customDialog := dialog.NewCustom("Upload Failed", "Cancel", content, w)
+
+	buttonBox := container.NewHBox(
+		createButtonWithAction("Open file", func() {
+			log.Infof("Attempting to open local file: %s", localPath)
+			if openErr := open.Start(localPath); openErr != nil {
+				log.Errorf("Failed to open local file '%s': %v", localPath, openErr)
+				dialog.ShowError(fmt.Errorf("open the local file:\n%s\n\nError: %v", localPath, openErr), w)
+			}
+		}),
+		createButtonWithAction("Open folder", func() {
+			folderPath := filepath.Dir(localPath)
+			log.Infof("Attempting to open local folder: %s", folderPath)
+			if openErr := open.Start(folderPath); openErr != nil {
+				log.Errorf("Failed to open local folder '%s': %v", folderPath, openErr)
+				dialog.ShowError(fmt.Errorf("open the local folder:\n%s\n\nError: %v", folderPath, openErr), w)
+			}
+		}),
+	)
+
+	content.Add(buttonBox)
+	customDialog.Show()
+}
+
+// showUploadSuccessDialog displays a dialog when upload succeeds
+func showUploadSuccessDialog(w fyne.Window, localPath, uploadedKey string) {
+	log.Infof("Upload key: %s", uploadedKey)
+	keyEntry := widget.NewEntry()
+	keyEntry.SetText(uploadedKey)
+	keyEntry.Disable()
+
+	content := container.NewVBox(
+		widget.NewLabel("Bundle uploaded successfully!"),
+		widget.NewLabel(""),
+		widget.NewLabel("Upload key:"),
+		keyEntry,
+		widget.NewLabel(""),
+		widget.NewLabel(fmt.Sprintf("Local copy saved at:\n%s", localPath)),
+	)
+
+	customDialog := dialog.NewCustom("Upload Successful", "OK", content, w)
+
+	copyBtn := createButtonWithAction("Copy key", func() {
+		w.Clipboard().SetContent(uploadedKey)
+		log.Info("Upload key copied to clipboard")
+	})
+
+	buttonBox := createButtonBox(localPath, w, copyBtn)
+	content.Add(buttonBox)
+	customDialog.Show()
+}
+
+// showBundleCreatedDialog displays a dialog when bundle is created without upload
+func showBundleCreatedDialog(w fyne.Window, localPath string) {
+	content := container.NewVBox(
+		widget.NewLabel(fmt.Sprintf("Bundle created locally at:\n%s\n\n"+
+			"Administrator privileges may be required to access the file.", localPath)),
+	)
+
+	customDialog := dialog.NewCustom("Debug Bundle Created", "Cancel", content, w)
+
+	buttonBox := createButtonBox(localPath, w, nil)
+	content.Add(buttonBox)
+	customDialog.Show()
+}
+
+func createButtonBox(localPath string, w fyne.Window, elems ...fyne.Widget) *fyne.Container {
+	box := container.NewHBox()
+	for _, elem := range elems {
+		box.Add(elem)
 	}
 
-	s.app.SendNotification(fyne.NewNotification(
-		"Debug Bundle",
-		fmt.Sprintf("Debug bundle created at %s. Administrator privileges are required to access it.", resp.GetPath()),
-	))
+	fileBtn := createButtonWithAction("Open file", func() {
+		log.Infof("Attempting to open local file: %s", localPath)
+		if openErr := open.Start(localPath); openErr != nil {
+			log.Errorf("Failed to open local file '%s': %v", localPath, openErr)
+			dialog.ShowError(fmt.Errorf("open the local file:\n%s\n\nError: %v", localPath, openErr), w)
+		}
+	})
 
-	return nil
+	folderBtn := createButtonWithAction("Open folder", func() {
+		folderPath := filepath.Dir(localPath)
+		log.Infof("Attempting to open local folder: %s", folderPath)
+		if openErr := open.Start(folderPath); openErr != nil {
+			log.Errorf("Failed to open local folder '%s': %v", folderPath, openErr)
+			dialog.ShowError(fmt.Errorf("open the local folder:\n%s\n\nError: %v", folderPath, openErr), w)
+		}
+	})
+
+	box.Add(fileBtn)
+	box.Add(folderBtn)
+
+	return box
 }
diff --git a/client/ui/network.go b/client/ui/network.go
index b21554f0..b3748a89 100644
--- a/client/ui/network.go
+++ b/client/ui/network.go
@@ -6,6 +6,7 @@ import (
 	"context"
 	"fmt"
 	"runtime"
+	"slices"
 	"sort"
 	"strings"
 	"time"
@@ -33,8 +34,14 @@ const (
 
 type filter string
 
+type exitNodeState struct {
+	id       string
+	selected bool
+}
+
 func (s *serviceClient) showNetworksUI() {
-	s.wRoutes = s.app.NewWindow("Networks")
+	s.wNetworks = s.app.NewWindow("Networks")
+	s.wNetworks.SetOnClosed(s.cancel)
 
 	allGrid := container.New(layout.NewGridLayout(3))
 	go s.updateNetworks(allGrid, allNetworks)
@@ -78,8 +85,8 @@ func (s *serviceClient) showNetworksUI() {
 
 	content := container.NewBorder(nil, buttonBox, nil, nil, scrollContainer)
 
-	s.wRoutes.SetContent(content)
-	s.wRoutes.Show()
+	s.wNetworks.SetContent(content)
+	s.wNetworks.Show()
 
 	s.startAutoRefresh(10*time.Second, tabs, allGrid, overlappingGrid, exitNodeGrid)
 }
@@ -148,7 +155,7 @@ func (s *serviceClient) updateNetworks(grid *fyne.Container, f filter) {
 		grid.Add(resolvedIPsSelector)
 	}
 
-	s.wRoutes.Content().Refresh()
+	s.wNetworks.Content().Refresh()
 	grid.Refresh()
 }
 
@@ -305,7 +312,7 @@ func (s *serviceClient) getNetworksRequest(f filter, appendRoute bool) *proto.Se
 func (s *serviceClient) showError(err error) {
 	wrappedMessage := wrapText(err.Error(), 50)
 
-	dialog.ShowError(fmt.Errorf("%s", wrappedMessage), s.wRoutes)
+	dialog.ShowError(fmt.Errorf("%s", wrappedMessage), s.wNetworks)
 }
 
 func (s *serviceClient) startAutoRefresh(interval time.Duration, tabs *container.AppTabs, allGrid, overlappingGrid, exitNodesGrid *fyne.Container) {
@@ -316,14 +323,15 @@ func (s *serviceClient) startAutoRefresh(interval time.Duration, tabs *container
 		}
 	}()
 
-	s.wRoutes.SetOnClosed(func() {
+	s.wNetworks.SetOnClosed(func() {
 		ticker.Stop()
+		s.cancel()
 	})
 }
 
 func (s *serviceClient) updateNetworksBasedOnDisplayTab(tabs *container.AppTabs, allGrid, overlappingGrid, exitNodesGrid *fyne.Container) {
 	grid, f := getGridAndFilterFromTab(tabs, allGrid, overlappingGrid, exitNodesGrid)
-	s.wRoutes.Content().Refresh()
+	s.wNetworks.Content().Refresh()
 	s.updateNetworks(grid, f)
 }
 
@@ -355,25 +363,52 @@ func (s *serviceClient) updateExitNodes() {
 }
 
 func (s *serviceClient) recreateExitNodeMenu(exitNodes []*proto.Network) {
+	var exitNodeIDs []exitNodeState
+	for _, node := range exitNodes {
+		exitNodeIDs = append(exitNodeIDs, exitNodeState{
+			id:       node.ID,
+			selected: node.Selected,
+		})
+	}
+
+	sort.Slice(exitNodeIDs, func(i, j int) bool {
+		return exitNodeIDs[i].id < exitNodeIDs[j].id
+	})
+	if slices.Equal(s.exitNodeStates, exitNodeIDs) {
+		log.Debug("Exit node menu already up to date")
+		return
+	}
+
 	for _, node := range s.mExitNodeItems {
 		node.cancel()
+		node.Hide()
 		node.Remove()
 	}
 	s.mExitNodeItems = nil
+	if s.mExitNodeDeselectAll != nil {
+		s.mExitNodeDeselectAll.Remove()
+		s.mExitNodeDeselectAll = nil
+	}
 
 	if runtime.GOOS == "linux" || runtime.GOOS == "freebsd" {
 		s.mExitNode.Remove()
 		s.mExitNode = systray.AddMenuItem("Exit Node", exitNodeMenuDescr)
 	}
 
+	var showDeselectAll bool
+
 	for _, node := range exitNodes {
+		if node.Selected {
+			showDeselectAll = true
+		}
+
 		menuItem := s.mExitNode.AddSubMenuItemCheckbox(
 			node.ID,
 			fmt.Sprintf("Use exit node %s", node.ID),
 			node.Selected,
 		)
 
-		ctx, cancel := context.WithCancel(context.Background())
+		ctx, cancel := context.WithCancel(s.ctx)
 		s.mExitNodeItems = append(s.mExitNodeItems, menuHandler{
 			MenuItem: menuItem,
 			cancel:   cancel,
@@ -381,6 +416,32 @@ func (s *serviceClient) recreateExitNodeMenu(exitNodes []*proto.Network) {
 		go s.handleChecked(ctx, node.ID, menuItem)
 	}
 
+	s.exitNodeStates = exitNodeIDs
+
+	if showDeselectAll {
+		s.mExitNode.AddSeparator()
+		deselectAllItem := s.mExitNode.AddSubMenuItem("Deselect All", "Deselect All")
+		s.mExitNodeDeselectAll = deselectAllItem
+		go func() {
+			for {
+				_, ok := <-deselectAllItem.ClickedCh
+				if !ok {
+					// channel closed: exit the goroutine
+					return
+				}
+				exitNodes, err := s.handleExitNodeMenuDeselectAll()
+				if err != nil {
+					log.Warnf("failed to handle deselect all exit nodes: %v", err)
+				} else {
+					s.exitNodeMu.Lock()
+					s.recreateExitNodeMenu(exitNodes)
+					s.exitNodeMu.Unlock()
+				}
+			}
+
+		}()
+	}
+
 }
 
 func (s *serviceClient) getExitNodes(conn proto.DaemonServiceClient) ([]*proto.Network, error) {
@@ -418,6 +479,37 @@ func (s *serviceClient) handleChecked(ctx context.Context, id string, item *syst
 	}
 }
 
+func (s *serviceClient) handleExitNodeMenuDeselectAll() ([]*proto.Network, error) {
+	conn, err := s.getSrvClient(defaultFailTimeout)
+	if err != nil {
+		return nil, fmt.Errorf("get client: %v", err)
+	}
+
+	exitNodes, err := s.getExitNodes(conn)
+	if err != nil {
+		return nil, fmt.Errorf("get exit nodes: %v", err)
+	}
+
+	var ids []string
+	for _, e := range exitNodes {
+		if e.Selected {
+			ids = append(ids, e.ID)
+		}
+	}
+
+	// deselect selected exit nodes
+	if err := s.deselectOtherExitNodes(conn, ids); err != nil {
+		return nil, err
+	}
+
+	updatedExitNodes, err := s.getExitNodes(conn)
+	if err != nil {
+		return nil, fmt.Errorf("re-fetch exit nodes: %v", err)
+	}
+
+	return updatedExitNodes, nil
+}
+
 // Add function to toggle exit node selection
 func (s *serviceClient) toggleExitNode(nodeID string, item *systray.MenuItem) error {
 	conn, err := s.getSrvClient(defaultFailTimeout)
@@ -456,19 +548,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 +579,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 +594,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 +615,7 @@ func (s *serviceClient) selectNewExitNode(conn proto.DaemonServiceClient, exitNo
 	}
 
 	item.Check()
+	log.Infof("Checked exit node '%s'", nodeID)
 
 	return nil
 }
diff --git a/client/ui/process/process.go b/client/ui/process/process.go
index f9a8a4fe..d0ef5489 100644
--- a/client/ui/process/process.go
+++ b/client/ui/process/process.go
@@ -8,10 +8,10 @@ import (
 	"github.com/shirou/gopsutil/v3/process"
 )
 
-func IsAnotherProcessRunning() (bool, error) {
+func IsAnotherProcessRunning() (int32, bool, error) {
 	processes, err := process.Processes()
 	if err != nil {
-		return false, err
+		return 0, false, err
 	}
 
 	pid := os.Getpid()
@@ -29,9 +29,9 @@ func IsAnotherProcessRunning() (bool, error) {
 		}
 
 		if strings.Contains(strings.ToLower(runningProcessPath), processName) && isProcessOwnedByCurrentUser(p) {
-			return true, nil
+			return p.Pid, true, nil
 		}
 	}
 
-	return false, nil
+	return 0, false, nil
 }
diff --git a/dns/dns.go b/dns/dns.go
index 8dfdf852..f889a32e 100644
--- a/dns/dns.go
+++ b/dns/dns.go
@@ -66,17 +66,17 @@ func (s SimpleRecord) String() string {
 func (s SimpleRecord) Len() uint16 {
 	emptyString := s.RData == ""
 	switch s.Type {
-	case 1:
+	case int(dns.TypeA):
 		if emptyString {
 			return 0
 		}
 		return net.IPv4len
-	case 5:
+	case int(dns.TypeCNAME):
 		if emptyString || s.RData == "." {
 			return 1
 		}
 		return uint16(len(s.RData) + 1)
-	case 28:
+	case int(dns.TypeAAAA):
 		if emptyString {
 			return 0
 		}
@@ -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 c00f3206..11dc88c4 100644
--- a/go.mod
+++ b/go.mod
@@ -18,9 +18,9 @@ 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
@@ -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
@@ -49,7 +51,7 @@ require (
 	github.com/golang/mock v1.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
@@ -57,13 +59,12 @@ require (
 	github.com/hashicorp/go-version v1.6.0
 	github.com/libdns/route53 v1.5.0
 	github.com/libp2p/go-netroute v0.2.1
-	github.com/mattn/go-sqlite3 v1.14.22
 	github.com/mdlayher/socket v0.5.1
 	github.com/miekg/dns v1.1.59
 	github.com/mitchellh/hashstructure/v2 v2.0.2
 	github.com/nadoo/ipset v0.5.0
-	github.com/netbirdio/management-integrations/integrations v0.0.0-20250330143713-7901e0a82203
-	github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20241010133937-e0df50df217d
+	github.com/netbirdio/management-integrations/integrations v0.0.0-20250529122842-6700aa91190c
+	github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250514131221-a464fd5f30cb
 	github.com/okta/okta-sdk-golang/v2 v2.18.0
 	github.com/oschwald/maxminddb-golang v1.12.0
 	github.com/patrickmn/go-cache v2.1.0+incompatible
@@ -75,7 +76,7 @@ require (
 	github.com/pion/turn/v3 v3.0.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 +101,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/net v0.39.0
 	golang.org/x/oauth2 v0.24.0
-	golang.org/x/sync v0.12.0
-	golang.org/x/term v0.30.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 +125,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 +148,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,7 +187,6 @@ 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.18.0 // indirect
@@ -191,8 +194,9 @@ require (
 	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/mattn/go-sqlite3 v1.14.22 // 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
@@ -235,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 f00b42be..f887cee9 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=
@@ -301,8 +311,8 @@ github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo
 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=
@@ -399,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=
@@ -447,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=
@@ -495,12 +503,12 @@ github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944 h1:TDtJKmM6S
 github.com/netbirdio/go-netroute v0.0.0-20240611143515-f59b0e1d3944/go.mod h1:sHA6TRxjQ6RLbnI+3R4DZo2Eseg/iKiPRfNmcuNySVQ=
 github.com/netbirdio/ice/v3 v3.0.0-20240315174635-e72a50fcb64e h1:PURA50S8u4mF6RrkYYCAvvPCixhqqEiEy3Ej6avh04c=
 github.com/netbirdio/ice/v3 v3.0.0-20240315174635-e72a50fcb64e/go.mod h1:YMLU7qbKfVjmEv7EoZPIVEI+kNYxWCdPK3VS0BU+U4Q=
-github.com/netbirdio/management-integrations/integrations v0.0.0-20250330143713-7901e0a82203 h1:uxxbLPXQgC9VO15epNPtrD6zazyd5rZeqC5hQSmCdZU=
-github.com/netbirdio/management-integrations/integrations v0.0.0-20250330143713-7901e0a82203/go.mod h1:2ZE6/tBBCKHQggPfO2UOQjyjXI7k+JDVl2ymorTOVQs=
+github.com/netbirdio/management-integrations/integrations v0.0.0-20250529122842-6700aa91190c h1:SdZxYjR9XXHLyRsTbS1EHBr6+RI15oie1K9Q8yvi3FY=
+github.com/netbirdio/management-integrations/integrations v0.0.0-20250529122842-6700aa91190c/go.mod h1:Gi9raplYzCCyh07Olw/DVfCJTFgpr1WCXJ/Q+8TSA9Q=
 github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502 h1:3tHlFmhTdX9axERMVN63dqyFqnvuD+EMJHzM7mNGON8=
 github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
-github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20241010133937-e0df50df217d h1:bRq5TKgC7Iq20pDiuC54yXaWnAVeS5PdGpSokFTlR28=
-github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20241010133937-e0df50df217d/go.mod h1:5/sjFmLb8O96B5737VCqhHyGRzNFIaN/Bu7ZodXc3qQ=
+github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250514131221-a464fd5f30cb h1:Cr6age+ePALqlSvtp7wc6lYY97XN7rkD1K4XEDmY+TU=
+github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250514131221-a464fd5f30cb/go.mod h1:5/sjFmLb8O96B5737VCqhHyGRzNFIaN/Bu7ZodXc3qQ=
 github.com/netbirdio/wireguard-go v0.0.0-20241230120307-6a676aebaaf6 h1:X5h5QgP7uHAv78FWgHV8+WYLjHxK9v3ilkVXT1cpCrQ=
 github.com/netbirdio/wireguard-go v0.0.0-20241230120307-6a676aebaaf6/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
 github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM=
@@ -576,8 +584,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg
 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=
@@ -665,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=
@@ -752,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=
@@ -846,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=
@@ -876,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=
@@ -902,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=
@@ -911,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=
@@ -939,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=
@@ -954,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=
@@ -969,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=
diff --git a/infrastructure_files/base.setup.env b/infrastructure_files/base.setup.env
index 4b137692..fdba1f21 100644
--- a/infrastructure_files/base.setup.env
+++ b/infrastructure_files/base.setup.env
@@ -23,6 +23,7 @@ NETBIRD_SIGNAL_PORT=${NETBIRD_SIGNAL_PORT:-10000}
 # Relay
 NETBIRD_RELAY_DOMAIN=${NETBIRD_RELAY_DOMAIN:-$NETBIRD_DOMAIN}
 NETBIRD_RELAY_PORT=${NETBIRD_RELAY_PORT:-33080}
+NETBIRD_RELAY_ENDPOINT=${NETBIRD_RELAY_ENDPOINT:-rel://$NETBIRD_RELAY_DOMAIN:$NETBIRD_RELAY_PORT}
 # Relay auth secret
 NETBIRD_RELAY_AUTH_SECRET=
 
@@ -59,6 +60,7 @@ NETBIRD_TOKEN_SOURCE=${NETBIRD_TOKEN_SOURCE:-accessToken}
 NETBIRD_AUTH_PKCE_REDIRECT_URL_PORTS=${NETBIRD_AUTH_PKCE_REDIRECT_URL_PORTS:-"53000"}
 NETBIRD_AUTH_PKCE_USE_ID_TOKEN=${NETBIRD_AUTH_PKCE_USE_ID_TOKEN:-false}
 NETBIRD_AUTH_PKCE_DISABLE_PROMPT_LOGIN=${NETBIRD_AUTH_PKCE_DISABLE_PROMPT_LOGIN:-false}
+NETBIRD_AUTH_PKCE_LOGIN_FLAG=${NETBIRD_AUTH_PKCE_LOGIN_FLAG:-1}
 NETBIRD_AUTH_PKCE_AUDIENCE=$NETBIRD_AUTH_AUDIENCE
 
 # Dashboard
@@ -122,6 +124,7 @@ export NETBIRD_AUTH_DEVICE_AUTH_USE_ID_TOKEN
 export NETBIRD_AUTH_PKCE_AUTHORIZATION_ENDPOINT
 export NETBIRD_AUTH_PKCE_USE_ID_TOKEN
 export NETBIRD_AUTH_PKCE_DISABLE_PROMPT_LOGIN
+export NETBIRD_AUTH_PKCE_LOGIN_FLAG
 export NETBIRD_AUTH_PKCE_AUDIENCE
 export NETBIRD_DASH_AUTH_USE_AUDIENCE
 export NETBIRD_DASH_AUTH_AUDIENCE
@@ -133,5 +136,6 @@ export COTURN_TAG
 export NETBIRD_TURN_EXTERNAL_IP
 export NETBIRD_RELAY_DOMAIN
 export NETBIRD_RELAY_PORT
+export NETBIRD_RELAY_ENDPOINT
 export NETBIRD_RELAY_AUTH_SECRET
 export NETBIRD_RELAY_TAG
diff --git a/infrastructure_files/configure.sh b/infrastructure_files/configure.sh
index d02e4f40..6898555a 100755
--- a/infrastructure_files/configure.sh
+++ b/infrastructure_files/configure.sh
@@ -170,6 +170,7 @@ fi
 if [[ "$NETBIRD_DISABLE_LETSENCRYPT" == "true" ]]; then
   export NETBIRD_DASHBOARD_ENDPOINT="https://$NETBIRD_DOMAIN:443"
   export NETBIRD_SIGNAL_ENDPOINT="https://$NETBIRD_DOMAIN:$NETBIRD_SIGNAL_PORT"
+  export NETBIRD_RELAY_ENDPOINT="rels://$NETBIRD_DOMAIN:$NETBIRD_SIGNAL_PORT/relay"
 
   echo "Letsencrypt was disabled, the Https-endpoints cannot be used anymore"
   echo " and a reverse-proxy with Https needs to be placed in front of netbird!"
@@ -178,6 +179,7 @@ if [[ "$NETBIRD_DISABLE_LETSENCRYPT" == "true" ]]; then
   echo "- $NETBIRD_MGMT_API_ENDPOINT/api -http-> management:$NETBIRD_MGMT_API_PORT"
   echo "- $NETBIRD_MGMT_API_ENDPOINT/management.ManagementService/ -grpc-> management:$NETBIRD_MGMT_API_PORT"
   echo "- $NETBIRD_SIGNAL_ENDPOINT/signalexchange.SignalExchange/ -grpc-> signal:80"
+  echo "- $NETBIRD_RELAY_ENDPOINT/ -http-> relay:33080"
   echo "You most likely also have to change NETBIRD_MGMT_API_ENDPOINT in base.setup.env and port-mappings in docker-compose.yml.tmpl and rerun this script."
   echo " The target of the forwards depends on your setup. Beware of the gRPC protocol instead of http for management and signal!"
   echo "You are also free to remove any occurrences of the Letsencrypt-volume $LETSENCRYPT_VOLUMENAME"
diff --git a/infrastructure_files/docker-compose.yml.tmpl b/infrastructure_files/docker-compose.yml.tmpl
index dc491ae2..b529f960 100644
--- a/infrastructure_files/docker-compose.yml.tmpl
+++ b/infrastructure_files/docker-compose.yml.tmpl
@@ -57,7 +57,7 @@ services:
     environment:
     - NB_LOG_LEVEL=info
     - NB_LISTEN_ADDRESS=:$NETBIRD_RELAY_PORT
-    - NB_EXPOSED_ADDRESS=$NETBIRD_RELAY_DOMAIN:$NETBIRD_RELAY_PORT
+    - NB_EXPOSED_ADDRESS=$NETBIRD_RELAY_ENDPOINT
     # todo: change to a secure secret
     - NB_AUTH_SECRET=$NETBIRD_RELAY_AUTH_SECRET
     ports:
diff --git a/infrastructure_files/docker-compose.yml.tmpl.traefik b/infrastructure_files/docker-compose.yml.tmpl.traefik
index 8cc3df30..8da3cabb 100644
--- a/infrastructure_files/docker-compose.yml.tmpl.traefik
+++ b/infrastructure_files/docker-compose.yml.tmpl.traefik
@@ -3,9 +3,6 @@ services:
   dashboard:
     image: netbirdio/dashboard:$NETBIRD_DASHBOARD_TAG
     restart: unless-stopped
-    #ports:
-    #  - 80:80
-    #  - 443:443
     environment:
       # Endpoints
       - NETBIRD_MGMT_API_ENDPOINT=$NETBIRD_MGMT_API_ENDPOINT
@@ -43,11 +40,6 @@ services:
     restart: unless-stopped
     volumes:
       - $SIGNAL_VOLUMENAME:/var/lib/netbird
-    #ports:
-    #  - $NETBIRD_SIGNAL_PORT:80
-  #      # port and command for Let's Encrypt validation
-  #      - 443:443
-  #    command: ["--letsencrypt-domain", "$NETBIRD_LETSENCRYPT_DOMAIN", "--log-file", "console"]
     labels:
     - traefik.enable=true
     - traefik.http.routers.netbird-signal.rule=Host(`$NETBIRD_DOMAIN`) && PathPrefix(`/signalexchange.SignalExchange/`)
@@ -65,12 +57,10 @@ services:
     restart: unless-stopped
     environment:
     - NB_LOG_LEVEL=info
-    - NB_LISTEN_ADDRESS=:$NETBIRD_RELAY_PORT
-    - NB_EXPOSED_ADDRESS=$NETBIRD_RELAY_DOMAIN:$NETBIRD_RELAY_PORT
+    - NB_LISTEN_ADDRESS=:33080
+    - NB_EXPOSED_ADDRESS=$NETBIRD_RELAY_ENDPOINT
     # todo: change to a secure secret
     - NB_AUTH_SECRET=$NETBIRD_RELAY_AUTH_SECRET
-    # ports:
-    #   - $NETBIRD_RELAY_PORT:$NETBIRD_RELAY_PORT
     logging:
       driver: "json-file"
       options:
@@ -79,7 +69,7 @@ services:
     labels:
     - traefik.enable=true
     - traefik.http.routers.netbird-relay.rule=Host(`$NETBIRD_DOMAIN`) && PathPrefix(`/relay`)
-    - traefik.http.services.netbird-relay.loadbalancer.server.port=$NETBIRD_RELAY_PORT
+    - traefik.http.services.netbird-relay.loadbalancer.server.port=33080
 
   # Management
   management:
@@ -91,10 +81,6 @@ services:
       - $MGMT_VOLUMENAME:/var/lib/netbird
       - $LETSENCRYPT_VOLUMENAME:/etc/letsencrypt:ro
       - ./management.json:/etc/netbird/management.json
-    #ports:
-    #  - $NETBIRD_MGMT_API_PORT:443 #API port
-  #    # command for Let's Encrypt validation without dashboard container
-  #    command: ["--letsencrypt-domain", "$NETBIRD_LETSENCRYPT_DOMAIN", "--log-file", "console"]
     command: [
       "--port", "33073",
       "--log-file", "console",
@@ -129,8 +115,6 @@ services:
     domainname: $TURN_DOMAIN
     volumes:
       - ./turnserver.conf:/etc/turnserver.conf:ro
-    #      - ./privkey.pem:/etc/coturn/private/privkey.pem:ro
-    #      - ./cert.pem:/etc/coturn/certs/cert.pem:ro
     network_mode: host
     command:
       - -c /etc/turnserver.conf
diff --git a/infrastructure_files/management.json.tmpl b/infrastructure_files/management.json.tmpl
index aa1739c6..4d09816e 100644
--- a/infrastructure_files/management.json.tmpl
+++ b/infrastructure_files/management.json.tmpl
@@ -21,7 +21,7 @@
         "TimeBasedCredentials": false
     },
     "Relay": {
-        "Addresses": ["rel://$NETBIRD_RELAY_DOMAIN:$NETBIRD_RELAY_PORT"],
+        "Addresses": ["$NETBIRD_RELAY_ENDPOINT"],
         "CredentialsTTL": "24h",
         "Secret": "$NETBIRD_RELAY_AUTH_SECRET"
     },
@@ -95,7 +95,8 @@
             "Scope": "$NETBIRD_AUTH_SUPPORTED_SCOPES",
             "RedirectURLs": [$NETBIRD_AUTH_PKCE_REDIRECT_URLS],
             "UseIDToken": $NETBIRD_AUTH_PKCE_USE_ID_TOKEN,
-            "DisablePromptLogin": $NETBIRD_AUTH_PKCE_DISABLE_PROMPT_LOGIN
+            "DisablePromptLogin": $NETBIRD_AUTH_PKCE_DISABLE_PROMPT_LOGIN,
+            "LoginFlag": $NETBIRD_AUTH_PKCE_LOGIN_FLAG
         }
     }
 }
diff --git a/infrastructure_files/setup.env.example b/infrastructure_files/setup.env.example
index b1b64de7..b5b718a7 100644
--- a/infrastructure_files/setup.env.example
+++ b/infrastructure_files/setup.env.example
@@ -102,4 +102,15 @@ NETBIRD_RELAY_DOMAIN=""
 
 # Relay server connection port. If none is supplied
 # it will default to 33080
+# should be updated to match TLS-port of reverse proxy when netbird is running behind reverse proxy
 NETBIRD_RELAY_PORT=""
+
+# Management API connectin port. If none is supplied
+# it will default to 33073
+# should be updated to match TLS-port of reverse proxy when netbird is running behind reverse proxy
+NETBIRD_MGMT_API_PORT=""
+
+# Signal service connectin port. If none is supplied
+# it will default to 10000
+# should be updated to match TLS-port of reverse proxy when netbird is running behind reverse proxy
+NETBIRD_SIGNAL_PORT=""
diff --git a/management/client/common/types.go b/management/client/common/types.go
new file mode 100644
index 00000000..69961757
--- /dev/null
+++ b/management/client/common/types.go
@@ -0,0 +1,19 @@
+package common
+
+// LoginFlag introduces additional login flags to the PKCE authorization request
+type LoginFlag uint8
+
+const (
+	// LoginFlagPrompt adds prompt=login to the authorization request
+	LoginFlagPrompt LoginFlag = iota
+	// LoginFlagMaxAge0 adds max_age=0 to the authorization request
+	LoginFlagMaxAge0
+)
+
+func (l LoginFlag) IsPromptLogin() bool {
+	return l == LoginFlagPrompt
+}
+
+func (l LoginFlag) IsMaxAge0Login() bool {
+	return l == LoginFlagMaxAge0
+}
diff --git a/management/client/grpc.go b/management/client/grpc.go
index d3aaffec..e85e16e7 100644
--- a/management/client/grpc.go
+++ b/management/client/grpc.go
@@ -159,6 +159,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 +168,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 +258,8 @@ 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
 		}
 	}
 }
diff --git a/management/client/rest/accounts.go b/management/client/rest/accounts.go
index f38b19f7..2530e4f7 100644
--- a/management/client/rest/accounts.go
+++ b/management/client/rest/accounts.go
@@ -16,11 +16,13 @@ type AccountsAPI struct {
 // List list all accounts, only returns one account always
 // See more: https://docs.netbird.io/api/resources/accounts#list-all-accounts
 func (a *AccountsAPI) List(ctx context.Context) ([]api.Account, error) {
-	resp, err := a.c.newRequest(ctx, "GET", "/api/accounts", nil)
+	resp, err := a.c.NewRequest(ctx, "GET", "/api/accounts", nil)
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[[]api.Account](resp)
 	return ret, err
 }
@@ -32,11 +34,13 @@ func (a *AccountsAPI) Update(ctx context.Context, accountID string, request api.
 	if err != nil {
 		return nil, err
 	}
-	resp, err := a.c.newRequest(ctx, "PUT", "/api/accounts/"+accountID, bytes.NewReader(requestBytes))
+	resp, err := a.c.NewRequest(ctx, "PUT", "/api/accounts/"+accountID, bytes.NewReader(requestBytes))
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.Account](resp)
 	return &ret, err
 }
@@ -44,11 +48,13 @@ func (a *AccountsAPI) Update(ctx context.Context, accountID string, request api.
 // Delete delete account
 // See more: https://docs.netbird.io/api/resources/accounts#delete-an-account
 func (a *AccountsAPI) Delete(ctx context.Context, accountID string) error {
-	resp, err := a.c.newRequest(ctx, "DELETE", "/api/accounts/"+accountID, nil)
+	resp, err := a.c.NewRequest(ctx, "DELETE", "/api/accounts/"+accountID, nil)
 	if err != nil {
 		return err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 
 	return nil
 }
diff --git a/management/client/rest/client.go b/management/client/rest/client.go
index 0785d88a..8bf11caa 100644
--- a/management/client/rest/client.go
+++ b/management/client/rest/client.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"encoding/json"
 	"errors"
+	"fmt"
 	"io"
 	"net/http"
 
@@ -14,6 +15,7 @@ import (
 type Client struct {
 	managementURL string
 	authHeader    string
+	httpClient    HttpClient
 
 	// Accounts NetBird account APIs
 	// see more: https://docs.netbird.io/api/resources/accounts
@@ -70,20 +72,30 @@ type Client struct {
 
 // New initialize new Client instance using PAT token
 func New(managementURL, token string) *Client {
-	client := &Client{
-		managementURL: managementURL,
-		authHeader:    "Token " + token,
-	}
-	client.initialize()
-	return client
+	return NewWithOptions(
+		WithManagementURL(managementURL),
+		WithPAT(token),
+	)
 }
 
 // NewWithBearerToken initialize new Client instance using Bearer token type
 func NewWithBearerToken(managementURL, token string) *Client {
+	return NewWithOptions(
+		WithManagementURL(managementURL),
+		WithBearerToken(token),
+	)
+}
+
+// NewWithOptions initialize new Client instance with options
+func NewWithOptions(opts ...option) *Client {
 	client := &Client{
-		managementURL: managementURL,
-		authHeader:    "Bearer " + token,
+		httpClient: http.DefaultClient,
 	}
+
+	for _, option := range opts {
+		option(client)
+	}
+
 	client.initialize()
 	return client
 }
@@ -104,7 +116,8 @@ func (c *Client) initialize() {
 	c.Events = &EventsAPI{c}
 }
 
-func (c *Client) newRequest(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) {
+// NewRequest creates and executes new management API request
+func (c *Client) NewRequest(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) {
 	req, err := http.NewRequestWithContext(ctx, method, c.managementURL+path, body)
 	if err != nil {
 		return nil, err
@@ -116,7 +129,7 @@ func (c *Client) newRequest(ctx context.Context, method, path string, body io.Re
 		req.Header.Add("Content-Type", "application/json")
 	}
 
-	resp, err := http.DefaultClient.Do(req)
+	resp, err := c.httpClient.Do(req)
 	if err != nil {
 		return nil, err
 	}
@@ -124,7 +137,8 @@ func (c *Client) newRequest(ctx context.Context, method, path string, body io.Re
 	if resp.StatusCode > 299 {
 		parsedErr, pErr := parseResponse[util.ErrorResponse](resp)
 		if pErr != nil {
-			return nil, err
+
+			return nil, pErr
 		}
 		return nil, errors.New(parsedErr.Message)
 	}
@@ -135,13 +149,16 @@ func (c *Client) newRequest(ctx context.Context, method, path string, body io.Re
 func parseResponse[T any](resp *http.Response) (T, error) {
 	var ret T
 	if resp.Body == nil {
-		return ret, errors.New("No body")
+		return ret, fmt.Errorf("Body missing, HTTP Error code %d", resp.StatusCode)
 	}
 	bs, err := io.ReadAll(resp.Body)
 	if err != nil {
 		return ret, err
 	}
 	err = json.Unmarshal(bs, &ret)
+	if err != nil {
+		return ret, fmt.Errorf("Error code %d, error unmarshalling body: %w", resp.StatusCode, err)
+	}
 
-	return ret, err
+	return ret, nil
 }
diff --git a/management/client/rest/dns.go b/management/client/rest/dns.go
index ef9923b1..1e35c022 100644
--- a/management/client/rest/dns.go
+++ b/management/client/rest/dns.go
@@ -16,11 +16,13 @@ type DNSAPI struct {
 // ListNameserverGroups list all nameserver groups
 // See more: https://docs.netbird.io/api/resources/dns#list-all-nameserver-groups
 func (a *DNSAPI) ListNameserverGroups(ctx context.Context) ([]api.NameserverGroup, error) {
-	resp, err := a.c.newRequest(ctx, "GET", "/api/dns/nameservers", nil)
+	resp, err := a.c.NewRequest(ctx, "GET", "/api/dns/nameservers", nil)
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[[]api.NameserverGroup](resp)
 	return ret, err
 }
@@ -28,11 +30,13 @@ func (a *DNSAPI) ListNameserverGroups(ctx context.Context) ([]api.NameserverGrou
 // GetNameserverGroup get nameserver group info
 // See more: https://docs.netbird.io/api/resources/dns#retrieve-a-nameserver-group
 func (a *DNSAPI) GetNameserverGroup(ctx context.Context, nameserverGroupID string) (*api.NameserverGroup, error) {
-	resp, err := a.c.newRequest(ctx, "GET", "/api/dns/nameservers/"+nameserverGroupID, nil)
+	resp, err := a.c.NewRequest(ctx, "GET", "/api/dns/nameservers/"+nameserverGroupID, nil)
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.NameserverGroup](resp)
 	return &ret, err
 }
@@ -44,11 +48,13 @@ func (a *DNSAPI) CreateNameserverGroup(ctx context.Context, request api.PostApiD
 	if err != nil {
 		return nil, err
 	}
-	resp, err := a.c.newRequest(ctx, "POST", "/api/dns/nameservers", bytes.NewReader(requestBytes))
+	resp, err := a.c.NewRequest(ctx, "POST", "/api/dns/nameservers", bytes.NewReader(requestBytes))
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.NameserverGroup](resp)
 	return &ret, err
 }
@@ -60,11 +66,13 @@ func (a *DNSAPI) UpdateNameserverGroup(ctx context.Context, nameserverGroupID st
 	if err != nil {
 		return nil, err
 	}
-	resp, err := a.c.newRequest(ctx, "PUT", "/api/dns/nameservers/"+nameserverGroupID, bytes.NewReader(requestBytes))
+	resp, err := a.c.NewRequest(ctx, "PUT", "/api/dns/nameservers/"+nameserverGroupID, bytes.NewReader(requestBytes))
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.NameserverGroup](resp)
 	return &ret, err
 }
@@ -72,11 +80,13 @@ func (a *DNSAPI) UpdateNameserverGroup(ctx context.Context, nameserverGroupID st
 // DeleteNameserverGroup delete nameserver group
 // See more: https://docs.netbird.io/api/resources/dns#delete-a-nameserver-group
 func (a *DNSAPI) DeleteNameserverGroup(ctx context.Context, nameserverGroupID string) error {
-	resp, err := a.c.newRequest(ctx, "DELETE", "/api/dns/nameservers/"+nameserverGroupID, nil)
+	resp, err := a.c.NewRequest(ctx, "DELETE", "/api/dns/nameservers/"+nameserverGroupID, nil)
 	if err != nil {
 		return err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 
 	return nil
 }
@@ -84,11 +94,13 @@ func (a *DNSAPI) DeleteNameserverGroup(ctx context.Context, nameserverGroupID st
 // GetSettings get DNS settings
 // See more: https://docs.netbird.io/api/resources/dns#retrieve-dns-settings
 func (a *DNSAPI) GetSettings(ctx context.Context) (*api.DNSSettings, error) {
-	resp, err := a.c.newRequest(ctx, "GET", "/api/dns/settings", nil)
+	resp, err := a.c.NewRequest(ctx, "GET", "/api/dns/settings", nil)
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.DNSSettings](resp)
 	return &ret, err
 }
@@ -100,11 +112,13 @@ func (a *DNSAPI) UpdateSettings(ctx context.Context, request api.PutApiDnsSettin
 	if err != nil {
 		return nil, err
 	}
-	resp, err := a.c.newRequest(ctx, "PUT", "/api/dns/settings", bytes.NewReader(requestBytes))
+	resp, err := a.c.NewRequest(ctx, "PUT", "/api/dns/settings", bytes.NewReader(requestBytes))
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.DNSSettings](resp)
 	return &ret, err
 }
diff --git a/management/client/rest/events.go b/management/client/rest/events.go
index 1157700f..cae813e8 100644
--- a/management/client/rest/events.go
+++ b/management/client/rest/events.go
@@ -14,11 +14,13 @@ type EventsAPI struct {
 // List list all events
 // See more: https://docs.netbird.io/api/resources/events#list-all-events
 func (a *EventsAPI) List(ctx context.Context) ([]api.Event, error) {
-	resp, err := a.c.newRequest(ctx, "GET", "/api/events", nil)
+	resp, err := a.c.NewRequest(ctx, "GET", "/api/events", nil)
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[[]api.Event](resp)
 	return ret, err
 }
diff --git a/management/client/rest/geo.go b/management/client/rest/geo.go
index ed9090fe..d06d65d8 100644
--- a/management/client/rest/geo.go
+++ b/management/client/rest/geo.go
@@ -14,11 +14,13 @@ type GeoLocationAPI struct {
 // ListCountries list all country codes
 // See more: https://docs.netbird.io/api/resources/geo-locations#list-all-country-codes
 func (a *GeoLocationAPI) ListCountries(ctx context.Context) ([]api.Country, error) {
-	resp, err := a.c.newRequest(ctx, "GET", "/api/locations/countries", nil)
+	resp, err := a.c.NewRequest(ctx, "GET", "/api/locations/countries", nil)
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[[]api.Country](resp)
 	return ret, err
 }
@@ -26,11 +28,13 @@ func (a *GeoLocationAPI) ListCountries(ctx context.Context) ([]api.Country, erro
 // ListCountryCities Get a list of all English city names for a given country code
 // See more: https://docs.netbird.io/api/resources/geo-locations#list-all-city-names-by-country
 func (a *GeoLocationAPI) ListCountryCities(ctx context.Context, countryCode string) ([]api.City, error) {
-	resp, err := a.c.newRequest(ctx, "GET", "/api/locations/countries/"+countryCode+"/cities", nil)
+	resp, err := a.c.NewRequest(ctx, "GET", "/api/locations/countries/"+countryCode+"/cities", nil)
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[[]api.City](resp)
 	return ret, err
 }
diff --git a/management/client/rest/groups.go b/management/client/rest/groups.go
index feb66427..7612b718 100644
--- a/management/client/rest/groups.go
+++ b/management/client/rest/groups.go
@@ -16,11 +16,13 @@ type GroupsAPI struct {
 // List list all groups
 // See more: https://docs.netbird.io/api/resources/groups#list-all-groups
 func (a *GroupsAPI) List(ctx context.Context) ([]api.Group, error) {
-	resp, err := a.c.newRequest(ctx, "GET", "/api/groups", nil)
+	resp, err := a.c.NewRequest(ctx, "GET", "/api/groups", nil)
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[[]api.Group](resp)
 	return ret, err
 }
@@ -28,11 +30,13 @@ func (a *GroupsAPI) List(ctx context.Context) ([]api.Group, error) {
 // Get get group info
 // See more: https://docs.netbird.io/api/resources/groups#retrieve-a-group
 func (a *GroupsAPI) Get(ctx context.Context, groupID string) (*api.Group, error) {
-	resp, err := a.c.newRequest(ctx, "GET", "/api/groups/"+groupID, nil)
+	resp, err := a.c.NewRequest(ctx, "GET", "/api/groups/"+groupID, nil)
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.Group](resp)
 	return &ret, err
 }
@@ -44,11 +48,13 @@ func (a *GroupsAPI) Create(ctx context.Context, request api.PostApiGroupsJSONReq
 	if err != nil {
 		return nil, err
 	}
-	resp, err := a.c.newRequest(ctx, "POST", "/api/groups", bytes.NewReader(requestBytes))
+	resp, err := a.c.NewRequest(ctx, "POST", "/api/groups", bytes.NewReader(requestBytes))
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.Group](resp)
 	return &ret, err
 }
@@ -60,11 +66,13 @@ func (a *GroupsAPI) Update(ctx context.Context, groupID string, request api.PutA
 	if err != nil {
 		return nil, err
 	}
-	resp, err := a.c.newRequest(ctx, "PUT", "/api/groups/"+groupID, bytes.NewReader(requestBytes))
+	resp, err := a.c.NewRequest(ctx, "PUT", "/api/groups/"+groupID, bytes.NewReader(requestBytes))
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.Group](resp)
 	return &ret, err
 }
@@ -72,11 +80,13 @@ func (a *GroupsAPI) Update(ctx context.Context, groupID string, request api.PutA
 // Delete delete group
 // See more: https://docs.netbird.io/api/resources/groups#delete-a-group
 func (a *GroupsAPI) Delete(ctx context.Context, groupID string) error {
-	resp, err := a.c.newRequest(ctx, "DELETE", "/api/groups/"+groupID, nil)
+	resp, err := a.c.NewRequest(ctx, "DELETE", "/api/groups/"+groupID, nil)
 	if err != nil {
 		return err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 
 	return nil
 }
diff --git a/management/client/rest/impersonation.go b/management/client/rest/impersonation.go
new file mode 100644
index 00000000..4d47c937
--- /dev/null
+++ b/management/client/rest/impersonation.go
@@ -0,0 +1,48 @@
+package rest
+
+import (
+	"net/http"
+	"net/url"
+)
+
+// Impersonate returns a Client impersonated for a specific account
+func (c *Client) Impersonate(account string) *Client {
+	client := NewWithOptions(
+		WithManagementURL(c.managementURL),
+		WithAuthHeader(c.authHeader),
+		WithHttpClient(newImpersonatedHttpClient(c, account)),
+	)
+	return client
+}
+
+type impersonatedHttpClient struct {
+	baseClient HttpClient
+	account    string
+}
+
+func newImpersonatedHttpClient(c *Client, account string) *impersonatedHttpClient {
+	if hc, ok := c.httpClient.(*impersonatedHttpClient); ok {
+		hc.account = account
+		return hc
+	}
+
+	return &impersonatedHttpClient{
+		baseClient: c.httpClient,
+		account:    account,
+	}
+}
+
+func (c *impersonatedHttpClient) Do(req *http.Request) (*http.Response, error) {
+	parsedURL, err := url.Parse(req.URL.String())
+	if err != nil {
+		return nil, err
+	}
+
+	query := parsedURL.Query()
+	query.Set("account", c.account)
+	parsedURL.RawQuery = query.Encode()
+
+	req.URL = parsedURL
+
+	return c.baseClient.Do(req)
+}
diff --git a/management/client/rest/networks.go b/management/client/rest/networks.go
index 2cdd6d73..b744e3fe 100644
--- a/management/client/rest/networks.go
+++ b/management/client/rest/networks.go
@@ -16,11 +16,13 @@ type NetworksAPI struct {
 // List list all networks
 // See more: https://docs.netbird.io/api/resources/networks#list-all-networks
 func (a *NetworksAPI) List(ctx context.Context) ([]api.Network, error) {
-	resp, err := a.c.newRequest(ctx, "GET", "/api/networks", nil)
+	resp, err := a.c.NewRequest(ctx, "GET", "/api/networks", nil)
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[[]api.Network](resp)
 	return ret, err
 }
@@ -28,11 +30,13 @@ func (a *NetworksAPI) List(ctx context.Context) ([]api.Network, error) {
 // Get get network info
 // See more: https://docs.netbird.io/api/resources/networks#retrieve-a-network
 func (a *NetworksAPI) Get(ctx context.Context, networkID string) (*api.Network, error) {
-	resp, err := a.c.newRequest(ctx, "GET", "/api/networks/"+networkID, nil)
+	resp, err := a.c.NewRequest(ctx, "GET", "/api/networks/"+networkID, nil)
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.Network](resp)
 	return &ret, err
 }
@@ -44,11 +48,13 @@ func (a *NetworksAPI) Create(ctx context.Context, request api.PostApiNetworksJSO
 	if err != nil {
 		return nil, err
 	}
-	resp, err := a.c.newRequest(ctx, "POST", "/api/networks", bytes.NewReader(requestBytes))
+	resp, err := a.c.NewRequest(ctx, "POST", "/api/networks", bytes.NewReader(requestBytes))
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.Network](resp)
 	return &ret, err
 }
@@ -60,11 +66,13 @@ func (a *NetworksAPI) Update(ctx context.Context, networkID string, request api.
 	if err != nil {
 		return nil, err
 	}
-	resp, err := a.c.newRequest(ctx, "PUT", "/api/networks/"+networkID, bytes.NewReader(requestBytes))
+	resp, err := a.c.NewRequest(ctx, "PUT", "/api/networks/"+networkID, bytes.NewReader(requestBytes))
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.Network](resp)
 	return &ret, err
 }
@@ -72,11 +80,13 @@ func (a *NetworksAPI) Update(ctx context.Context, networkID string, request api.
 // Delete delete network
 // See more: https://docs.netbird.io/api/resources/networks#delete-a-network
 func (a *NetworksAPI) Delete(ctx context.Context, networkID string) error {
-	resp, err := a.c.newRequest(ctx, "DELETE", "/api/networks/"+networkID, nil)
+	resp, err := a.c.NewRequest(ctx, "DELETE", "/api/networks/"+networkID, nil)
 	if err != nil {
 		return err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 
 	return nil
 }
@@ -98,11 +108,13 @@ func (a *NetworksAPI) Resources(networkID string) *NetworkResourcesAPI {
 // List list all resources in networks
 // See more: https://docs.netbird.io/api/resources/networks#list-all-network-resources
 func (a *NetworkResourcesAPI) List(ctx context.Context) ([]api.NetworkResource, error) {
-	resp, err := a.c.newRequest(ctx, "GET", "/api/networks/"+a.networkID+"/resources", nil)
+	resp, err := a.c.NewRequest(ctx, "GET", "/api/networks/"+a.networkID+"/resources", nil)
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[[]api.NetworkResource](resp)
 	return ret, err
 }
@@ -110,11 +122,13 @@ func (a *NetworkResourcesAPI) List(ctx context.Context) ([]api.NetworkResource,
 // Get get network resource info
 // See more: https://docs.netbird.io/api/resources/networks#retrieve-a-network-resource
 func (a *NetworkResourcesAPI) Get(ctx context.Context, networkResourceID string) (*api.NetworkResource, error) {
-	resp, err := a.c.newRequest(ctx, "GET", "/api/networks/"+a.networkID+"/resources/"+networkResourceID, nil)
+	resp, err := a.c.NewRequest(ctx, "GET", "/api/networks/"+a.networkID+"/resources/"+networkResourceID, nil)
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.NetworkResource](resp)
 	return &ret, err
 }
@@ -126,11 +140,13 @@ func (a *NetworkResourcesAPI) Create(ctx context.Context, request api.PostApiNet
 	if err != nil {
 		return nil, err
 	}
-	resp, err := a.c.newRequest(ctx, "POST", "/api/networks/"+a.networkID+"/resources", bytes.NewReader(requestBytes))
+	resp, err := a.c.NewRequest(ctx, "POST", "/api/networks/"+a.networkID+"/resources", bytes.NewReader(requestBytes))
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.NetworkResource](resp)
 	return &ret, err
 }
@@ -142,11 +158,13 @@ func (a *NetworkResourcesAPI) Update(ctx context.Context, networkResourceID stri
 	if err != nil {
 		return nil, err
 	}
-	resp, err := a.c.newRequest(ctx, "PUT", "/api/networks/"+a.networkID+"/resources/"+networkResourceID, bytes.NewReader(requestBytes))
+	resp, err := a.c.NewRequest(ctx, "PUT", "/api/networks/"+a.networkID+"/resources/"+networkResourceID, bytes.NewReader(requestBytes))
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.NetworkResource](resp)
 	return &ret, err
 }
@@ -154,11 +172,13 @@ func (a *NetworkResourcesAPI) Update(ctx context.Context, networkResourceID stri
 // Delete delete network resource
 // See more: https://docs.netbird.io/api/resources/networks#delete-a-network-resource
 func (a *NetworkResourcesAPI) Delete(ctx context.Context, networkResourceID string) error {
-	resp, err := a.c.newRequest(ctx, "DELETE", "/api/networks/"+a.networkID+"/resources/"+networkResourceID, nil)
+	resp, err := a.c.NewRequest(ctx, "DELETE", "/api/networks/"+a.networkID+"/resources/"+networkResourceID, nil)
 	if err != nil {
 		return err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 
 	return nil
 }
@@ -180,11 +200,13 @@ func (a *NetworksAPI) Routers(networkID string) *NetworkRoutersAPI {
 // List list all routers in networks
 // See more: https://docs.netbird.io/api/routers/networks#list-all-network-routers
 func (a *NetworkRoutersAPI) List(ctx context.Context) ([]api.NetworkRouter, error) {
-	resp, err := a.c.newRequest(ctx, "GET", "/api/networks/"+a.networkID+"/routers", nil)
+	resp, err := a.c.NewRequest(ctx, "GET", "/api/networks/"+a.networkID+"/routers", nil)
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[[]api.NetworkRouter](resp)
 	return ret, err
 }
@@ -192,11 +214,13 @@ func (a *NetworkRoutersAPI) List(ctx context.Context) ([]api.NetworkRouter, erro
 // Get get network router info
 // See more: https://docs.netbird.io/api/routers/networks#retrieve-a-network-router
 func (a *NetworkRoutersAPI) Get(ctx context.Context, networkRouterID string) (*api.NetworkRouter, error) {
-	resp, err := a.c.newRequest(ctx, "GET", "/api/networks/"+a.networkID+"/routers/"+networkRouterID, nil)
+	resp, err := a.c.NewRequest(ctx, "GET", "/api/networks/"+a.networkID+"/routers/"+networkRouterID, nil)
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.NetworkRouter](resp)
 	return &ret, err
 }
@@ -208,11 +232,13 @@ func (a *NetworkRoutersAPI) Create(ctx context.Context, request api.PostApiNetwo
 	if err != nil {
 		return nil, err
 	}
-	resp, err := a.c.newRequest(ctx, "POST", "/api/networks/"+a.networkID+"/routers", bytes.NewReader(requestBytes))
+	resp, err := a.c.NewRequest(ctx, "POST", "/api/networks/"+a.networkID+"/routers", bytes.NewReader(requestBytes))
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.NetworkRouter](resp)
 	return &ret, err
 }
@@ -224,11 +250,13 @@ func (a *NetworkRoutersAPI) Update(ctx context.Context, networkRouterID string,
 	if err != nil {
 		return nil, err
 	}
-	resp, err := a.c.newRequest(ctx, "PUT", "/api/networks/"+a.networkID+"/routers/"+networkRouterID, bytes.NewReader(requestBytes))
+	resp, err := a.c.NewRequest(ctx, "PUT", "/api/networks/"+a.networkID+"/routers/"+networkRouterID, bytes.NewReader(requestBytes))
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.NetworkRouter](resp)
 	return &ret, err
 }
@@ -236,11 +264,13 @@ func (a *NetworkRoutersAPI) Update(ctx context.Context, networkRouterID string,
 // Delete delete network router
 // See more: https://docs.netbird.io/api/routers/networks#delete-a-network-router
 func (a *NetworkRoutersAPI) Delete(ctx context.Context, networkRouterID string) error {
-	resp, err := a.c.newRequest(ctx, "DELETE", "/api/networks/"+a.networkID+"/routers/"+networkRouterID, nil)
+	resp, err := a.c.NewRequest(ctx, "DELETE", "/api/networks/"+a.networkID+"/routers/"+networkRouterID, nil)
 	if err != nil {
 		return err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 
 	return nil
 }
diff --git a/management/client/rest/options.go b/management/client/rest/options.go
new file mode 100644
index 00000000..21f2394e
--- /dev/null
+++ b/management/client/rest/options.go
@@ -0,0 +1,44 @@
+package rest
+
+import "net/http"
+
+// option modifier for creation of Client
+type option func(*Client)
+
+// HTTPClient interface for HTTP client
+type HttpClient interface {
+	Do(req *http.Request) (*http.Response, error)
+}
+
+// WithHTTPClient overrides HTTPClient used
+func WithHttpClient(client HttpClient) option {
+	return func(c *Client) {
+		c.httpClient = client
+	}
+}
+
+// WithBearerToken uses provided bearer token acquired from SSO for authentication
+func WithBearerToken(token string) option {
+	return WithAuthHeader("Bearer " + token)
+}
+
+// WithPAT uses provided Personal Access Token
+// (created from NetBird Management Dashboard) for authentication
+func WithPAT(token string) option {
+	return WithAuthHeader("Token " + token)
+}
+
+// WithManagementURL overrides target NetBird Management server
+func WithManagementURL(url string) option {
+	return func(c *Client) {
+		c.managementURL = url
+	}
+}
+
+// WithAuthHeader overrides auth header completely, this should generally not be used
+// and WithBearerToken or WithPAT should be used instead
+func WithAuthHeader(value string) option {
+	return func(c *Client) {
+		c.authHeader = value
+	}
+}
diff --git a/management/client/rest/peers.go b/management/client/rest/peers.go
index 9d35f013..37679fdb 100644
--- a/management/client/rest/peers.go
+++ b/management/client/rest/peers.go
@@ -16,11 +16,13 @@ type PeersAPI struct {
 // List list all peers
 // See more: https://docs.netbird.io/api/resources/peers#list-all-peers
 func (a *PeersAPI) List(ctx context.Context) ([]api.Peer, error) {
-	resp, err := a.c.newRequest(ctx, "GET", "/api/peers", nil)
+	resp, err := a.c.NewRequest(ctx, "GET", "/api/peers", nil)
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[[]api.Peer](resp)
 	return ret, err
 }
@@ -28,11 +30,13 @@ func (a *PeersAPI) List(ctx context.Context) ([]api.Peer, error) {
 // Get retrieve a peer
 // See more: https://docs.netbird.io/api/resources/peers#retrieve-a-peer
 func (a *PeersAPI) Get(ctx context.Context, peerID string) (*api.Peer, error) {
-	resp, err := a.c.newRequest(ctx, "GET", "/api/peers/"+peerID, nil)
+	resp, err := a.c.NewRequest(ctx, "GET", "/api/peers/"+peerID, nil)
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.Peer](resp)
 	return &ret, err
 }
@@ -44,11 +48,13 @@ func (a *PeersAPI) Update(ctx context.Context, peerID string, request api.PutApi
 	if err != nil {
 		return nil, err
 	}
-	resp, err := a.c.newRequest(ctx, "PUT", "/api/peers/"+peerID, bytes.NewReader(requestBytes))
+	resp, err := a.c.NewRequest(ctx, "PUT", "/api/peers/"+peerID, bytes.NewReader(requestBytes))
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.Peer](resp)
 	return &ret, err
 }
@@ -56,11 +62,13 @@ func (a *PeersAPI) Update(ctx context.Context, peerID string, request api.PutApi
 // Delete delete a peer
 // See more: https://docs.netbird.io/api/resources/peers#delete-a-peer
 func (a *PeersAPI) Delete(ctx context.Context, peerID string) error {
-	resp, err := a.c.newRequest(ctx, "DELETE", "/api/peers/"+peerID, nil)
+	resp, err := a.c.NewRequest(ctx, "DELETE", "/api/peers/"+peerID, nil)
 	if err != nil {
 		return err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 
 	return nil
 }
@@ -68,11 +76,13 @@ func (a *PeersAPI) Delete(ctx context.Context, peerID string) error {
 // ListAccessiblePeers list all peers that the specified peer can connect to within the network
 // See more: https://docs.netbird.io/api/resources/peers#list-accessible-peers
 func (a *PeersAPI) ListAccessiblePeers(ctx context.Context, peerID string) ([]api.Peer, error) {
-	resp, err := a.c.newRequest(ctx, "GET", "/api/peers/"+peerID+"/accessible-peers", nil)
+	resp, err := a.c.NewRequest(ctx, "GET", "/api/peers/"+peerID+"/accessible-peers", nil)
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[[]api.Peer](resp)
 	return ret, err
 }
diff --git a/management/client/rest/policies.go b/management/client/rest/policies.go
index be6abafa..2f2df4a7 100644
--- a/management/client/rest/policies.go
+++ b/management/client/rest/policies.go
@@ -16,11 +16,15 @@ type PoliciesAPI struct {
 // List list all policies
 // See more: https://docs.netbird.io/api/resources/policies#list-all-policies
 func (a *PoliciesAPI) List(ctx context.Context) ([]api.Policy, error) {
-	resp, err := a.c.newRequest(ctx, "GET", "/api/policies", nil)
+	path := "/api/policies"
+
+	resp, err := a.c.NewRequest(ctx, "GET", path, nil)
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[[]api.Policy](resp)
 	return ret, err
 }
@@ -28,11 +32,13 @@ func (a *PoliciesAPI) List(ctx context.Context) ([]api.Policy, error) {
 // Get get policy info
 // See more: https://docs.netbird.io/api/resources/policies#retrieve-a-policy
 func (a *PoliciesAPI) Get(ctx context.Context, policyID string) (*api.Policy, error) {
-	resp, err := a.c.newRequest(ctx, "GET", "/api/policies/"+policyID, nil)
+	resp, err := a.c.NewRequest(ctx, "GET", "/api/policies/"+policyID, nil)
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.Policy](resp)
 	return &ret, err
 }
@@ -44,11 +50,13 @@ func (a *PoliciesAPI) Create(ctx context.Context, request api.PostApiPoliciesJSO
 	if err != nil {
 		return nil, err
 	}
-	resp, err := a.c.newRequest(ctx, "POST", "/api/policies", bytes.NewReader(requestBytes))
+	resp, err := a.c.NewRequest(ctx, "POST", "/api/policies", bytes.NewReader(requestBytes))
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.Policy](resp)
 	return &ret, err
 }
@@ -56,15 +64,19 @@ func (a *PoliciesAPI) Create(ctx context.Context, request api.PostApiPoliciesJSO
 // Update update policy info
 // See more: https://docs.netbird.io/api/resources/policies#update-a-policy
 func (a *PoliciesAPI) Update(ctx context.Context, policyID string, request api.PutApiPoliciesPolicyIdJSONRequestBody) (*api.Policy, error) {
+	path := "/api/policies/" + policyID
+
 	requestBytes, err := json.Marshal(request)
 	if err != nil {
 		return nil, err
 	}
-	resp, err := a.c.newRequest(ctx, "PUT", "/api/policies/"+policyID, bytes.NewReader(requestBytes))
+	resp, err := a.c.NewRequest(ctx, "PUT", path, bytes.NewReader(requestBytes))
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.Policy](resp)
 	return &ret, err
 }
@@ -72,11 +84,13 @@ func (a *PoliciesAPI) Update(ctx context.Context, policyID string, request api.P
 // Delete delete policy
 // See more: https://docs.netbird.io/api/resources/policies#delete-a-policy
 func (a *PoliciesAPI) Delete(ctx context.Context, policyID string) error {
-	resp, err := a.c.newRequest(ctx, "DELETE", "/api/policies/"+policyID, nil)
+	resp, err := a.c.NewRequest(ctx, "DELETE", "/api/policies/"+policyID, nil)
 	if err != nil {
 		return err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 
 	return nil
 }
diff --git a/management/client/rest/posturechecks.go b/management/client/rest/posturechecks.go
index 950d17ba..622eeeb6 100644
--- a/management/client/rest/posturechecks.go
+++ b/management/client/rest/posturechecks.go
@@ -16,11 +16,13 @@ type PostureChecksAPI struct {
 // List list all posture checks
 // See more: https://docs.netbird.io/api/resources/posture-checks#list-all-posture-checks
 func (a *PostureChecksAPI) List(ctx context.Context) ([]api.PostureCheck, error) {
-	resp, err := a.c.newRequest(ctx, "GET", "/api/posture-checks", nil)
+	resp, err := a.c.NewRequest(ctx, "GET", "/api/posture-checks", nil)
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[[]api.PostureCheck](resp)
 	return ret, err
 }
@@ -28,11 +30,13 @@ func (a *PostureChecksAPI) List(ctx context.Context) ([]api.PostureCheck, error)
 // Get get posture check info
 // See more: https://docs.netbird.io/api/resources/posture-checks#retrieve-a-posture-check
 func (a *PostureChecksAPI) Get(ctx context.Context, postureCheckID string) (*api.PostureCheck, error) {
-	resp, err := a.c.newRequest(ctx, "GET", "/api/posture-checks/"+postureCheckID, nil)
+	resp, err := a.c.NewRequest(ctx, "GET", "/api/posture-checks/"+postureCheckID, nil)
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.PostureCheck](resp)
 	return &ret, err
 }
@@ -44,11 +48,13 @@ func (a *PostureChecksAPI) Create(ctx context.Context, request api.PostApiPostur
 	if err != nil {
 		return nil, err
 	}
-	resp, err := a.c.newRequest(ctx, "POST", "/api/posture-checks", bytes.NewReader(requestBytes))
+	resp, err := a.c.NewRequest(ctx, "POST", "/api/posture-checks", bytes.NewReader(requestBytes))
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.PostureCheck](resp)
 	return &ret, err
 }
@@ -60,11 +66,13 @@ func (a *PostureChecksAPI) Update(ctx context.Context, postureCheckID string, re
 	if err != nil {
 		return nil, err
 	}
-	resp, err := a.c.newRequest(ctx, "PUT", "/api/posture-checks/"+postureCheckID, bytes.NewReader(requestBytes))
+	resp, err := a.c.NewRequest(ctx, "PUT", "/api/posture-checks/"+postureCheckID, bytes.NewReader(requestBytes))
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.PostureCheck](resp)
 	return &ret, err
 }
@@ -72,11 +80,13 @@ func (a *PostureChecksAPI) Update(ctx context.Context, postureCheckID string, re
 // Delete delete posture check
 // See more: https://docs.netbird.io/api/resources/posture-checks#delete-a-posture-check
 func (a *PostureChecksAPI) Delete(ctx context.Context, postureCheckID string) error {
-	resp, err := a.c.newRequest(ctx, "DELETE", "/api/posture-checks/"+postureCheckID, nil)
+	resp, err := a.c.NewRequest(ctx, "DELETE", "/api/posture-checks/"+postureCheckID, nil)
 	if err != nil {
 		return err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 
 	return nil
 }
diff --git a/management/client/rest/routes.go b/management/client/rest/routes.go
index bccbb884..671c3bfc 100644
--- a/management/client/rest/routes.go
+++ b/management/client/rest/routes.go
@@ -16,11 +16,13 @@ type RoutesAPI struct {
 // List list all routes
 // See more: https://docs.netbird.io/api/resources/routes#list-all-routes
 func (a *RoutesAPI) List(ctx context.Context) ([]api.Route, error) {
-	resp, err := a.c.newRequest(ctx, "GET", "/api/routes", nil)
+	resp, err := a.c.NewRequest(ctx, "GET", "/api/routes", nil)
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[[]api.Route](resp)
 	return ret, err
 }
@@ -28,11 +30,13 @@ func (a *RoutesAPI) List(ctx context.Context) ([]api.Route, error) {
 // Get get route info
 // See more: https://docs.netbird.io/api/resources/routes#retrieve-a-route
 func (a *RoutesAPI) Get(ctx context.Context, routeID string) (*api.Route, error) {
-	resp, err := a.c.newRequest(ctx, "GET", "/api/routes/"+routeID, nil)
+	resp, err := a.c.NewRequest(ctx, "GET", "/api/routes/"+routeID, nil)
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.Route](resp)
 	return &ret, err
 }
@@ -44,11 +48,13 @@ func (a *RoutesAPI) Create(ctx context.Context, request api.PostApiRoutesJSONReq
 	if err != nil {
 		return nil, err
 	}
-	resp, err := a.c.newRequest(ctx, "POST", "/api/routes", bytes.NewReader(requestBytes))
+	resp, err := a.c.NewRequest(ctx, "POST", "/api/routes", bytes.NewReader(requestBytes))
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.Route](resp)
 	return &ret, err
 }
@@ -60,11 +66,13 @@ func (a *RoutesAPI) Update(ctx context.Context, routeID string, request api.PutA
 	if err != nil {
 		return nil, err
 	}
-	resp, err := a.c.newRequest(ctx, "PUT", "/api/routes/"+routeID, bytes.NewReader(requestBytes))
+	resp, err := a.c.NewRequest(ctx, "PUT", "/api/routes/"+routeID, bytes.NewReader(requestBytes))
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.Route](resp)
 	return &ret, err
 }
@@ -72,11 +80,13 @@ func (a *RoutesAPI) Update(ctx context.Context, routeID string, request api.PutA
 // Delete delete route
 // See more: https://docs.netbird.io/api/resources/routes#delete-a-route
 func (a *RoutesAPI) Delete(ctx context.Context, routeID string) error {
-	resp, err := a.c.newRequest(ctx, "DELETE", "/api/routes/"+routeID, nil)
+	resp, err := a.c.NewRequest(ctx, "DELETE", "/api/routes/"+routeID, nil)
 	if err != nil {
 		return err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 
 	return nil
 }
diff --git a/management/client/rest/setupkeys.go b/management/client/rest/setupkeys.go
index 645614fc..5625b6ac 100644
--- a/management/client/rest/setupkeys.go
+++ b/management/client/rest/setupkeys.go
@@ -16,11 +16,13 @@ type SetupKeysAPI struct {
 // List list all setup keys
 // See more: https://docs.netbird.io/api/resources/setup-keys#list-all-setup-keys
 func (a *SetupKeysAPI) List(ctx context.Context) ([]api.SetupKey, error) {
-	resp, err := a.c.newRequest(ctx, "GET", "/api/setup-keys", nil)
+	resp, err := a.c.NewRequest(ctx, "GET", "/api/setup-keys", nil)
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[[]api.SetupKey](resp)
 	return ret, err
 }
@@ -28,11 +30,13 @@ func (a *SetupKeysAPI) List(ctx context.Context) ([]api.SetupKey, error) {
 // Get get setup key info
 // See more: https://docs.netbird.io/api/resources/setup-keys#retrieve-a-setup-key
 func (a *SetupKeysAPI) Get(ctx context.Context, setupKeyID string) (*api.SetupKey, error) {
-	resp, err := a.c.newRequest(ctx, "GET", "/api/setup-keys/"+setupKeyID, nil)
+	resp, err := a.c.NewRequest(ctx, "GET", "/api/setup-keys/"+setupKeyID, nil)
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.SetupKey](resp)
 	return &ret, err
 }
@@ -40,15 +44,19 @@ func (a *SetupKeysAPI) Get(ctx context.Context, setupKeyID string) (*api.SetupKe
 // Create generate new Setup Key
 // See more: https://docs.netbird.io/api/resources/setup-keys#create-a-setup-key
 func (a *SetupKeysAPI) Create(ctx context.Context, request api.PostApiSetupKeysJSONRequestBody) (*api.SetupKeyClear, error) {
+	path := "/api/setup-keys"
+
 	requestBytes, err := json.Marshal(request)
 	if err != nil {
 		return nil, err
 	}
-	resp, err := a.c.newRequest(ctx, "POST", "/api/setup-keys", bytes.NewReader(requestBytes))
+	resp, err := a.c.NewRequest(ctx, "POST", path, bytes.NewReader(requestBytes))
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.SetupKeyClear](resp)
 	return &ret, err
 }
@@ -60,11 +68,13 @@ func (a *SetupKeysAPI) Update(ctx context.Context, setupKeyID string, request ap
 	if err != nil {
 		return nil, err
 	}
-	resp, err := a.c.newRequest(ctx, "PUT", "/api/setup-keys/"+setupKeyID, bytes.NewReader(requestBytes))
+	resp, err := a.c.NewRequest(ctx, "PUT", "/api/setup-keys/"+setupKeyID, bytes.NewReader(requestBytes))
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.SetupKey](resp)
 	return &ret, err
 }
@@ -72,11 +82,13 @@ func (a *SetupKeysAPI) Update(ctx context.Context, setupKeyID string, request ap
 // Delete delete setup key
 // See more: https://docs.netbird.io/api/resources/setup-keys#delete-a-setup-key
 func (a *SetupKeysAPI) Delete(ctx context.Context, setupKeyID string) error {
-	resp, err := a.c.newRequest(ctx, "DELETE", "/api/setup-keys/"+setupKeyID, nil)
+	resp, err := a.c.NewRequest(ctx, "DELETE", "/api/setup-keys/"+setupKeyID, nil)
 	if err != nil {
 		return err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 
 	return nil
 }
diff --git a/management/client/rest/tokens.go b/management/client/rest/tokens.go
index 3275bea8..278a0d15 100644
--- a/management/client/rest/tokens.go
+++ b/management/client/rest/tokens.go
@@ -16,11 +16,13 @@ type TokensAPI struct {
 // List list user tokens
 // See more: https://docs.netbird.io/api/resources/tokens#list-all-tokens
 func (a *TokensAPI) List(ctx context.Context, userID string) ([]api.PersonalAccessToken, error) {
-	resp, err := a.c.newRequest(ctx, "GET", "/api/users/"+userID+"/tokens", nil)
+	resp, err := a.c.NewRequest(ctx, "GET", "/api/users/"+userID+"/tokens", nil)
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[[]api.PersonalAccessToken](resp)
 	return ret, err
 }
@@ -28,11 +30,13 @@ func (a *TokensAPI) List(ctx context.Context, userID string) ([]api.PersonalAcce
 // Get get user token info
 // See more: https://docs.netbird.io/api/resources/tokens#retrieve-a-token
 func (a *TokensAPI) Get(ctx context.Context, userID, tokenID string) (*api.PersonalAccessToken, error) {
-	resp, err := a.c.newRequest(ctx, "GET", "/api/users/"+userID+"/tokens/"+tokenID, nil)
+	resp, err := a.c.NewRequest(ctx, "GET", "/api/users/"+userID+"/tokens/"+tokenID, nil)
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.PersonalAccessToken](resp)
 	return &ret, err
 }
@@ -44,11 +48,13 @@ func (a *TokensAPI) Create(ctx context.Context, userID string, request api.PostA
 	if err != nil {
 		return nil, err
 	}
-	resp, err := a.c.newRequest(ctx, "POST", "/api/users/"+userID+"/tokens", bytes.NewReader(requestBytes))
+	resp, err := a.c.NewRequest(ctx, "POST", "/api/users/"+userID+"/tokens", bytes.NewReader(requestBytes))
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.PersonalAccessTokenGenerated](resp)
 	return &ret, err
 }
@@ -56,11 +62,13 @@ func (a *TokensAPI) Create(ctx context.Context, userID string, request api.PostA
 // Delete delete user token
 // See more: https://docs.netbird.io/api/resources/tokens#delete-a-token
 func (a *TokensAPI) Delete(ctx context.Context, userID, tokenID string) error {
-	resp, err := a.c.newRequest(ctx, "DELETE", "/api/users/"+userID+"/tokens/"+tokenID, nil)
+	resp, err := a.c.NewRequest(ctx, "DELETE", "/api/users/"+userID+"/tokens/"+tokenID, nil)
 	if err != nil {
 		return err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 
 	return nil
 }
diff --git a/management/client/rest/users.go b/management/client/rest/users.go
index 31ffad05..107b0581 100644
--- a/management/client/rest/users.go
+++ b/management/client/rest/users.go
@@ -16,11 +16,13 @@ type UsersAPI struct {
 // List list all users, only returns one user always
 // See more: https://docs.netbird.io/api/resources/users#list-all-users
 func (a *UsersAPI) List(ctx context.Context) ([]api.User, error) {
-	resp, err := a.c.newRequest(ctx, "GET", "/api/users", nil)
+	resp, err := a.c.NewRequest(ctx, "GET", "/api/users", nil)
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[[]api.User](resp)
 	return ret, err
 }
@@ -32,11 +34,13 @@ func (a *UsersAPI) Create(ctx context.Context, request api.PostApiUsersJSONReque
 	if err != nil {
 		return nil, err
 	}
-	resp, err := a.c.newRequest(ctx, "POST", "/api/users", bytes.NewReader(requestBytes))
+	resp, err := a.c.NewRequest(ctx, "POST", "/api/users", bytes.NewReader(requestBytes))
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.User](resp)
 	return &ret, err
 }
@@ -48,11 +52,13 @@ func (a *UsersAPI) Update(ctx context.Context, userID string, request api.PutApi
 	if err != nil {
 		return nil, err
 	}
-	resp, err := a.c.newRequest(ctx, "PUT", "/api/users/"+userID, bytes.NewReader(requestBytes))
+	resp, err := a.c.NewRequest(ctx, "PUT", "/api/users/"+userID, bytes.NewReader(requestBytes))
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 	ret, err := parseResponse[api.User](resp)
 	return &ret, err
 }
@@ -60,11 +66,13 @@ func (a *UsersAPI) Update(ctx context.Context, userID string, request api.PutApi
 // Delete delete user
 // See more: https://docs.netbird.io/api/resources/users#delete-a-user
 func (a *UsersAPI) Delete(ctx context.Context, userID string) error {
-	resp, err := a.c.newRequest(ctx, "DELETE", "/api/users/"+userID, nil)
+	resp, err := a.c.NewRequest(ctx, "DELETE", "/api/users/"+userID, nil)
 	if err != nil {
 		return err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 
 	return nil
 }
@@ -72,11 +80,13 @@ func (a *UsersAPI) Delete(ctx context.Context, userID string) error {
 // ResendInvitation resend user invitation
 // See more: https://docs.netbird.io/api/resources/users#resend-user-invitation
 func (a *UsersAPI) ResendInvitation(ctx context.Context, userID string) error {
-	resp, err := a.c.newRequest(ctx, "POST", "/api/users/"+userID+"/invite", nil)
+	resp, err := a.c.NewRequest(ctx, "POST", "/api/users/"+userID+"/invite", nil)
 	if err != nil {
 		return err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 
 	return nil
 }
@@ -84,11 +94,13 @@ func (a *UsersAPI) ResendInvitation(ctx context.Context, userID string) error {
 // 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)
+	resp, err := a.c.NewRequest(ctx, "GET", "/api/users/current", nil)
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+	if resp.Body != nil {
+		defer resp.Body.Close()
+	}
 
 	ret, err := parseResponse[api.User](resp)
 	return &ret, err
diff --git a/management/cmd/management.go b/management/cmd/management.go
index d6735f95..5fb07890 100644
--- a/management/cmd/management.go
+++ b/management/cmd/management.go
@@ -159,7 +159,7 @@ var (
 			if err != nil {
 				return err
 			}
-			store, err := store.NewStore(ctx, config.StoreConfig.Engine, config.Datadir, appMetrics)
+			store, err := store.NewStore(ctx, config.StoreConfig.Engine, config.Datadir, appMetrics, false)
 			if err != nil {
 				return fmt.Errorf("failed creating Store: %s: %v", config.Datadir, err)
 			}
diff --git a/management/domain/domain.go b/management/domain/domain.go
index 2e089b01..97acec68 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 b6090c71..a988f4f7 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 bcbf26e0..bf2af711 100644
--- a/management/domain/validate.go
+++ b/management/domain/validate.go
@@ -8,6 +8,8 @@ import (
 
 const maxDomains = 32
 
+var domainRegex = regexp.MustCompile(`^(?:\*\.)?(?:(?:xn--)?[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?\.)*(?:xn--)?[a-zA-Z0-9](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?$`)
+
 // ValidateDomains checks if each domain in the list is valid and returns a punycode-encoded DomainList.
 func ValidateDomains(domains []string) (List, error) {
 	if len(domains) == 0 {
@@ -17,13 +19,9 @@ func ValidateDomains(domains []string) (List, error) {
 		return nil, fmt.Errorf("domains list exceeds maximum allowed domains: %d", maxDomains)
 	}
 
-	domainRegex := regexp.MustCompile(`^(?:\*\.)?(?:(?:xn--)?[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?\.)*(?:xn--)?[a-zA-Z0-9](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?$`)
-
 	var domainList List
 
 	for _, d := range domains {
-		d := strings.ToLower(d)
-
 		// handles length and idna conversion
 		punycode, err := FromString(d)
 		if err != nil {
@@ -39,27 +37,20 @@ func ValidateDomains(domains []string) (List, error) {
 	return domainList, nil
 }
 
-// ValidateDomainsStrSlice checks if each domain in the list is valid
-func ValidateDomainsStrSlice(domains []string) ([]string, error) {
+// ValidateDomainsList checks if each domain in the list is valid
+func ValidateDomainsList(domains []string) error {
 	if len(domains) == 0 {
-		return nil, nil
+		return nil
 	}
 	if len(domains) > maxDomains {
-		return nil, fmt.Errorf("domains list exceeds maximum allowed domains: %d", maxDomains)
+		return fmt.Errorf("domains list exceeds maximum allowed domains: %d", maxDomains)
 	}
 
-	domainRegex := regexp.MustCompile(`^(?:\*\.)?(?:(?:xn--)?[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?\.)*(?:xn--)?[a-zA-Z0-9](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?$`)
-
-	var domainList []string
-
 	for _, d := range domains {
 		d := strings.ToLower(d)
-
 		if !domainRegex.MatchString(d) {
-			return domainList, fmt.Errorf("invalid domain format: %s", d)
+			return fmt.Errorf("invalid domain format: %s", d)
 		}
-
-		domainList = append(domainList, d)
 	}
-	return domainList, nil
+	return nil
 }
diff --git a/management/proto/management.pb.go b/management/proto/management.pb.go
index f3f53bfd..11e72d6e 100644
--- a/management/proto/management.pb.go
+++ b/management/proto/management.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.26.0
-// 	protoc        v3.21.9
+// 	protoc        v3.21.12
 // source: management.proto
 
 package proto
@@ -1624,6 +1624,7 @@ type PeerConfig struct {
 	// Peer fully qualified domain name
 	Fqdn                            string `protobuf:"bytes,4,opt,name=fqdn,proto3" json:"fqdn,omitempty"`
 	RoutingPeerDnsResolutionEnabled bool   `protobuf:"varint,5,opt,name=RoutingPeerDnsResolutionEnabled,proto3" json:"RoutingPeerDnsResolutionEnabled,omitempty"`
+	LazyConnectionEnabled           bool   `protobuf:"varint,6,opt,name=LazyConnectionEnabled,proto3" json:"LazyConnectionEnabled,omitempty"`
 }
 
 func (x *PeerConfig) Reset() {
@@ -1693,6 +1694,13 @@ func (x *PeerConfig) GetRoutingPeerDnsResolutionEnabled() bool {
 	return false
 }
 
+func (x *PeerConfig) GetLazyConnectionEnabled() bool {
+	if x != nil {
+		return x.LazyConnectionEnabled
+	}
+	return false
+}
+
 // NetworkMap represents a network state of the peer with the corresponding configuration parameters to establish peer-to-peer connections
 type NetworkMap struct {
 	state         protoimpl.MessageState
@@ -1856,7 +1864,8 @@ type RemotePeerConfig struct {
 	// SSHConfig is a SSH config of the remote peer. SSHConfig.sshPubKey should be ignored because peer knows it's SSH key.
 	SshConfig *SSHConfig `protobuf:"bytes,3,opt,name=sshConfig,proto3" json:"sshConfig,omitempty"`
 	// Peer fully qualified domain name
-	Fqdn string `protobuf:"bytes,4,opt,name=fqdn,proto3" json:"fqdn,omitempty"`
+	Fqdn         string `protobuf:"bytes,4,opt,name=fqdn,proto3" json:"fqdn,omitempty"`
+	AgentVersion string `protobuf:"bytes,5,opt,name=agentVersion,proto3" json:"agentVersion,omitempty"`
 }
 
 func (x *RemotePeerConfig) Reset() {
@@ -1919,6 +1928,13 @@ func (x *RemotePeerConfig) GetFqdn() string {
 	return ""
 }
 
+func (x *RemotePeerConfig) GetAgentVersion() string {
+	if x != nil {
+		return x.AgentVersion
+	}
+	return ""
+}
+
 // SSHConfig represents SSH configurations of a peer.
 type SSHConfig struct {
 	state         protoimpl.MessageState
@@ -2194,6 +2210,8 @@ type ProviderConfig struct {
 	RedirectURLs []string `protobuf:"bytes,10,rep,name=RedirectURLs,proto3" json:"RedirectURLs,omitempty"`
 	// DisablePromptLogin makes the PKCE flow to not prompt the user for login
 	DisablePromptLogin bool `protobuf:"varint,11,opt,name=DisablePromptLogin,proto3" json:"DisablePromptLogin,omitempty"`
+	// LoginFlags sets the PKCE flow login details
+	LoginFlag uint32 `protobuf:"varint,12,opt,name=LoginFlag,proto3" json:"LoginFlag,omitempty"`
 }
 
 func (x *ProviderConfig) Reset() {
@@ -2305,6 +2323,13 @@ func (x *ProviderConfig) GetDisablePromptLogin() bool {
 	return false
 }
 
+func (x *ProviderConfig) GetLoginFlag() uint32 {
+	if x != nil {
+		return x.LoginFlag
+	}
+	return 0
+}
+
 // Route represents a route.Route object
 type Route struct {
 	state         protoimpl.MessageState
@@ -3057,6 +3082,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 +3181,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
@@ -3494,7 +3528,7 @@ var file_management_proto_rawDesc = []byte{
 	0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72,
 	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08,
 	0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
-	0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0xcb, 0x01, 0x0a, 0x0a, 0x50, 0x65, 0x65,
+	0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x81, 0x02, 0x0a, 0x0a, 0x50, 0x65, 0x65,
 	0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65,
 	0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73,
 	0x73, 0x12, 0x10, 0x0a, 0x03, 0x64, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03,
@@ -3507,284 +3541,293 @@ var file_management_proto_rawDesc = []byte{
 	0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18,
 	0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1f, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x50, 0x65,
 	0x65, 0x72, 0x44, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45,
-	0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0xb9, 0x05, 0x0a, 0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f,
-	0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18,
-	0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x36, 0x0a,
-	0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28,
-	0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50,
-	0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43,
-	0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3e, 0x0a, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50,
-	0x65, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e,
-	0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65,
-	0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65,
-	0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50,
-	0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28,
-	0x08, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73,
-	0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18,
-	0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
-	0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73,
-	0x12, 0x33, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x06, 0x20,
-	0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
-	0x2e, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x44, 0x4e, 0x53, 0x43,
-	0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x40, 0x0a, 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65,
-	0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61,
-	0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50,
-	0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69,
-	0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x3e, 0x0a, 0x0d, 0x46, 0x69, 0x72, 0x65, 0x77,
-	0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18,
-	0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65,
-	0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61,
-	0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x66, 0x69, 0x72, 0x65, 0x77,
-	0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18,
-	0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52,
-	0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x4f, 0x0a, 0x13, 0x72,
+	0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x15, 0x4c, 0x61, 0x7a, 0x79, 0x43, 0x6f,
+	0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18,
+	0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x15, 0x4c, 0x61, 0x7a, 0x79, 0x43, 0x6f, 0x6e, 0x6e, 0x65,
+	0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0xb9, 0x05, 0x0a,
+	0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x53,
+	0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x53, 0x65, 0x72,
+	0x69, 0x61, 0x6c, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69,
+	0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
+	0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52,
+	0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3e, 0x0a, 0x0b, 0x72,
+	0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b,
+	0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65,
+	0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b,
+	0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x72,
+	0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74,
+	0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50,
+	0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x06, 0x52,
+	0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61,
+	0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x06,
+	0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e,
+	0x66, 0x69, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
+	0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
+	0x52, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x40, 0x0a, 0x0c, 0x6f,
+	0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28,
+	0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52,
+	0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52,
+	0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x3e, 0x0a,
+	0x0d, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x08,
+	0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
+	0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d,
+	0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x32, 0x0a,
+	0x14, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73,
+	0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x66, 0x69, 0x72,
+	0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74,
+	0x79, 0x12, 0x4f, 0x0a, 0x13, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77,
+	0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d,
+	0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74,
+	0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x13, 0x72,
 	0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c,
-	0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
-	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77,
-	0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x13, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46,
-	0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x1a,
-	0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75,
-	0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08,
-	0x52, 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c,
-	0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x44, 0x0a, 0x0f,
-	0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18,
-	0x0c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
-	0x6e, 0x74, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c,
-	0x65, 0x52, 0x0f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c,
-	0x65, 0x73, 0x22, 0x97, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65,
-	0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62,
-	0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62,
-	0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70,
-	0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64,
-	0x49, 0x70, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
-	0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
-	0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x73,
-	0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e,
-	0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22, 0x49, 0x0a, 0x09,
-	0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x73, 0x68,
-	0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73,
-	0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68,
-	0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73,
-	0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x20, 0x0a, 0x1e, 0x44, 0x65, 0x76, 0x69, 0x63,
-	0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c,
-	0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xbf, 0x01, 0x0a, 0x17, 0x44, 0x65,
-	0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f,
-	0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x48, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65,
-	0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
-	0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f,
-	0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x72, 0x6f,
-	0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12,
-	0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69,
-	0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
-	0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e,
-	0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e,
-	0x66, 0x69, 0x67, 0x22, 0x16, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12,
-	0x0a, 0x0a, 0x06, 0x48, 0x4f, 0x53, 0x54, 0x45, 0x44, 0x10, 0x00, 0x22, 0x1e, 0x0a, 0x1c, 0x50,
-	0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e,
-	0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x5b, 0x0a, 0x15, 0x50,
-	0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e,
-	0x46, 0x6c, 0x6f, 0x77, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72,
-	0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d,
-	0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64,
-	0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64,
-	0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x9a, 0x03, 0x0a, 0x0e, 0x50, 0x72, 0x6f,
-	0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x43,
-	0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43,
-	0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e,
-	0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x43,
-	0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x44,
-	0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d,
-	0x61, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18,
-	0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12,
-	0x2e, 0x0a, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64,
-	0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x44, 0x65, 0x76,
-	0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12,
-	0x24, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74,
-	0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64,
-	0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07,
-	0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x55,
-	0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52,
-	0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x34, 0x0a, 0x15, 0x41,
-	0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70,
-	0x6f, 0x69, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x41, 0x75, 0x74, 0x68,
-	0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e,
-	0x74, 0x12, 0x22, 0x0a, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c,
-	0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63,
-	0x74, 0x55, 0x52, 0x4c, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65,
-	0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28,
-	0x08, 0x52, 0x12, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74,
-	0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x22, 0xed, 0x01, 0x0a, 0x05, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12,
-	0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12,
-	0x18, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
-	0x52, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x20, 0x0a, 0x0b, 0x4e, 0x65, 0x74,
-	0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b,
-	0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50,
-	0x65, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12,
-	0x16, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52,
-	0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75,
-	0x65, 0x72, 0x61, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x4d, 0x61, 0x73,
-	0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44,
-	0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x12, 0x18, 0x0a,
-	0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07,
-	0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52,
-	0x6f, 0x75, 0x74, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x6b, 0x65, 0x65, 0x70,
-	0x52, 0x6f, 0x75, 0x74, 0x65, 0x22, 0xb4, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e,
-	0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e,
-	0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76,
-	0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x4e, 0x61, 0x6d,
-	0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20,
-	0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
-	0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70,
-	0x52, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75,
-	0x70, 0x73, 0x12, 0x38, 0x0a, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65,
-	0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
-	0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x52,
-	0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x22, 0x58, 0x0a, 0x0a,
-	0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f,
-	0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61,
-	0x69, 0x6e, 0x12, 0x32, 0x0a, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x02, 0x20,
-	0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
-	0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x52,
-	0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x74, 0x0a, 0x0c, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65,
-	0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01,
-	0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79,
-	0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14,
-	0x0a, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x43,
-	0x6c, 0x61, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x54, 0x54, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28,
-	0x03, 0x52, 0x03, 0x54, 0x54, 0x4c, 0x12, 0x14, 0x0a, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x18,
-	0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x22, 0xb3, 0x01, 0x0a,
-	0x0f, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70,
-	0x12, 0x38, 0x0a, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18,
-	0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
-	0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0b, 0x4e,
-	0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72,
-	0x69, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x50, 0x72, 0x69,
-	0x6d, 0x61, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18,
-	0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x32,
-	0x0a, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45,
-	0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x53, 0x65,
-	0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c,
-	0x65, 0x64, 0x22, 0x48, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
-	0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50,
-	0x12, 0x16, 0x0a, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03,
-	0x52, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74,
-	0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0xa7, 0x02, 0x0a,
-	0x0c, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a,
-	0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50,
-	0x65, 0x65, 0x72, 0x49, 0x50, 0x12, 0x37, 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69,
-	0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
-	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74,
-	0x69, 0x6f, 0x6e, 0x52, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e,
-	0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16,
-	0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65,
-	0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34,
-	0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e,
-	0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75,
-	0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74,
-	0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01,
-	0x28, 0x09, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x30, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74,
-	0x49, 0x6e, 0x66, 0x6f, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e,
+	0x65, 0x73, 0x12, 0x3e, 0x0a, 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69, 0x72, 0x65,
+	0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79,
+	0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1a, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x46, 0x69,
+	0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70,
+	0x74, 0x79, 0x12, 0x44, 0x0a, 0x0f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67,
+	0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61,
+	0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64,
+	0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0f, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64,
+	0x69, 0x6e, 0x67, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x22, 0xbb, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x6d,
+	0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a,
+	0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
+	0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x6c, 0x6c,
+	0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61,
+	0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68,
+	0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d,
+	0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e,
+	0x66, 0x69, 0x67, 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12,
+	0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71,
+	0x64, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69,
+	0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x56,
+	0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x49, 0x0a, 0x09, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e,
+	0x66, 0x69, 0x67, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65,
+	0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62,
+	0x6c, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79,
+	0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65,
+	0x79, 0x22, 0x20, 0x0a, 0x1e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f,
+	0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75,
+	0x65, 0x73, 0x74, 0x22, 0xbf, 0x01, 0x0a, 0x17, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75,
+	0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12,
+	0x48, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28,
+	0x0e, 0x32, 0x2c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44,
+	0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69,
+	0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52,
+	0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f,
+	0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28,
+	0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50,
+	0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50,
+	0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x16, 0x0a,
+	0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x0a, 0x0a, 0x06, 0x48, 0x4f, 0x53,
+	0x54, 0x45, 0x44, 0x10, 0x00, 0x22, 0x1e, 0x0a, 0x1c, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74,
+	0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65,
+	0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x5b, 0x0a, 0x15, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74,
+	0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x42,
+	0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
+	0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
+	0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66,
+	0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66,
+	0x69, 0x67, 0x22, 0xb8, 0x03, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43,
+	0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49,
+	0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49,
+	0x44, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65,
+	0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53,
+	0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18,
+	0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x1a, 0x0a,
+	0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52,
+	0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x44, 0x65, 0x76,
+	0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18,
+	0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74,
+	0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x54, 0x6f, 0x6b,
+	0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12,
+	0x14, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05,
+	0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f,
+	0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44,
+	0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x34, 0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69,
+	0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x09,
+	0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74,
+	0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x52,
+	0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28,
+	0x09, 0x52, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x12,
+	0x2e, 0x0a, 0x12, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74,
+	0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x44, 0x69, 0x73,
+	0x61, 0x62, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12,
+	0x1c, 0x0a, 0x09, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x46, 0x6c, 0x61, 0x67, 0x18, 0x0c, 0x20, 0x01,
+	0x28, 0x0d, 0x52, 0x09, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x46, 0x6c, 0x61, 0x67, 0x22, 0xed, 0x01,
+	0x0a, 0x05, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20,
+	0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f,
+	0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72,
+	0x6b, 0x12, 0x20, 0x0a, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65,
+	0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54,
+	0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28,
+	0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69,
+	0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x12,
+	0x1e, 0x0a, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x18, 0x06, 0x20,
+	0x01, 0x28, 0x08, 0x52, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x12,
+	0x14, 0x0a, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05,
+	0x4e, 0x65, 0x74, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73,
+	0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12,
+	0x1c, 0x0a, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x18, 0x09, 0x20, 0x01,
+	0x28, 0x08, 0x52, 0x09, 0x6b, 0x65, 0x65, 0x70, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x22, 0xb4, 0x01,
+	0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x0d, 0x53,
+	0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x08, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c,
+	0x65, 0x12, 0x47, 0x0a, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47,
+	0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x61,
+	0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72,
+	0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65,
+	0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x38, 0x0a, 0x0b, 0x43, 0x75,
+	0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32,
+	0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x75, 0x73,
+	0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a,
+	0x6f, 0x6e, 0x65, 0x73, 0x22, 0x58, 0x0a, 0x0a, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f,
+	0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x32, 0x0a, 0x07, 0x52, 0x65,
+	0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61,
+	0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52,
+	0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x74,
+	0x0a, 0x0c, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12,
+	0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61,
+	0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03,
+	0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x18,
+	0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03,
+	0x54, 0x54, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x54, 0x54, 0x4c, 0x12, 0x14,
+	0x0a, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x52,
+	0x44, 0x61, 0x74, 0x61, 0x22, 0xb3, 0x01, 0x0a, 0x0f, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72,
+	0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x38, 0x0a, 0x0b, 0x4e, 0x61, 0x6d, 0x65,
+	0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e,
+	0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53,
+	0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65,
+	0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x02, 0x20,
+	0x01, 0x28, 0x08, 0x52, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07,
+	0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44,
+	0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68,
+	0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x04,
+	0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61,
+	0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x48, 0x0a, 0x0a, 0x4e, 0x61,
+	0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01,
+	0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x4e, 0x53, 0x54, 0x79,
+	0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65,
+	0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04,
+	0x50, 0x6f, 0x72, 0x74, 0x22, 0xa7, 0x02, 0x0a, 0x0c, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c,
+	0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x18,
+	0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x12, 0x37, 0x0a,
+	0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e,
+	0x32, 0x19, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75,
+	0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x44, 0x69, 0x72,
+	0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e,
+	0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d,
+	0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06,
+	0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63,
+	0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
+	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63,
+	0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04,
+	0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74,
+	0x12, 0x30, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x06, 0x20, 0x01,
+	0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
+	0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e,
+	0x66, 0x6f, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x18, 0x07,
+	0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x22, 0x38,
+	0x0a, 0x0e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73,
+	0x12, 0x14, 0x0a, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
+	0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x61, 0x63, 0x18, 0x02, 0x20,
+	0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x61, 0x63, 0x22, 0x1e, 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63,
+	0x6b, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28,
+	0x09, 0x52, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x08, 0x50, 0x6f, 0x72,
+	0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20,
+	0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x32, 0x0a, 0x05, 0x72,
+	0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e,
 	0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f,
-	0x52, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x6f,
-	0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x50, 0x6f,
-	0x6c, 0x69, 0x63, 0x79, 0x49, 0x44, 0x22, 0x38, 0x0a, 0x0e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72,
-	0x6b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x65, 0x74, 0x49,
-	0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x65, 0x74, 0x49, 0x50, 0x12, 0x10,
-	0x0a, 0x03, 0x6d, 0x61, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x61, 0x63,
-	0x22, 0x1e, 0x0a, 0x06, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x46, 0x69,
-	0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73,
-	0x22, 0x96, 0x01, 0x0a, 0x08, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a,
-	0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x04, 0x70,
-	0x6f, 0x72, 0x74, 0x12, 0x32, 0x0a, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01,
-	0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
-	0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00,
-	0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x1a, 0x2f, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65,
-	0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52,
-	0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20,
-	0x01, 0x28, 0x0d, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x42, 0x0f, 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74,
-	0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xed, 0x02, 0x0a, 0x11, 0x52, 0x6f,
-	0x75, 0x74, 0x65, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12,
-	0x22, 0x0a, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18,
-	0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e,
-	0x67, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20,
-	0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
-	0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74,
-	0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69,
-	0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e,
-	0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f,
-	0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
-	0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f,
-	0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x30, 0x0a, 0x08, 0x70,
-	0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e,
+	0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x05, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x1a,
+	0x2f, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72,
+	0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x10,
+	0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x65, 0x6e, 0x64,
+	0x42, 0x0f, 0x0a, 0x0d, 0x70, 0x6f, 0x72, 0x74, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f,
+	0x6e, 0x22, 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, 0x67, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x61,
+	0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x6d, 0x61,
+	0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74,
+	0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x64,
+	0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x34, 0x0a,
+	0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32,
+	0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x75, 0x6c,
+	0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+	0x63, 0x6f, 0x6c, 0x12, 0x30, 0x0a, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x18,
+	0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
+	0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x70, 0x6f, 0x72,
+	0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d,
+	0x69, 0x63, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61,
+	0x6d, 0x69, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x07,
+	0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x26, 0x0a,
+	0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18,
+	0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f,
+	0x74, 0x6f, 0x63, 0x6f, 0x6c, 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, 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, 0x08, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1c, 0x0a,
-	0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08,
-	0x52, 0x09, 0x69, 0x73, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x64,
-	0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6f,
-	0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50,
-	0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x63,
-	0x75, 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 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,
-	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,
+	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, 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,
+	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, 0x1a, 0x1c, 0x2e, 0x6d,
-	0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70,
-	0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x18,
-	0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61,
-	0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
+	0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x58,
+	0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69,
+	0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e,
+	0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65,
+	0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
 	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d,
-	0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 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,
+	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 0f1cdb97..3f985d5f 100644
--- a/management/proto/management.proto
+++ b/management/proto/management.proto
@@ -254,6 +254,8 @@ message PeerConfig {
   string fqdn = 4;
 
   bool RoutingPeerDnsResolutionEnabled = 5;
+
+  bool LazyConnectionEnabled = 6;
 }
 
 // NetworkMap represents a network state of the peer with the corresponding configuration parameters to establish peer-to-peer connections
@@ -312,6 +314,7 @@ message RemotePeerConfig {
   // Peer fully qualified domain name
   string fqdn = 4;
 
+  string agentVersion = 5;
 }
 
 // SSHConfig represents SSH configurations of a peer.
@@ -374,6 +377,8 @@ message ProviderConfig {
   repeated string RedirectURLs = 10;
   // DisablePromptLogin makes the PKCE flow to not prompt the user for login
   bool DisablePromptLogin = 11;
+  // LoginFlags sets the PKCE flow login details 
+  uint32 LoginFlag = 12;
 }
 
 // Route represents a route.Route object
@@ -509,6 +514,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 d7f108df..63879802 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"
@@ -195,21 +196,7 @@ func BuildManager(
 		permissionsManager:       permissionsManager,
 	}
 
-	var initialInterval int64
-	intervalStr := os.Getenv("PEER_UPDATE_INTERVAL_MS")
-	interval, err := strconv.Atoi(intervalStr)
-	if err != nil {
-		initialInterval = 1
-	} else {
-		initialInterval = int64(interval) * 10
-		go func() {
-			time.Sleep(30 * time.Second)
-			am.updateAccountPeersBufferInterval.Store(int64(time.Duration(interval) * time.Millisecond))
-			log.WithContext(ctx).Infof("set peer update buffer interval to %dms", interval)
-		}()
-	}
-	am.updateAccountPeersBufferInterval.Store(initialInterval)
-	log.WithContext(ctx).Infof("set peer update buffer interval to %dms", initialInterval)
+	am.startWarmup(ctx)
 
 	accountsCounter, err := store.GetAccountsCounter(ctx)
 	if err != nil {
@@ -237,7 +224,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?
@@ -253,6 +240,32 @@ func BuildManager(
 	return am, nil
 }
 
+func (am *DefaultAccountManager) startWarmup(ctx context.Context) {
+	var initialInterval int64
+	intervalStr := os.Getenv("NB_PEER_UPDATE_INTERVAL_MS")
+	interval, err := strconv.Atoi(intervalStr)
+	if err != nil {
+		initialInterval = 1
+		log.WithContext(ctx).Warnf("failed to parse peer update interval, using default value %dms: %v", initialInterval, err)
+	} else {
+		initialInterval = int64(interval) * 10
+		go func() {
+			startupPeriodStr := os.Getenv("NB_PEER_UPDATE_STARTUP_PERIOD_S")
+			startupPeriod, err := strconv.Atoi(startupPeriodStr)
+			if err != nil {
+				startupPeriod = 1
+				log.WithContext(ctx).Warnf("failed to parse peer update startup period, using default value %ds: %v", startupPeriod, err)
+			}
+			time.Sleep(time.Duration(startupPeriod) * time.Second)
+			am.updateAccountPeersBufferInterval.Store(int64(time.Duration(interval) * time.Millisecond))
+			log.WithContext(ctx).Infof("set peer update buffer interval to %dms", interval)
+		}()
+	}
+	am.updateAccountPeersBufferInterval.Store(initialInterval)
+	log.WithContext(ctx).Infof("set peer update buffer interval to %dms", initialInterval)
+
+}
+
 func (am *DefaultAccountManager) GetExternalCacheManager() account.ExternalCacheManager {
 	return am.externalCacheManager
 }
@@ -275,6 +288,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()
 
@@ -322,7 +339,20 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco
 			am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountRoutingPeerDNSResolutionDisabled, nil)
 		}
 		updateAccountPeers = true
-		account.Network.Serial++
+	}
+
+	if oldSettings.LazyConnectionEnabled != newSettings.LazyConnectionEnabled {
+		if newSettings.LazyConnectionEnabled {
+			am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountLazyConnectionEnabled, nil)
+		} else {
+			am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountLazyConnectionDisabled, nil)
+		}
+		updateAccountPeers = true
+	}
+
+	if oldSettings.DNSDomain != newSettings.DNSDomain {
+		am.StoreEvent(ctx, userID, accountID, accountID, activity.AccountDNSDomainUpdated, nil)
+		updateAccountPeers = true
 	}
 
 	err = am.handleInactivityExpirationSettings(ctx, oldSettings, newSettings, userID, accountID)
@@ -335,7 +365,11 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco
 		return nil, fmt.Errorf("groups propagation failed: %w", err)
 	}
 
-	updatedAccount := account.UpdateSettings(newSettings)
+	account.UpdateSettings(newSettings)
+
+	if updateAccountPeers {
+		account.Network.Serial++
+	}
 
 	err = am.Store.SaveAccount(ctx, account)
 	if err != nil {
@@ -351,7 +385,7 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco
 		go am.UpdateAccountPeers(ctx, accountID)
 	}
 
-	return updatedAccount, nil
+	return account, nil
 }
 
 func (am *DefaultAccountManager) handleGroupsPropagationSettings(ctx context.Context, oldSettings, newSettings *types.Settings, userID, accountID string) error {
@@ -484,7 +518,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 +576,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, &notFoundErr) {
+		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)
@@ -548,11 +626,15 @@ func (am *DefaultAccountManager) DeleteAccount(ctx context.Context, accountID, u
 	}
 
 	for _, otherUser := range account.Users {
-		if otherUser.IsServiceUser {
+		if otherUser.Id == userID {
 			continue
 		}
 
-		if otherUser.Id == userID {
+		if otherUser.IsServiceUser {
+			err = am.deleteServiceUser(ctx, accountID, userID, otherUser)
+			if err != nil {
+				return err
+			}
 			continue
 		}
 
@@ -657,7 +739,7 @@ func (am *DefaultAccountManager) loadAccount(ctx context.Context, accountID any)
 	log.WithContext(ctx).Debugf("account %s not found in cache, reloading", accountID)
 	accountIDString := fmt.Sprintf("%v", accountID)
 
-	account, err := am.Store.GetAccount(ctx, accountIDString)
+	accountUsers, err := am.Store.GetAccountUsers(ctx, store.LockingStrengthShare, accountIDString)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -666,7 +748,7 @@ func (am *DefaultAccountManager) loadAccount(ctx context.Context, accountID any)
 	if err != nil {
 		return nil, nil, err
 	}
-	log.WithContext(ctx).Debugf("%d entries received from IdP management", len(userData))
+	log.WithContext(ctx).Debugf("%d entries received from IdP management for account %s", len(userData), accountIDString)
 
 	dataMap := make(map[string]*idp.UserData, len(userData))
 	for _, datum := range userData {
@@ -674,7 +756,7 @@ func (am *DefaultAccountManager) loadAccount(ctx context.Context, accountID any)
 	}
 
 	matchedUserData := make([]*idp.UserData, 0)
-	for _, user := range account.Users {
+	for _, user := range accountUsers {
 		if user.IsServiceUser {
 			continue
 		}
@@ -1057,6 +1139,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)
@@ -1153,7 +1248,7 @@ func (am *DefaultAccountManager) SyncUserJWTGroups(ctx context.Context, userAuth
 			return nil
 		}
 
-		if err = transaction.SaveGroups(ctx, store.LockingStrengthUpdate, newGroupsToCreate); err != nil {
+		if err = transaction.SaveGroups(ctx, store.LockingStrengthUpdate, userAuth.AccountId, newGroupsToCreate); err != nil {
 			return fmt.Errorf("error saving groups: %w", err)
 		}
 
@@ -1187,7 +1282,7 @@ func (am *DefaultAccountManager) SyncUserJWTGroups(ctx context.Context, userAuth
 				return fmt.Errorf("error modifying user peers in groups: %w", err)
 			}
 
-			if err = transaction.SaveGroups(ctx, store.LockingStrengthUpdate, updatedGroups); err != nil {
+			if err = transaction.SaveGroups(ctx, store.LockingStrengthUpdate, userAuth.AccountId, updatedGroups); err != nil {
 				return fmt.Errorf("error saving groups: %w", err)
 			}
 
@@ -1480,8 +1575,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) {
@@ -1628,23 +1730,26 @@ func (am *DefaultAccountManager) GetStore() store.Store {
 	return am.Store
 }
 
-// Creates account by private domain.
-// Expects domain value to be a valid and a private dns domain.
-func (am *DefaultAccountManager) CreateAccountByPrivateDomain(ctx context.Context, initiatorId, domain string) (*types.Account, error) {
+func (am *DefaultAccountManager) GetOrCreateAccountByPrivateDomain(ctx context.Context, initiatorId, domain string) (*types.Account, bool, error) {
 	cancel := am.Store.AcquireGlobalLock(ctx)
 	defer cancel()
 
-	domain = strings.ToLower(domain)
-
-	count, err := am.Store.CountAccountsByPrivateDomain(ctx, domain)
-	if err != nil {
-		return nil, err
+	existingPrimaryAccountID, err := am.Store.GetAccountIDByPrivateDomain(ctx, store.LockingStrengthShare, domain)
+	if handleNotFound(err) != nil {
+		return nil, false, err
 	}
 
-	if count > 0 {
-		return nil, status.Errorf(status.InvalidArgument, "account with private domain already exists")
+	// a primary account already exists for this private domain
+	if err == nil {
+		existingAccount, err := am.Store.GetAccount(ctx, existingPrimaryAccountID)
+		if err != nil {
+			return nil, false, err
+		}
+
+		return existingAccount, false, nil
 	}
 
+	// create a new account for this private domain
 	// retry twice for new ID clashes
 	for range 2 {
 		accountId := xid.New().String()
@@ -1674,7 +1779,7 @@ func (am *DefaultAccountManager) CreateAccountByPrivateDomain(ctx context.Contex
 			Users:     users,
 			// @todo check if using the MSP owner id here is ok
 			CreatedBy:              initiatorId,
-			Domain:                 domain,
+			Domain:                 strings.ToLower(domain),
 			DomainCategory:         types.PrivateCategory,
 			IsDomainPrimaryAccount: false,
 			Routes:                 routes,
@@ -1693,19 +1798,22 @@ func (am *DefaultAccountManager) CreateAccountByPrivateDomain(ctx context.Contex
 		}
 
 		if err := newAccount.AddAllGroup(); err != nil {
-			return nil, status.Errorf(status.Internal, "failed to add all group to new account by private domain")
+			return nil, false, status.Errorf(status.Internal, "failed to add all group to new account by private domain")
 		}
 
 		if err := am.Store.SaveAccount(ctx, newAccount); err != nil {
-			log.WithContext(ctx).Errorf("failed to save new account %s by private domain: %v", newAccount.Id, err)
-			return nil, err
+			log.WithContext(ctx).WithFields(log.Fields{
+				"accountId": newAccount.Id,
+				"domain":    domain,
+			}).Errorf("failed to create new account: %v", err)
+			return nil, false, err
 		}
 
 		am.StoreEvent(ctx, initiatorId, newAccount.Id, accountId, activity.AccountCreated, nil)
-		return newAccount, nil
+		return newAccount, true, nil
 	}
 
-	return nil, status.Errorf(status.Internal, "failed to create new account by private domain")
+	return nil, false, status.Errorf(status.Internal, "failed to get or create new account by private domain")
 }
 
 func (am *DefaultAccountManager) UpdateToPrimaryAccount(ctx context.Context, accountId string) (*types.Account, error) {
@@ -1718,21 +1826,29 @@ func (am *DefaultAccountManager) UpdateToPrimaryAccount(ctx context.Context, acc
 		return account, nil
 	}
 
-	// additional check to ensure there is only one account for this domain at the time of update
-	count, err := am.Store.CountAccountsByPrivateDomain(ctx, account.Domain)
-	if err != nil {
+	existingPrimaryAccountID, err := am.Store.GetAccountIDByPrivateDomain(ctx, store.LockingStrengthShare, account.Domain)
+
+	// error is not a not found error
+	if handleNotFound(err) != nil {
 		return nil, err
 	}
 
-	if count > 1 {
-		return nil, status.Errorf(status.Internal, "more than one account exists with the same private domain")
+	// a primary account already exists for this private domain
+	if err == nil {
+		log.WithContext(ctx).WithFields(log.Fields{
+			"accountId":         accountId,
+			"existingAccountId": existingPrimaryAccountID,
+		}).Errorf("cannot update account to primary, another account already exists as primary for the same domain")
+		return nil, status.Errorf(status.Internal, "cannot update account to primary")
 	}
 
 	account.IsDomainPrimaryAccount = true
 
 	if err := am.Store.SaveAccount(ctx, account); err != nil {
-		log.WithContext(ctx).Errorf("failed to update primary account %s by private domain: %v", account.Id, err)
-		return nil, status.Errorf(status.Internal, "failed to update primary account %s by private domain", account.Id)
+		log.WithContext(ctx).WithFields(log.Fields{
+			"accountId": accountId,
+		}).Errorf("failed to update account to primary: %v", err)
+		return nil, status.Errorf(status.Internal, "failed to update account to primary")
 	}
 
 	return account, nil
diff --git a/management/server/account/manager.go b/management/server/account/manager.go
index ea664d10..030bd94e 100644
--- a/management/server/account/manager.go
+++ b/management/server/account/manager.go
@@ -16,6 +16,7 @@ import (
 	"github.com/netbirdio/netbird/management/server/posture"
 	"github.com/netbirdio/netbird/management/server/store"
 	"github.com/netbirdio/netbird/management/server/types"
+	"github.com/netbirdio/netbird/management/server/users"
 	"github.com/netbirdio/netbird/route"
 )
 
@@ -37,6 +38,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)
@@ -80,7 +82,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)
@@ -111,8 +113,8 @@ type Manager interface {
 	BuildUserInfosForAccount(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error)
 	SyncUserJWTGroups(ctx context.Context, userAuth nbcontext.UserAuth) error
 	GetStore() store.Store
-	CreateAccountByPrivateDomain(ctx context.Context, initiatorId, domain string) (*types.Account, error)
+	GetOrCreateAccountByPrivateDomain(ctx context.Context, initiatorId, domain string) (*types.Account, bool, 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)
+	GetCurrentUserInfo(ctx context.Context, userAuth nbcontext.UserAuth) (*users.UserInfoWithPermissions, error)
 }
diff --git a/management/server/activity/codes.go b/management/server/activity/codes.go
index 46ae754c..d9f56f09 100644
--- a/management/server/activity/codes.go
+++ b/management/server/activity/codes.go
@@ -169,6 +169,11 @@ const (
 
 	ResourceAddedToGroup     Activity = 82
 	ResourceRemovedFromGroup Activity = 83
+
+	AccountDNSDomainUpdated Activity = 84
+
+	AccountLazyConnectionEnabled  Activity = 85
+	AccountLazyConnectionDisabled Activity = 86
 )
 
 var activityMap = map[Activity]Code{
@@ -264,6 +269,11 @@ 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"},
+
+	AccountLazyConnectionEnabled:  {"Account lazy connection enabled", "account.setting.lazy.connection.enable"},
+	AccountLazyConnectionDisabled: {"Account lazy connection disabled", "account.setting.lazy.connection.disable"},
 }
 
 // StringCode returns a string code of the activity
diff --git a/management/server/activity/event.go b/management/server/activity/event.go
index 0e819c3a..5bcc67fb 100644
--- a/management/server/activity/event.go
+++ b/management/server/activity/event.go
@@ -19,22 +19,22 @@ type Event struct {
 	// Timestamp of the event
 	Timestamp time.Time
 	// Activity that was performed during the event
-	Activity ActivityDescriber
+	Activity Activity `gorm:"type:integer"`
 	// ID of the event (can be empty, meaning that it wasn't yet generated)
-	ID uint64
+	ID uint64 `gorm:"primaryKey;autoIncrement"`
 	// InitiatorID is the ID of an object that initiated the event (e.g., a user)
 	InitiatorID string
 	// InitiatorName is the name of an object that initiated the event.
-	InitiatorName string
+	InitiatorName string `gorm:"-"`
 	// InitiatorEmail is the email address of an object that initiated the event.
-	InitiatorEmail string
+	InitiatorEmail string `gorm:"-"`
 	// TargetID is the ID of an object that was effected by the event (e.g., a peer)
 	TargetID string
 	// AccountID is the ID of an account where the event happened
 	AccountID string
 
 	// Meta of the event, e.g. deleted peer information like name, IP, etc
-	Meta map[string]any
+	Meta map[string]any `gorm:"serializer:json"`
 }
 
 // Copy the event
@@ -57,3 +57,10 @@ func (e *Event) Copy() *Event {
 		Meta:           meta,
 	}
 }
+
+type DeletedUser struct {
+	ID      string `gorm:"primaryKey"`
+	Email   string `gorm:"not null"`
+	Name    string
+	EncAlgo string `gorm:"not null"`
+}
diff --git a/management/server/activity/sqlite/migration.go b/management/server/activity/sqlite/migration.go
deleted file mode 100644
index 28c5b302..00000000
--- a/management/server/activity/sqlite/migration.go
+++ /dev/null
@@ -1,157 +0,0 @@
-package sqlite
-
-import (
-	"context"
-	"database/sql"
-	"fmt"
-
-	log "github.com/sirupsen/logrus"
-)
-
-func migrate(ctx context.Context, crypt *FieldEncrypt, db *sql.DB) error {
-	if _, err := db.Exec(createTableQuery); err != nil {
-		return err
-	}
-
-	if _, err := db.Exec(creatTableDeletedUsersQuery); err != nil {
-		return err
-	}
-
-	if err := updateDeletedUsersTable(ctx, db); err != nil {
-		return fmt.Errorf("failed to update deleted_users table: %v", err)
-	}
-
-	return migrateLegacyEncryptedUsersToGCM(ctx, crypt, db)
-}
-
-// updateDeletedUsersTable checks and updates the deleted_users table schema to ensure required columns exist.
-func updateDeletedUsersTable(ctx context.Context, db *sql.DB) error {
-	exists, err := checkColumnExists(db, "deleted_users", "name")
-	if err != nil {
-		return err
-	}
-
-	if !exists {
-		log.WithContext(ctx).Debug("Adding name column to the deleted_users table")
-
-		_, err = db.Exec(`ALTER TABLE deleted_users ADD COLUMN name TEXT;`)
-		if err != nil {
-			return err
-		}
-
-		log.WithContext(ctx).Debug("Successfully added name column to the deleted_users table")
-	}
-
-	exists, err = checkColumnExists(db, "deleted_users", "enc_algo")
-	if err != nil {
-		return err
-	}
-
-	if !exists {
-		log.WithContext(ctx).Debug("Adding enc_algo column to the deleted_users table")
-
-		_, err = db.Exec(`ALTER TABLE deleted_users ADD COLUMN enc_algo TEXT;`)
-		if err != nil {
-			return err
-		}
-
-		log.WithContext(ctx).Debug("Successfully added enc_algo column to the deleted_users table")
-	}
-
-	return nil
-}
-
-// migrateLegacyEncryptedUsersToGCM migrates previously encrypted data using,
-// legacy CBC encryption with a static IV to the new GCM encryption method.
-func migrateLegacyEncryptedUsersToGCM(ctx context.Context, crypt *FieldEncrypt, db *sql.DB) error {
-	log.WithContext(ctx).Debug("Migrating CBC encrypted deleted users to GCM")
-
-	tx, err := db.Begin()
-	if err != nil {
-		return fmt.Errorf("failed to begin transaction: %v", err)
-	}
-	defer func() {
-		_ = tx.Rollback()
-	}()
-
-	rows, err := tx.Query(fmt.Sprintf(`SELECT id, email, name FROM deleted_users where enc_algo IS NULL OR enc_algo != '%s'`, gcmEncAlgo))
-	if err != nil {
-		return fmt.Errorf("failed to execute select query: %v", err)
-	}
-	defer rows.Close()
-
-	updateStmt, err := tx.Prepare(`UPDATE deleted_users SET email = ?, name = ?, enc_algo = ? WHERE id = ?`)
-	if err != nil {
-		return fmt.Errorf("failed to prepare update statement: %v", err)
-	}
-	defer updateStmt.Close()
-
-	if err = processUserRows(ctx, crypt, rows, updateStmt); err != nil {
-		return err
-	}
-
-	if err = tx.Commit(); err != nil {
-		return fmt.Errorf("failed to commit transaction: %v", err)
-	}
-
-	log.WithContext(ctx).Debug("Successfully migrated CBC encrypted deleted users to GCM")
-	return nil
-}
-
-// processUserRows processes database rows of user data, decrypts legacy encryption fields, and re-encrypts them using GCM.
-func processUserRows(ctx context.Context, crypt *FieldEncrypt, rows *sql.Rows, updateStmt *sql.Stmt) error {
-	for rows.Next() {
-		var (
-			id, decryptedEmail, decryptedName string
-			email, name                       *string
-		)
-
-		err := rows.Scan(&id, &email, &name)
-		if err != nil {
-			return err
-		}
-
-		if email != nil {
-			decryptedEmail, err = crypt.LegacyDecrypt(*email)
-			if err != nil {
-				log.WithContext(ctx).Warnf("skipping migrating deleted user %s: %v",
-					id,
-					fmt.Errorf("failed to decrypt email: %w", err),
-				)
-				continue
-			}
-		}
-
-		if name != nil {
-			decryptedName, err = crypt.LegacyDecrypt(*name)
-			if err != nil {
-				log.WithContext(ctx).Warnf("skipping migrating deleted user %s: %v",
-					id,
-					fmt.Errorf("failed to decrypt name: %w", err),
-				)
-				continue
-			}
-		}
-
-		encryptedEmail, err := crypt.Encrypt(decryptedEmail)
-		if err != nil {
-			return fmt.Errorf("failed to encrypt email: %w", err)
-		}
-
-		encryptedName, err := crypt.Encrypt(decryptedName)
-		if err != nil {
-			return fmt.Errorf("failed to encrypt name: %w", err)
-		}
-
-		_, err = updateStmt.Exec(encryptedEmail, encryptedName, gcmEncAlgo, id)
-		if err != nil {
-			return err
-		}
-	}
-
-	if err := rows.Err(); err != nil {
-		return err
-	}
-
-	return nil
-}
diff --git a/management/server/activity/sqlite/sqlite.go b/management/server/activity/sqlite/sqlite.go
deleted file mode 100644
index ffb863de..00000000
--- a/management/server/activity/sqlite/sqlite.go
+++ /dev/null
@@ -1,359 +0,0 @@
-package sqlite
-
-import (
-	"context"
-	"database/sql"
-	"encoding/json"
-	"fmt"
-	"path/filepath"
-	"runtime"
-	"time"
-
-	_ "github.com/mattn/go-sqlite3"
-	log "github.com/sirupsen/logrus"
-
-	"github.com/netbirdio/netbird/management/server/activity"
-)
-
-const (
-	// eventSinkDB is the default name of the events database
-	eventSinkDB      = "events.db"
-	createTableQuery = "CREATE TABLE IF NOT EXISTS events " +
-		"(id INTEGER PRIMARY KEY AUTOINCREMENT, " +
-		"activity INTEGER, " +
-		"timestamp DATETIME, " +
-		"initiator_id TEXT," +
-		"account_id TEXT," +
-		"meta TEXT," +
-		" target_id TEXT);"
-
-	creatTableDeletedUsersQuery = `CREATE TABLE IF NOT EXISTS deleted_users (id TEXT NOT NULL, email TEXT NOT NULL, name TEXT, enc_algo TEXT NOT NULL);`
-
-	selectDescQuery = `SELECT events.id, activity, timestamp, initiator_id, i.name as "initiator_name", i.email as "initiator_email", target_id, t.name as "target_name", t.email as "target_email", account_id, meta
-		FROM events 
-		LEFT JOIN (
-		    SELECT id, MAX(name) as name, MAX(email) as email 
-		    FROM deleted_users
-		    GROUP BY id
-		) i ON events.initiator_id = i.id 
-		LEFT JOIN (
-		    SELECT id, MAX(name) as name, MAX(email) as email 
-		    FROM deleted_users
-		    GROUP BY id
-		) t ON events.target_id = t.id
-		WHERE account_id = ? 
-		ORDER BY timestamp DESC LIMIT ? OFFSET ?;`
-
-	selectAscQuery = `SELECT events.id, activity, timestamp, initiator_id, i.name as "initiator_name", i.email as "initiator_email", target_id, t.name as "target_name", t.email as "target_email", account_id, meta
-		FROM events 
-		LEFT JOIN (
-		    SELECT id, MAX(name) as name, MAX(email) as email 
-		    FROM deleted_users
-		    GROUP BY id
-		) i ON events.initiator_id = i.id 
-		LEFT JOIN (
-		    SELECT id, MAX(name) as name, MAX(email) as email 
-		    FROM deleted_users
-		    GROUP BY id
-		) t ON events.target_id = t.id
-		WHERE account_id = ? 
-		ORDER BY timestamp ASC LIMIT ? OFFSET ?;`
-
-	insertQuery = "INSERT INTO events(activity, timestamp, initiator_id, target_id, account_id, meta) " +
-		"VALUES(?, ?, ?, ?, ?, ?)"
-
-	/*
-		 TODO:
-			The insert should avoid duplicated IDs in the table. So the query should be changes to something like:
-			`INSERT INTO deleted_users(id, email, name) VALUES(?, ?, ?) ON CONFLICT (id) DO UPDATE SET email = EXCLUDED.email, name = EXCLUDED.name;`
-			For this to work we have to set the id column as primary key. But this is not possible because the id column is not unique
-			and some selfhosted deployments might have duplicates already so we need to clean the table first.
-	*/
-
-	insertDeleteUserQuery = `INSERT INTO deleted_users(id, email, name, enc_algo) VALUES(?, ?, ?, ?)`
-
-	fallbackName  = "unknown"
-	fallbackEmail = "unknown@unknown.com"
-
-	gcmEncAlgo = "GCM"
-)
-
-// Store is the implementation of the activity.Store interface backed by SQLite
-type Store struct {
-	db           *sql.DB
-	fieldEncrypt *FieldEncrypt
-
-	insertStatement     *sql.Stmt
-	selectAscStatement  *sql.Stmt
-	selectDescStatement *sql.Stmt
-	deleteUserStmt      *sql.Stmt
-}
-
-// NewSQLiteStore creates a new Store with an event table if not exists.
-func NewSQLiteStore(ctx context.Context, dataDir string, encryptionKey string) (*Store, error) {
-	dbFile := filepath.Join(dataDir, eventSinkDB)
-	db, err := sql.Open("sqlite3", dbFile)
-	if err != nil {
-		return nil, err
-	}
-	db.SetMaxOpenConns(runtime.NumCPU())
-
-	crypt, err := NewFieldEncrypt(encryptionKey)
-	if err != nil {
-		_ = db.Close()
-		return nil, err
-	}
-
-	if err = migrate(ctx, crypt, db); err != nil {
-		_ = db.Close()
-		return nil, fmt.Errorf("events database migration: %w", err)
-	}
-
-	return createStore(crypt, db)
-}
-
-func (store *Store) processResult(ctx context.Context, result *sql.Rows) ([]*activity.Event, error) {
-	events := make([]*activity.Event, 0)
-	var cryptErr error
-	for result.Next() {
-		var id int64
-		var operation activity.Activity
-		var timestamp time.Time
-		var initiator string
-		var initiatorName *string
-		var initiatorEmail *string
-		var target string
-		var targetUserName *string
-		var targetEmail *string
-		var account string
-		var jsonMeta string
-		err := result.Scan(&id, &operation, &timestamp, &initiator, &initiatorName, &initiatorEmail, &target, &targetUserName, &targetEmail, &account, &jsonMeta)
-		if err != nil {
-			return nil, err
-		}
-
-		meta := make(map[string]any)
-		if jsonMeta != "" {
-			err = json.Unmarshal([]byte(jsonMeta), &meta)
-			if err != nil {
-				return nil, err
-			}
-		}
-
-		if targetUserName != nil {
-			name, err := store.fieldEncrypt.Decrypt(*targetUserName)
-			if err != nil {
-				cryptErr = fmt.Errorf("failed to decrypt username for target id: %s", target)
-				meta["username"] = fallbackName
-			} else {
-				meta["username"] = name
-			}
-		}
-
-		if targetEmail != nil {
-			email, err := store.fieldEncrypt.Decrypt(*targetEmail)
-			if err != nil {
-				cryptErr = fmt.Errorf("failed to decrypt email address for target id: %s", target)
-				meta["email"] = fallbackEmail
-			} else {
-				meta["email"] = email
-			}
-		}
-
-		event := &activity.Event{
-			Timestamp:   timestamp,
-			Activity:    operation,
-			ID:          uint64(id),
-			InitiatorID: initiator,
-			TargetID:    target,
-			AccountID:   account,
-			Meta:        meta,
-		}
-
-		if initiatorName != nil {
-			name, err := store.fieldEncrypt.Decrypt(*initiatorName)
-			if err != nil {
-				cryptErr = fmt.Errorf("failed to decrypt username of initiator: %s", initiator)
-				event.InitiatorName = fallbackName
-			} else {
-				event.InitiatorName = name
-			}
-		}
-
-		if initiatorEmail != nil {
-			email, err := store.fieldEncrypt.Decrypt(*initiatorEmail)
-			if err != nil {
-				cryptErr = fmt.Errorf("failed to decrypt email address of initiator: %s", initiator)
-				event.InitiatorEmail = fallbackEmail
-			} else {
-				event.InitiatorEmail = email
-			}
-		}
-
-		events = append(events, event)
-	}
-
-	if cryptErr != nil {
-		log.WithContext(ctx).Warnf("%s", cryptErr)
-	}
-
-	return events, nil
-}
-
-// Get returns "limit" number of events from index ordered descending or ascending by a timestamp
-func (store *Store) Get(ctx context.Context, accountID string, offset, limit int, descending bool) ([]*activity.Event, error) {
-	stmt := store.selectDescStatement
-	if !descending {
-		stmt = store.selectAscStatement
-	}
-
-	result, err := stmt.Query(accountID, limit, offset)
-	if err != nil {
-		return nil, err
-	}
-
-	defer result.Close() //nolint
-	return store.processResult(ctx, result)
-}
-
-// Save an event in the SQLite events table end encrypt the "email" element in meta map
-func (store *Store) Save(_ context.Context, event *activity.Event) (*activity.Event, error) {
-	var jsonMeta string
-	meta, err := store.saveDeletedUserEmailAndNameInEncrypted(event)
-	if err != nil {
-		return nil, err
-	}
-
-	if meta != nil {
-		metaBytes, err := json.Marshal(event.Meta)
-		if err != nil {
-			return nil, err
-		}
-		jsonMeta = string(metaBytes)
-	}
-
-	result, err := store.insertStatement.Exec(event.Activity, event.Timestamp, event.InitiatorID, event.TargetID, event.AccountID, jsonMeta)
-	if err != nil {
-		return nil, err
-	}
-
-	id, err := result.LastInsertId()
-	if err != nil {
-		return nil, err
-	}
-
-	eventCopy := event.Copy()
-	eventCopy.ID = uint64(id)
-	return eventCopy, nil
-}
-
-// saveDeletedUserEmailAndNameInEncrypted if the meta contains email and name then store it in encrypted way and delete
-// this item from meta map
-func (store *Store) saveDeletedUserEmailAndNameInEncrypted(event *activity.Event) (map[string]any, error) {
-	email, ok := event.Meta["email"]
-	if !ok {
-		return event.Meta, nil
-	}
-
-	name, ok := event.Meta["name"]
-	if !ok {
-		return event.Meta, nil
-	}
-
-	encryptedEmail, err := store.fieldEncrypt.Encrypt(fmt.Sprintf("%s", email))
-	if err != nil {
-		return nil, err
-	}
-	encryptedName, err := store.fieldEncrypt.Encrypt(fmt.Sprintf("%s", name))
-	if err != nil {
-		return nil, err
-	}
-
-	_, err = store.deleteUserStmt.Exec(event.TargetID, encryptedEmail, encryptedName, gcmEncAlgo)
-	if err != nil {
-		return nil, err
-	}
-
-	if len(event.Meta) == 2 {
-		return nil, nil // nolint
-	}
-	delete(event.Meta, "email")
-	delete(event.Meta, "name")
-	return event.Meta, nil
-}
-
-// Close the Store
-func (store *Store) Close(_ context.Context) error {
-	if store.db != nil {
-		return store.db.Close()
-	}
-	return nil
-}
-
-// createStore initializes and returns a new Store instance with prepared SQL statements.
-func createStore(crypt *FieldEncrypt, db *sql.DB) (*Store, error) {
-	insertStmt, err := db.Prepare(insertQuery)
-	if err != nil {
-		_ = db.Close()
-		return nil, err
-	}
-
-	selectDescStmt, err := db.Prepare(selectDescQuery)
-	if err != nil {
-		_ = db.Close()
-		return nil, err
-	}
-
-	selectAscStmt, err := db.Prepare(selectAscQuery)
-	if err != nil {
-		_ = db.Close()
-		return nil, err
-	}
-
-	deleteUserStmt, err := db.Prepare(insertDeleteUserQuery)
-	if err != nil {
-		_ = db.Close()
-		return nil, err
-	}
-
-	return &Store{
-		db:                  db,
-		fieldEncrypt:        crypt,
-		insertStatement:     insertStmt,
-		selectDescStatement: selectDescStmt,
-		selectAscStatement:  selectAscStmt,
-		deleteUserStmt:      deleteUserStmt,
-	}, nil
-}
-
-// checkColumnExists checks if a column exists in a specified table
-func checkColumnExists(db *sql.DB, tableName, columnName string) (bool, error) {
-	query := fmt.Sprintf("PRAGMA table_info(%s);", tableName)
-	rows, err := db.Query(query)
-	if err != nil {
-		return false, fmt.Errorf("failed to query table info: %w", err)
-	}
-	defer rows.Close()
-
-	for rows.Next() {
-		var cid int
-		var name, ctype string
-		var notnull, pk int
-		var dfltValue sql.NullString
-
-		err = rows.Scan(&cid, &name, &ctype, &notnull, &dfltValue, &pk)
-		if err != nil {
-			return false, fmt.Errorf("failed to scan row: %w", err)
-		}
-
-		if name == columnName {
-			return true, nil
-		}
-	}
-
-	if err = rows.Err(); err != nil {
-		return false, err
-	}
-
-	return false, nil
-}
diff --git a/management/server/activity/sqlite/crypt.go b/management/server/activity/store/crypt.go
similarity index 99%
rename from management/server/activity/sqlite/crypt.go
rename to management/server/activity/store/crypt.go
index 096f49ea..ce97347d 100644
--- a/management/server/activity/sqlite/crypt.go
+++ b/management/server/activity/store/crypt.go
@@ -1,4 +1,4 @@
-package sqlite
+package store
 
 import (
 	"bytes"
diff --git a/management/server/activity/store/migration.go b/management/server/activity/store/migration.go
new file mode 100644
index 00000000..af19a34e
--- /dev/null
+++ b/management/server/activity/store/migration.go
@@ -0,0 +1,185 @@
+package store
+
+import (
+	"context"
+	"fmt"
+
+	log "github.com/sirupsen/logrus"
+	"gorm.io/gorm"
+	"gorm.io/gorm/clause"
+
+	"github.com/netbirdio/netbird/management/server/activity"
+	"github.com/netbirdio/netbird/management/server/migration"
+)
+
+func migrate(ctx context.Context, crypt *FieldEncrypt, db *gorm.DB) error {
+	migrations := getMigrations(ctx, crypt)
+
+	for _, m := range migrations {
+		if err := m(db); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+type migrationFunc func(*gorm.DB) error
+
+func getMigrations(ctx context.Context, crypt *FieldEncrypt) []migrationFunc {
+	return []migrationFunc{
+		func(db *gorm.DB) error {
+			return migration.MigrateNewField[activity.DeletedUser](ctx, db, "name", "")
+		},
+		func(db *gorm.DB) error {
+			return migration.MigrateNewField[activity.DeletedUser](ctx, db, "enc_algo", "")
+		},
+		func(db *gorm.DB) error {
+			return migrateLegacyEncryptedUsersToGCM(ctx, db, crypt)
+		},
+		func(db *gorm.DB) error {
+			return migrateDuplicateDeletedUsers(ctx, db)
+		},
+	}
+}
+
+// migrateLegacyEncryptedUsersToGCM migrates previously encrypted data using
+// legacy CBC encryption with a static IV to the new GCM encryption method.
+func migrateLegacyEncryptedUsersToGCM(ctx context.Context, db *gorm.DB, crypt *FieldEncrypt) error {
+	model := &activity.DeletedUser{}
+
+	if !db.Migrator().HasTable(model) {
+		log.WithContext(ctx).Debugf("Table for %T does not exist, no CBC to GCM migration needed", model)
+		return nil
+	}
+
+	var deletedUsers []activity.DeletedUser
+	err := db.Model(model).Find(&deletedUsers, "enc_algo IS NULL OR enc_algo != ?", gcmEncAlgo).Error
+	if err != nil {
+		return fmt.Errorf("failed to query deleted_users: %w", err)
+	}
+
+	if len(deletedUsers) == 0 {
+		log.WithContext(ctx).Debug("No CBC encrypted deleted users to migrate")
+		return nil
+	}
+
+	if err = db.Transaction(func(tx *gorm.DB) error {
+		for _, user := range deletedUsers {
+			if err = updateDeletedUserData(tx, user, crypt); err != nil {
+				return fmt.Errorf("failed to migrate deleted user %s: %w", user.ID, err)
+			}
+		}
+		return nil
+	}); err != nil {
+		return err
+	}
+
+	log.WithContext(ctx).Debug("Successfully migrated CBC encrypted deleted users to GCM")
+
+	return nil
+}
+
+func updateDeletedUserData(transaction *gorm.DB, user activity.DeletedUser, crypt *FieldEncrypt) error {
+	var err error
+	var decryptedEmail, decryptedName string
+
+	if user.Email != "" {
+		decryptedEmail, err = crypt.LegacyDecrypt(user.Email)
+		if err != nil {
+			return fmt.Errorf("failed to decrypt email: %w", err)
+		}
+	}
+
+	if user.Name != "" {
+		decryptedName, err = crypt.LegacyDecrypt(user.Name)
+		if err != nil {
+			return fmt.Errorf("failed to decrypt name: %w", err)
+		}
+	}
+
+	updatedUser := user
+	updatedUser.EncAlgo = gcmEncAlgo
+
+	updatedUser.Email, err = crypt.Encrypt(decryptedEmail)
+	if err != nil {
+		return fmt.Errorf("failed to encrypt email: %w", err)
+	}
+
+	updatedUser.Name, err = crypt.Encrypt(decryptedName)
+	if err != nil {
+		return fmt.Errorf("failed to encrypt name: %w", err)
+	}
+
+	return transaction.Model(&updatedUser).Omit("id").Updates(updatedUser).Error
+}
+
+// MigrateDuplicateDeletedUsers removes duplicates and ensures the id column is marked as the primary key
+func migrateDuplicateDeletedUsers(ctx context.Context, db *gorm.DB) error {
+	model := &activity.DeletedUser{}
+	if !db.Migrator().HasTable(model) {
+		log.WithContext(ctx).Debugf("Table for %T does not exist, no duplicate migration needed", model)
+		return nil
+	}
+
+	isPrimaryKey, err := isColumnPrimaryKey[activity.DeletedUser](db, "id")
+	if err != nil {
+		return err
+	}
+
+	if isPrimaryKey {
+		log.WithContext(ctx).Debug("No duplicate deleted users to migrate")
+		return nil
+	}
+
+	if err = db.Transaction(func(tx *gorm.DB) error {
+		if err = tx.Migrator().RenameTable("deleted_users", "deleted_users_old"); err != nil {
+			return err
+		}
+
+		if err = tx.Migrator().CreateTable(model); err != nil {
+			return err
+		}
+
+		var deletedUsers []activity.DeletedUser
+		if err = tx.Table("deleted_users_old").Find(&deletedUsers).Error; err != nil {
+			return err
+		}
+
+		for _, deletedUser := range deletedUsers {
+			if err = tx.Clauses(clause.OnConflict{
+				Columns:   []clause.Column{{Name: "id"}},
+				DoUpdates: clause.AssignmentColumns([]string{"email", "name", "enc_algo"}),
+			}).Create(&deletedUser).Error; err != nil {
+				return err
+			}
+		}
+
+		return tx.Migrator().DropTable("deleted_users_old")
+	}); err != nil {
+		return err
+	}
+
+	log.WithContext(ctx).Debug("Successfully migrated duplicate deleted users")
+
+	return nil
+}
+
+// isColumnPrimaryKey checks if a column is a primary key in the given model
+func isColumnPrimaryKey[T any](db *gorm.DB, columnName string) (bool, error) {
+	var model T
+
+	cols, err := db.Migrator().ColumnTypes(&model)
+	if err != nil {
+		return false, err
+	}
+
+	for _, col := range cols {
+		if col.Name() == columnName {
+			isPrimaryKey, _ := col.PrimaryKey()
+			return isPrimaryKey, nil
+		}
+	}
+
+	return false, nil
+}
diff --git a/management/server/activity/store/sql_store.go b/management/server/activity/store/sql_store.go
new file mode 100644
index 00000000..80b16593
--- /dev/null
+++ b/management/server/activity/store/sql_store.go
@@ -0,0 +1,287 @@
+package store
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"path/filepath"
+	"runtime"
+	"strconv"
+
+	log "github.com/sirupsen/logrus"
+	"gorm.io/driver/postgres"
+	"gorm.io/driver/sqlite"
+	"gorm.io/gorm"
+	"gorm.io/gorm/clause"
+	"gorm.io/gorm/logger"
+
+	"github.com/netbirdio/netbird/management/server/activity"
+	"github.com/netbirdio/netbird/management/server/types"
+)
+
+const (
+	// eventSinkDB is the default name of the events database
+	eventSinkDB = "events.db"
+
+	fallbackName  = "unknown"
+	fallbackEmail = "unknown@unknown.com"
+
+	gcmEncAlgo = "GCM"
+
+	storeEngineEnv     = "NB_ACTIVITY_EVENT_STORE_ENGINE"
+	postgresDsnEnv     = "NB_ACTIVITY_EVENT_POSTGRES_DSN"
+	sqlMaxOpenConnsEnv = "NB_SQL_MAX_OPEN_CONNS"
+)
+
+type eventWithNames struct {
+	activity.Event
+	InitiatorName  string
+	InitiatorEmail string
+	TargetName     string
+	TargetEmail    string
+}
+
+// Store is the implementation of the activity.Store interface backed by SQLite
+type Store struct {
+	db           *gorm.DB
+	fieldEncrypt *FieldEncrypt
+}
+
+// NewSqlStore creates a new Store with an event table if not exists.
+func NewSqlStore(ctx context.Context, dataDir string, encryptionKey string) (*Store, error) {
+	crypt, err := NewFieldEncrypt(encryptionKey)
+	if err != nil {
+
+		return nil, err
+	}
+
+	db, err := initDatabase(ctx, dataDir)
+	if err != nil {
+		return nil, fmt.Errorf("initialize database: %w", err)
+	}
+
+	if err = migrate(ctx, crypt, db); err != nil {
+		return nil, fmt.Errorf("events database migration: %w", err)
+	}
+
+	err = db.AutoMigrate(&activity.Event{}, &activity.DeletedUser{})
+	if err != nil {
+		return nil, fmt.Errorf("events auto migrate: %w", err)
+	}
+
+	return &Store{
+		db:           db,
+		fieldEncrypt: crypt,
+	}, nil
+}
+
+func (store *Store) processResult(ctx context.Context, events []*eventWithNames) ([]*activity.Event, error) {
+	activityEvents := make([]*activity.Event, 0)
+	var cryptErr error
+
+	for _, event := range events {
+		e := event.Event
+		if e.Meta == nil {
+			e.Meta = make(map[string]any)
+		}
+
+		if event.TargetName != "" {
+			name, err := store.fieldEncrypt.Decrypt(event.TargetName)
+			if err != nil {
+				cryptErr = fmt.Errorf("failed to decrypt username for target id: %s", event.TargetName)
+				e.Meta["username"] = fallbackName
+			} else {
+				e.Meta["username"] = name
+			}
+		}
+
+		if event.TargetEmail != "" {
+			email, err := store.fieldEncrypt.Decrypt(event.TargetEmail)
+			if err != nil {
+				cryptErr = fmt.Errorf("failed to decrypt email address for target id: %s", event.TargetEmail)
+				e.Meta["email"] = fallbackEmail
+			} else {
+				e.Meta["email"] = email
+			}
+		}
+
+		if event.InitiatorName != "" {
+			name, err := store.fieldEncrypt.Decrypt(event.InitiatorName)
+			if err != nil {
+				cryptErr = fmt.Errorf("failed to decrypt username of initiator: %s", event.InitiatorName)
+				e.InitiatorName = fallbackName
+			} else {
+				e.InitiatorName = name
+			}
+		}
+
+		if event.InitiatorEmail != "" {
+			email, err := store.fieldEncrypt.Decrypt(event.InitiatorEmail)
+			if err != nil {
+				cryptErr = fmt.Errorf("failed to decrypt email address of initiator: %s", event.InitiatorEmail)
+				e.InitiatorEmail = fallbackEmail
+			} else {
+				e.InitiatorEmail = email
+			}
+		}
+
+		activityEvents = append(activityEvents, &e)
+	}
+
+	if cryptErr != nil {
+		log.WithContext(ctx).Warnf("%s", cryptErr)
+	}
+
+	return activityEvents, nil
+}
+
+// Get returns "limit" number of events from index ordered descending or ascending by a timestamp
+func (store *Store) Get(ctx context.Context, accountID string, offset, limit int, descending bool) ([]*activity.Event, error) {
+	baseQuery := store.db.Model(&activity.Event{}).
+		Select(`
+      events.*,
+      u.name  AS initiator_name,
+      u.email AS initiator_email,
+      t.name  AS target_name,
+      t.email AS target_email
+    `).
+		Joins(`LEFT JOIN deleted_users u ON u.id = events.initiator_id`).
+		Joins(`LEFT JOIN deleted_users t ON t.id = events.target_id`)
+
+	orderDir := "DESC"
+	if !descending {
+		orderDir = "ASC"
+	}
+
+	var events []*eventWithNames
+	err := baseQuery.Order("events.timestamp "+orderDir).Offset(offset).Limit(limit).
+		Find(&events, "account_id = ?", accountID).Error
+	if err != nil {
+		return nil, err
+	}
+
+	return store.processResult(ctx, events)
+}
+
+// Save an event in the SQLite events table end encrypt the "email" element in meta map
+func (store *Store) Save(_ context.Context, event *activity.Event) (*activity.Event, error) {
+	eventCopy := event.Copy()
+	meta, err := store.saveDeletedUserEmailAndNameInEncrypted(eventCopy)
+	if err != nil {
+		return nil, err
+	}
+	eventCopy.Meta = meta
+
+	if err = store.db.Create(eventCopy).Error; err != nil {
+		return nil, err
+	}
+
+	return eventCopy, nil
+}
+
+// saveDeletedUserEmailAndNameInEncrypted if the meta contains email and name then store it in encrypted way and delete
+// this item from meta map
+func (store *Store) saveDeletedUserEmailAndNameInEncrypted(event *activity.Event) (map[string]any, error) {
+	email, ok := event.Meta["email"]
+	if !ok {
+		return event.Meta, nil
+	}
+
+	name, ok := event.Meta["name"]
+	if !ok {
+		return event.Meta, nil
+	}
+
+	deletedUser := activity.DeletedUser{
+		ID:      event.TargetID,
+		EncAlgo: gcmEncAlgo,
+	}
+
+	encryptedEmail, err := store.fieldEncrypt.Encrypt(fmt.Sprintf("%s", email))
+	if err != nil {
+		return nil, err
+	}
+	deletedUser.Email = encryptedEmail
+
+	encryptedName, err := store.fieldEncrypt.Encrypt(fmt.Sprintf("%s", name))
+	if err != nil {
+		return nil, err
+	}
+	deletedUser.Name = encryptedName
+
+	err = store.db.Clauses(clause.OnConflict{
+		Columns:   []clause.Column{{Name: "id"}},
+		DoUpdates: clause.AssignmentColumns([]string{"email", "name"}),
+	}).Create(deletedUser).Error
+	if err != nil {
+		return nil, err
+	}
+
+	if len(event.Meta) == 2 {
+		return nil, nil // nolint
+	}
+	delete(event.Meta, "email")
+	delete(event.Meta, "name")
+	return event.Meta, nil
+}
+
+// Close the Store
+func (store *Store) Close(_ context.Context) error {
+	if store.db != nil {
+		sql, err := store.db.DB()
+		if err != nil {
+			return err
+		}
+		return sql.Close()
+	}
+	return nil
+}
+
+func initDatabase(ctx context.Context, dataDir string) (*gorm.DB, error) {
+	var dialector gorm.Dialector
+	var storeEngine = types.SqliteStoreEngine
+
+	if engine, ok := os.LookupEnv(storeEngineEnv); ok {
+		storeEngine = types.Engine(engine)
+	}
+
+	switch storeEngine {
+	case types.SqliteStoreEngine:
+		dialector = sqlite.Open(filepath.Join(dataDir, eventSinkDB))
+	case types.PostgresStoreEngine:
+		dsn, ok := os.LookupEnv(postgresDsnEnv)
+		if !ok {
+			return nil, fmt.Errorf("%s environment variable not set", postgresDsnEnv)
+		}
+		dialector = postgres.Open(dsn)
+	default:
+		return nil, fmt.Errorf("unsupported store engine: %s", storeEngine)
+	}
+	log.WithContext(ctx).Infof("using %s as activity event store engine", storeEngine)
+
+	db, err := gorm.Open(dialector, &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
+	if err != nil {
+		return nil, fmt.Errorf("open db connection: %w", err)
+	}
+
+	return configureConnectionPool(db, storeEngine)
+}
+
+func configureConnectionPool(db *gorm.DB, storeEngine types.Engine) (*gorm.DB, error) {
+	sqlDB, err := db.DB()
+	if err != nil {
+		return nil, err
+	}
+
+	if storeEngine == types.SqliteStoreEngine {
+		sqlDB.SetMaxOpenConns(1)
+	} else {
+		conns, err := strconv.Atoi(os.Getenv(sqlMaxOpenConnsEnv))
+		if err != nil {
+			conns = runtime.NumCPU()
+		}
+		sqlDB.SetMaxOpenConns(conns)
+	}
+
+	return db, nil
+}
diff --git a/management/server/event.go b/management/server/event.go
index 6342bfed..d94714e2 100644
--- a/management/server/event.go
+++ b/management/server/event.go
@@ -66,7 +66,7 @@ func (am *DefaultAccountManager) StoreEvent(ctx context.Context, initiatorID, ta
 		go func() {
 			_, err := am.eventStore.Save(ctx, &activity.Event{
 				Timestamp:   time.Now().UTC(),
-				Activity:    activityID,
+				Activity:    activityID.(activity.Activity),
 				InitiatorID: initiatorID,
 				TargetID:    targetID,
 				AccountID:   accountID,
@@ -143,11 +143,10 @@ func (am *DefaultAccountManager) getEventsUserInfo(ctx context.Context, events [
 		return eventUserInfos, nil
 	}
 
-	return am.getEventsExternalUserInfo(ctx, externalUserIds, eventUserInfos, userId)
+	return am.getEventsExternalUserInfo(ctx, externalUserIds, eventUserInfos)
 }
 
-func (am *DefaultAccountManager) getEventsExternalUserInfo(ctx context.Context, externalUserIds []string, eventUserInfos map[string]eventUserInfo, userId string) (map[string]eventUserInfo, error) {
-	externalAccountId := ""
+func (am *DefaultAccountManager) getEventsExternalUserInfo(ctx context.Context, externalUserIds []string, eventUserInfos map[string]eventUserInfo) (map[string]eventUserInfo, error) {
 	fetched := make(map[string]struct{})
 	externalUsers := []*types.User{}
 	for _, id := range externalUserIds {
@@ -161,34 +160,30 @@ func (am *DefaultAccountManager) getEventsExternalUserInfo(ctx context.Context,
 			continue
 		}
 
-		if externalAccountId != "" && externalAccountId != externalUser.AccountID {
-			return nil, fmt.Errorf("multiple external user accounts in events")
-		}
-
-		if externalAccountId == "" {
-			externalAccountId = externalUser.AccountID
-		}
-
 		fetched[id] = struct{}{}
 		externalUsers = append(externalUsers, externalUser)
 	}
 
-	// if we couldn't determine an account, return what we have
-	if externalAccountId == "" {
-		log.WithContext(ctx).Warnf("failed to determine external user account from users: %v", externalUserIds)
-		return eventUserInfos, nil
+	usersByExternalAccount := map[string][]*types.User{}
+	for _, u := range externalUsers {
+		if _, ok := usersByExternalAccount[u.AccountID]; !ok {
+			usersByExternalAccount[u.AccountID] = make([]*types.User, 0)
+		}
+		usersByExternalAccount[u.AccountID] = append(usersByExternalAccount[u.AccountID], u)
 	}
 
-	externalUserInfos, err := am.BuildUserInfosForAccount(ctx, externalAccountId, userId, externalUsers)
-	if err != nil {
-		return nil, err
-	}
+	for externalAccountId, externalUsers := range usersByExternalAccount {
+		externalUserInfos, err := am.BuildUserInfosForAccount(ctx, externalAccountId, "", externalUsers)
+		if err != nil {
+			return nil, err
+		}
 
-	for i, k := range externalUserInfos {
-		eventUserInfos[i] = eventUserInfo{
-			email:     k.Email,
-			name:      k.Name,
-			accountId: externalAccountId,
+		for i, k := range externalUserInfos {
+			eventUserInfos[i] = eventUserInfo{
+				email:     k.Email,
+				name:      k.Name,
+				accountId: externalAccountId,
+			}
 		}
 	}
 
diff --git a/management/server/group.go b/management/server/group.go
index 0bd84079..c26a0cfc 100644
--- a/management/server/group.go
+++ b/management/server/group.go
@@ -116,7 +116,7 @@ func (am *DefaultAccountManager) SaveGroups(ctx context.Context, accountID, user
 			return err
 		}
 
-		return transaction.SaveGroups(ctx, store.LockingStrengthUpdate, groupsToSave)
+		return transaction.SaveGroups(ctx, store.LockingStrengthUpdate, accountID, groupsToSave)
 	})
 	if err != nil {
 		return err
@@ -158,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 {
@@ -168,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)
 		})
@@ -184,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)
 		})
diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go
index a7ed639c..5786dc87 100644
--- a/management/server/grpcserver.go
+++ b/management/server/grpcserver.go
@@ -480,7 +480,27 @@ func (s *GRPCServer) Login(ctx context.Context, req *proto.EncryptedMessage) (*p
 		s.ephemeralManager.OnPeerDisconnected(ctx, peer)
 	}
 
+	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")
+	}
+
+	encryptedResp, err := encryption.EncryptMessage(peerKey, s.wgKey, loginResp)
+	if err != nil {
+		log.WithContext(ctx).Warnf("failed encrypting peer %s message", peer.ID)
+		return nil, status.Errorf(codes.Internal, "failed logging in peer")
+	}
+
+	return &proto.EncryptedMessage{
+		WgPubKey: s.wgKey.PublicKey().String(),
+		Body:     encryptedResp,
+	}, 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 {
@@ -488,22 +508,20 @@ func (s *GRPCServer) Login(ctx context.Context, req *proto.EncryptedMessage) (*p
 		}
 	}
 
+	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(), false),
+		PeerConfig:    toPeerConfig(peer, netMap.Network, s.accountManager.GetDNSDomain(settings), settings),
 		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)
-		return nil, status.Errorf(codes.Internal, "failed logging in peer")
-	}
 
-	return &proto.EncryptedMessage{
-		WgPubKey: s.wgKey.PublicKey().String(),
-		Body:     encryptedResp,
-	}, nil
+	return loginResp, nil
 }
 
 // processJwtToken validates the existence of a JWT token in the login request, and returns the corresponding user ID if
@@ -614,20 +632,21 @@ func toNetbirdConfig(config *types.Config, turnCredentials *Token, relayToken *T
 	return nbConfig
 }
 
-func toPeerConfig(peer *nbpeer.Peer, network *types.Network, dnsName string, dnsResolutionOnRoutingPeerEnabled bool) *proto.PeerConfig {
+func toPeerConfig(peer *nbpeer.Peer, network *types.Network, dnsName string, settings *types.Settings) *proto.PeerConfig {
 	netmask, _ := network.Net.Mask.Size()
 	fqdn := peer.FQDN(dnsName)
 	return &proto.PeerConfig{
 		Address:                         fmt.Sprintf("%s/%d", peer.IP.String(), netmask), // take it from the network
 		SshConfig:                       &proto.SSHConfig{SshEnabled: peer.SSHEnabled},
 		Fqdn:                            fqdn,
-		RoutingPeerDnsResolutionEnabled: dnsResolutionOnRoutingPeerEnabled,
+		RoutingPeerDnsResolutionEnabled: settings.RoutingPeerDNSResolutionEnabled,
+		LazyConnectionEnabled:           settings.LazyConnectionEnabled,
 	}
 }
 
-func toSyncResponse(ctx context.Context, config *types.Config, peer *nbpeer.Peer, turnCredentials *Token, relayCredentials *Token, networkMap *types.NetworkMap, dnsName string, checks []*posture.Checks, dnsCache *DNSConfigCache, dnsResolutionOnRoutingPeerEnabled bool, extraSettings *types.ExtraSettings) *proto.SyncResponse {
+func toSyncResponse(ctx context.Context, config *types.Config, peer *nbpeer.Peer, turnCredentials *Token, relayCredentials *Token, networkMap *types.NetworkMap, dnsName string, checks []*posture.Checks, dnsCache *DNSConfigCache, settings *types.Settings, extraSettings *types.ExtraSettings) *proto.SyncResponse {
 	response := &proto.SyncResponse{
-		PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, dnsResolutionOnRoutingPeerEnabled),
+		PeerConfig: toPeerConfig(peer, networkMap.Network, dnsName, settings),
 		NetworkMap: &proto.NetworkMap{
 			Serial:    networkMap.Network.CurrentSerial(),
 			Routes:    toProtocolRoutes(networkMap.Routes),
@@ -673,10 +692,11 @@ func toSyncResponse(ctx context.Context, config *types.Config, peer *nbpeer.Peer
 func appendRemotePeerConfig(dst []*proto.RemotePeerConfig, peers []*nbpeer.Peer, dnsName string) []*proto.RemotePeerConfig {
 	for _, rPeer := range peers {
 		dst = append(dst, &proto.RemotePeerConfig{
-			WgPubKey:   rPeer.Key,
-			AllowedIps: []string{rPeer.IP.String() + "/32"},
-			SshConfig:  &proto.SSHConfig{SshPubKey: []byte(rPeer.SSHKey)},
-			Fqdn:       rPeer.FQDN(dnsName),
+			WgPubKey:     rPeer.Key,
+			AllowedIps:   []string{rPeer.IP.String() + "/32"},
+			SshConfig:    &proto.SSHConfig{SshPubKey: []byte(rPeer.SSHKey)},
+			Fqdn:         rPeer.FQDN(dnsName),
+			AgentVersion: rPeer.Meta.WtVersion,
 		})
 	}
 	return dst
@@ -712,7 +732,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, settings.Extra)
 
 	encryptedResp, err := encryption.EncryptMessage(peerKey, s.wgKey, plainResp)
 	if err != nil {
@@ -829,6 +849,7 @@ func (s *GRPCServer) GetPKCEAuthorizationFlow(ctx context.Context, req *proto.En
 			RedirectURLs:          s.config.PKCEAuthorizationFlow.ProviderConfig.RedirectURLs,
 			UseIDToken:            s.config.PKCEAuthorizationFlow.ProviderConfig.UseIDToken,
 			DisablePromptLogin:    s.config.PKCEAuthorizationFlow.ProviderConfig.DisablePromptLogin,
+			LoginFlag:             uint32(s.config.PKCEAuthorizationFlow.ProviderConfig.LoginFlag),
 		},
 	}
 
diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml
index c699e9ee..58134d37 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,8 +112,17 @@ 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'
+        lazy_connection_enabled:
+          x-experimental: true
+          description: Enables or disables experimental lazy connection
+          type: boolean
+          example: true
       required:
         - peer_login_expiration_enabled
         - peer_login_expiration
@@ -191,11 +221,25 @@ components:
     UserPermissions:
       type: object
       properties:
-        dashboard_view:
-          description: User's permission to view the dashboard
-          type: string
-          enum: [ "limited", "blocked", "full" ]
-          example: limited
+        is_restricted:
+          type: boolean
+          description: Indicates whether this User's Peers view is restricted
+        modules:
+          type: object
+          additionalProperties:
+            type: object
+            additionalProperties:
+              type: boolean
+            propertyNames:
+              type: string
+              description: The operation type
+          propertyNames:
+            type: string
+            description: The module name
+          example: {"networks": { "read": true, "create": false, "update": false, "delete": false}, "peers": { "read": false, "create": false, "update": false, "delete": false} }
+      required:
+        - modules
+        - is_restricted
     UserRequest:
       type: object
       properties:
@@ -1881,13 +1925,71 @@ components:
         - os
         - address
         - dns_label
-    NetworkTrafficEvent:
+    NetworkTrafficUser:
+      type: object
+      properties:
+        id:
+          type: string
+          description: "UserID is the ID of the user that initiated the event (can be empty as not every event is user-initiated)."
+          example: "google-oauth2|123456789012345678901"
+        email:
+          type: string
+          description: "Email of the user who initiated the event (if any)."
+          example: "alice@netbird.io"
+        name:
+            type: string
+            description: "Name of the user who initiated the event (if any)."
+            example: "Alice Smith"
+      required:
+        - id
+        - email
+        - name
+    NetworkTrafficPolicy:
       type: object
       properties:
         id:
           type: string
-          description: "ID of the event. Unique."
-          example: "18e204d6-f7c6-405d-8025-70becb216add"
+          description: "ID of the policy that allowed this event."
+          example: "ch8i4ug6lnn4g9hqv7m0"
+        name:
+          type: string
+          description: "Name of the policy that allowed this event."
+          example: "All to All"
+      required:
+        - id
+        - name
+    NetworkTrafficICMP:
+      type: object
+      properties:
+        type:
+          type: integer
+          description: "ICMP type (if applicable)."
+          example: 8
+        code:
+          type: integer
+          description: "ICMP code (if applicable)."
+          example: 0
+      required:
+        - type
+        - code
+    NetworkTrafficSubEvent:
+      type: object
+      properties:
+        type:
+          type: string
+          description: Type of the event (e.g., TYPE_UNKNOWN, TYPE_START, TYPE_END, TYPE_DROP).
+          example: TYPE_START
+        timestamp:
+          type: string
+          format: date-time
+          description: Timestamp of the event as sent by the peer.
+          example: 2025-03-20T16:23:58.125397Z
+      required:
+        - type
+        - timestamp
+    NetworkTrafficEvent:
+      type: object
+      properties:
         flow_id:
           type: string
           description: "FlowID is the ID of the connection flow. Not unique because it can be the same for multiple events (e.g., start and end of the connection)."
@@ -1896,43 +1998,20 @@ components:
           type: string
           description: "ID of the reporter of the event (e.g., the peer that reported the event)."
           example: "ch8i4ug6lnn4g9hqv7m0"
-        timestamp:
-          type: string
-          format: date-time
-          description: "Timestamp of the event. Send by the peer."
-          example: "2025-03-20T16:23:58.125397Z"
-        receive_timestamp:
-          type: string
-          format: date-time
-          description: "Timestamp when the event was received by our API."
-          example: "2025-03-20T16:23:58.125397Z"
         source:
           $ref: '#/components/schemas/NetworkTrafficEndpoint'
-        user_id:
-          type: string
-          nullable: true
-          description: "UserID is the ID of the user that initiated the event (can be empty as not every event is user-initiated)."
-          example: "google-oauth2|123456789012345678901"
-        user_email:
-          type: string
-          nullable: true
-          description: "Email of the user who initiated the event (if any)."
-          example: "alice@netbird.io"
-        user_name:
-          type: string
-          nullable: true
-          description: "Name of the user who initiated the event (if any)."
-          example: "Alice Smith"
         destination:
           $ref: '#/components/schemas/NetworkTrafficEndpoint'
+        user:
+          $ref: '#/components/schemas/NetworkTrafficUser'
+        policy:
+          $ref: '#/components/schemas/NetworkTrafficPolicy'
+        icmp:
+          $ref: '#/components/schemas/NetworkTrafficICMP'
         protocol:
           type: integer
           description: "Protocol is the protocol of the traffic (e.g. 1 = ICMP, 6 = TCP, 17 = UDP, etc.)."
           example: 6
-        type:
-          type: string
-          description: "Type of the event (e.g. TYPE_UNKNOWN, TYPE_START, TYPE_END, TYPE_DROP)."
-          example: "TYPE_START"
         direction:
           type: string
           description: "Direction of the traffic (e.g. DIRECTION_UNKNOWN, INGRESS, EGRESS)."
@@ -1953,43 +2032,54 @@ components:
           type: integer
           description: "Number of packets transmitted."
           example: 5
-        policy_id:
-          type: string
-          description: "ID of the policy that allowed this event."
-          example: "ch8i4ug6lnn4g9hqv7m0"
-        policy_name:
-          type: string
-          description: "Name of the policy that allowed this event."
-          example: "All to All"
-        icmp_type:
-          type: integer
-          description: "ICMP type (if applicable)."
-          example: 8
-        icmp_code:
-          type: integer
-          description: "ICMP code (if applicable)."
-          example: 0
+        events:
+          type: array
+          description: "List of events that are correlated to this flow (e.g., start, end)."
+          items:
+            $ref: '#/components/schemas/NetworkTrafficSubEvent'
       required:
         - id
         - flow_id
         - reporter_id
-        - timestamp
         - receive_timestamp
         - source
-        - user_id
-        - user_email
         - destination
+        - user
+        - policy
+        - icmp
         - protocol
-        - type
         - direction
         - rx_bytes
         - rx_packets
         - tx_bytes
         - tx_packets
-        - policy_id
-        - policy_name
-        - icmp_type
-        - icmp_code
+        - 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
@@ -3978,6 +4068,31 @@ paths:
           "$ref": "#/components/responses/forbidden"
         '500':
           "$ref": "#/components/responses/internal_error"
+  /api/networks/routers:
+    get:
+      summary: List all Network Routers
+      description: Returns a list of all routers in a network
+      tags: [ Networks ]
+      security:
+        - BearerAuth: [ ]
+        - TokenAuth: [ ]
+      responses:
+        '200':
+          description: A JSON Array of Routers
+          content:
+            application/json:
+              schema:
+                type: array
+                items:
+                  $ref: '#/components/schemas/NetworkRouter'
+        '400':
+          "$ref": "#/components/responses/bad_request"
+        '401':
+          "$ref": "#/components/responses/requires_authentication"
+        '403':
+          "$ref": "#/components/responses/forbidden"
+        '500':
+          "$ref": "#/components/responses/internal_error"
   /api/dns/nameservers:
     get:
       summary: List all Nameserver Groups
@@ -4206,15 +4321,90 @@ 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: reporter_id
+          in: query
+          description: Filter by reporter 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: connection_type
+          in: query
+          description: Filter by connection type
+          required: false
+          schema:
+            type: string
+            enum: [P2P, ROUTED]
+        - name: direction
+          in: query
+          description: Filter by direction
+          required: false
+          schema:
+            type: string
+            enum: [INGRESS, EGRESS, DIRECTION_UNKNOWN]
+        - name: search
+          in: query
+          description: Case-insensitive partial match on user email, source/destination names, and source/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 9bdb3e4a..0a09d7ca 100644
--- a/management/server/http/api/types.gen.go
+++ b/management/server/http/api/types.gen.go
@@ -178,11 +178,25 @@ const (
 	UserStatusInvited UserStatus = "invited"
 )
 
-// Defines values for UserPermissionsDashboardView.
+// Defines values for GetApiEventsNetworkTrafficParamsType.
 const (
-	UserPermissionsDashboardViewBlocked UserPermissionsDashboardView = "blocked"
-	UserPermissionsDashboardViewFull    UserPermissionsDashboardView = "full"
-	UserPermissionsDashboardViewLimited UserPermissionsDashboardView = "limited"
+	GetApiEventsNetworkTrafficParamsTypeTYPEDROP    GetApiEventsNetworkTrafficParamsType = "TYPE_DROP"
+	GetApiEventsNetworkTrafficParamsTypeTYPEEND     GetApiEventsNetworkTrafficParamsType = "TYPE_END"
+	GetApiEventsNetworkTrafficParamsTypeTYPESTART   GetApiEventsNetworkTrafficParamsType = "TYPE_START"
+	GetApiEventsNetworkTrafficParamsTypeTYPEUNKNOWN GetApiEventsNetworkTrafficParamsType = "TYPE_UNKNOWN"
+)
+
+// Defines values for GetApiEventsNetworkTrafficParamsConnectionType.
+const (
+	GetApiEventsNetworkTrafficParamsConnectionTypeP2P    GetApiEventsNetworkTrafficParamsConnectionType = "P2P"
+	GetApiEventsNetworkTrafficParamsConnectionTypeROUTED GetApiEventsNetworkTrafficParamsConnectionType = "ROUTED"
+)
+
+// Defines values for GetApiEventsNetworkTrafficParamsDirection.
+const (
+	GetApiEventsNetworkTrafficParamsDirectionDIRECTIONUNKNOWN GetApiEventsNetworkTrafficParamsDirection = "DIRECTION_UNKNOWN"
+	GetApiEventsNetworkTrafficParamsDirectionEGRESS           GetApiEventsNetworkTrafficParamsDirection = "EGRESS"
+	GetApiEventsNetworkTrafficParamsDirectionINGRESS          GetApiEventsNetworkTrafficParamsDirection = "INGRESS"
 )
 
 // AccessiblePeer defines model for AccessiblePeer.
@@ -223,6 +237,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 +273,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"`
@@ -261,6 +289,9 @@ type AccountSettings struct {
 	// JwtGroupsEnabled Allows extract groups from JWT claim and add it to account groups.
 	JwtGroupsEnabled *bool `json:"jwt_groups_enabled,omitempty"`
 
+	// LazyConnectionEnabled Enables or disables experimental lazy connection
+	LazyConnectionEnabled *bool `json:"lazy_connection_enabled,omitempty"`
+
 	// PeerInactivityExpiration Period of time of inactivity after which peer session expires (seconds).
 	PeerInactivityExpiration int `json:"peer_inactivity_expiration"`
 
@@ -852,30 +883,17 @@ type NetworkTrafficEvent struct {
 	// Direction Direction of the traffic (e.g. DIRECTION_UNKNOWN, INGRESS, EGRESS).
 	Direction string `json:"direction"`
 
-	// FlowId FlowID is the ID of the connection flow. Not unique because it can be the same for multiple events (e.g., start and end of the connection).
-	FlowId string `json:"flow_id"`
-
-	// IcmpCode ICMP code (if applicable).
-	IcmpCode int `json:"icmp_code"`
-
-	// IcmpType ICMP type (if applicable).
-	IcmpType int `json:"icmp_type"`
+	// Events List of events that are correlated to this flow (e.g., start, end).
+	Events []NetworkTrafficSubEvent `json:"events"`
 
-	// Id ID of the event. Unique.
-	Id string `json:"id"`
-
-	// PolicyId ID of the policy that allowed this event.
-	PolicyId string `json:"policy_id"`
-
-	// PolicyName Name of the policy that allowed this event.
-	PolicyName string `json:"policy_name"`
+	// FlowId FlowID is the ID of the connection flow. Not unique because it can be the same for multiple events (e.g., start and end of the connection).
+	FlowId string               `json:"flow_id"`
+	Icmp   NetworkTrafficICMP   `json:"icmp"`
+	Policy NetworkTrafficPolicy `json:"policy"`
 
 	// Protocol Protocol is the protocol of the traffic (e.g. 1 = ICMP, 6 = TCP, 17 = UDP, etc.).
 	Protocol int `json:"protocol"`
 
-	// ReceiveTimestamp Timestamp when the event was received by our API.
-	ReceiveTimestamp time.Time `json:"receive_timestamp"`
-
 	// ReporterId ID of the reporter of the event (e.g., the peer that reported the event).
 	ReporterId string `json:"reporter_id"`
 
@@ -886,26 +904,39 @@ type NetworkTrafficEvent struct {
 	RxPackets int                    `json:"rx_packets"`
 	Source    NetworkTrafficEndpoint `json:"source"`
 
-	// Timestamp Timestamp of the event. Send by the peer.
-	Timestamp time.Time `json:"timestamp"`
-
 	// TxBytes Number of bytes transmitted.
 	TxBytes int `json:"tx_bytes"`
 
 	// TxPackets Number of packets transmitted.
-	TxPackets int `json:"tx_packets"`
+	TxPackets int                `json:"tx_packets"`
+	User      NetworkTrafficUser `json:"user"`
+}
 
-	// Type Type of the event (e.g. TYPE_UNKNOWN, TYPE_START, TYPE_END, TYPE_DROP).
-	Type string `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"`
 
-	// UserEmail Email of the user who initiated the event (if any).
-	UserEmail *string `json:"user_email"`
+	// PageSize Number of items per page
+	PageSize int `json:"page_size"`
 
-	// UserId UserID is the ID of the user that initiated the event (can be empty as not every event is user-initiated).
-	UserId *string `json:"user_id"`
+	// TotalPages Total number of pages available
+	TotalPages int `json:"total_pages"`
 
-	// UserName Name of the user who initiated the event (if any).
-	UserName *string `json:"user_name"`
+	// TotalRecords Total number of event records available
+	TotalRecords int `json:"total_records"`
+}
+
+// NetworkTrafficICMP defines model for NetworkTrafficICMP.
+type NetworkTrafficICMP struct {
+	// Code ICMP code (if applicable).
+	Code int `json:"code"`
+
+	// Type ICMP type (if applicable).
+	Type int `json:"type"`
 }
 
 // NetworkTrafficLocation defines model for NetworkTrafficLocation.
@@ -917,6 +948,36 @@ type NetworkTrafficLocation struct {
 	CountryCode string `json:"country_code"`
 }
 
+// NetworkTrafficPolicy defines model for NetworkTrafficPolicy.
+type NetworkTrafficPolicy struct {
+	// Id ID of the policy that allowed this event.
+	Id string `json:"id"`
+
+	// Name Name of the policy that allowed this event.
+	Name string `json:"name"`
+}
+
+// NetworkTrafficSubEvent defines model for NetworkTrafficSubEvent.
+type NetworkTrafficSubEvent struct {
+	// Timestamp Timestamp of the event as sent by the peer.
+	Timestamp time.Time `json:"timestamp"`
+
+	// Type Type of the event (e.g., TYPE_UNKNOWN, TYPE_START, TYPE_END, TYPE_DROP).
+	Type string `json:"type"`
+}
+
+// NetworkTrafficUser defines model for NetworkTrafficUser.
+type NetworkTrafficUser struct {
+	// Email Email of the user who initiated the event (if any).
+	Email string `json:"email"`
+
+	// Id UserID is the ID of the user that initiated the event (can be empty as not every event is user-initiated).
+	Id string `json:"id"`
+
+	// Name Name of the user who initiated the event (if any).
+	Name string `json:"name"`
+}
+
 // OSVersionCheck Posture check for the version of operating system
 type OSVersionCheck struct {
 	// Android Posture check for the version of operating system
@@ -1710,13 +1771,11 @@ type UserCreateRequest struct {
 
 // UserPermissions defines model for UserPermissions.
 type UserPermissions struct {
-	// DashboardView User's permission to view the dashboard
-	DashboardView *UserPermissionsDashboardView `json:"dashboard_view,omitempty"`
+	// IsRestricted Indicates whether this User's Peers view is restricted
+	IsRestricted bool                       `json:"is_restricted"`
+	Modules      map[string]map[string]bool `json:"modules"`
 }
 
-// UserPermissionsDashboardView User's permission to view the dashboard
-type UserPermissionsDashboardView string
-
 // UserRequest defines model for UserRequest.
 type UserRequest struct {
 	// AutoGroups Group IDs to auto-assign to peers registered by this user
@@ -1729,6 +1788,51 @@ 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"`
+
+	// ReporterId Filter by reporter ID
+	ReporterId *string `form:"reporter_id,omitempty" json:"reporter_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"`
+
+	// ConnectionType Filter by connection type
+	ConnectionType *GetApiEventsNetworkTrafficParamsConnectionType `form:"connection_type,omitempty" json:"connection_type,omitempty"`
+
+	// Direction Filter by direction
+	Direction *GetApiEventsNetworkTrafficParamsDirection `form:"direction,omitempty" json:"direction,omitempty"`
+
+	// Search Case-insensitive partial match on user email, source/destination names, and source/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
+
+// GetApiEventsNetworkTrafficParamsConnectionType defines parameters for GetApiEventsNetworkTraffic.
+type GetApiEventsNetworkTrafficParamsConnectionType 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 483bb989..3d4de31d 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()
diff --git a/management/server/http/handlers/accounts/accounts_handler.go b/management/server/http/handlers/accounts/accounts_handler.go
index 6c8f8028..638524e3 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,12 @@ 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
+	}
+	if req.Settings.LazyConnectionEnabled != nil {
+		settings.LazyConnectionEnabled = *req.Settings.LazyConnectionEnabled
+	}
 
 	updatedAccount, err := h.accountManager.UpdateAccountSettings(r.Context(), accountID, userID, settings)
 	if err != nil {
@@ -120,7 +132,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 +167,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 +184,8 @@ func toAccountResponse(accountID string, settings *types.Settings) *api.Account
 		JwtAllowGroups:                  &jwtAllowGroups,
 		RegularUsersViewBlocked:         settings.RegularUsersViewBlocked,
 		RoutingPeerDnsResolutionEnabled: &settings.RoutingPeerDNSResolutionEnabled,
+		LazyConnectionEnabled:           &settings.LazyConnectionEnabled,
+		DnsDomain:                       &settings.DNSDomain,
 	}
 
 	if settings.Extra != nil {
@@ -177,7 +197,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/networks/routers_handler.go b/management/server/http/handlers/networks/routers_handler.go
index f1a3fba0..6b00534f 100644
--- a/management/server/http/handlers/networks/routers_handler.go
+++ b/management/server/http/handlers/networks/routers_handler.go
@@ -19,7 +19,8 @@ type routersHandler struct {
 
 func addRouterEndpoints(routersManager routers.Manager, router *mux.Router) {
 	routersHandler := newRoutersHandler(routersManager)
-	router.HandleFunc("/networks/{networkId}/routers", routersHandler.getAllRouters).Methods("GET", "OPTIONS")
+	router.HandleFunc("/networks/routers", routersHandler.getAllRouters).Methods("GET", "OPTIONS")
+	router.HandleFunc("/networks/{networkId}/routers", routersHandler.getNetworkRouters).Methods("GET", "OPTIONS")
 	router.HandleFunc("/networks/{networkId}/routers", routersHandler.createRouter).Methods("POST", "OPTIONS")
 	router.HandleFunc("/networks/{networkId}/routers/{routerId}", routersHandler.getRouter).Methods("GET", "OPTIONS")
 	router.HandleFunc("/networks/{networkId}/routers/{routerId}", routersHandler.updateRouter).Methods("PUT", "OPTIONS")
@@ -41,6 +42,31 @@ func (h *routersHandler) getAllRouters(w http.ResponseWriter, r *http.Request) {
 
 	accountID, userID := userAuth.AccountId, userAuth.UserId
 
+	routersMap, err := h.routersManager.GetAllRoutersInAccount(r.Context(), accountID, userID)
+	if err != nil {
+		util.WriteError(r.Context(), err, w)
+		return
+	}
+
+	routersResponse := make([]*api.NetworkRouter, 0)
+	for _, routers := range routersMap {
+		for _, router := range routers {
+			routersResponse = append(routersResponse, router.ToAPIResponse())
+		}
+	}
+
+	util.WriteJSONObject(r.Context(), w, routersResponse)
+}
+
+func (h *routersHandler) getNetworkRouters(w http.ResponseWriter, r *http.Request) {
+	userAuth, err := nbcontext.GetUserAuthFromContext(r.Context())
+	if err != nil {
+		util.WriteError(r.Context(), err, w)
+		return
+	}
+
+	accountID, userID := userAuth.AccountId, userAuth.UserId
+
 	networkID := mux.Vars(r)["networkId"]
 	routers, err := h.routersManager.GetAllRoutersInNetwork(r.Context(), accountID, userID, networkID)
 	if err != nil {
diff --git a/management/server/http/handlers/peers/peers_handler.go b/management/server/http/handlers/peers/peers_handler.go
index fa78836d..58ea06ea 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/users/users_handler.go b/management/server/http/handlers/users/users_handler.go
index c69c6b94..ac04b8e3 100644
--- a/management/server/http/handlers/users/users_handler.go
+++ b/management/server/http/handlers/users/users_handler.go
@@ -13,6 +13,7 @@ import (
 	"github.com/netbirdio/netbird/management/server/http/util"
 	"github.com/netbirdio/netbird/management/server/status"
 	"github.com/netbirdio/netbird/management/server/types"
+	"github.com/netbirdio/netbird/management/server/users"
 
 	nbcontext "github.com/netbirdio/netbird/management/server/context"
 )
@@ -272,15 +273,33 @@ func (h *handler) getCurrentUser(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	accountID, userID := userAuth.AccountId, userAuth.UserId
-
-	user, err := h.accountManager.GetCurrentUserInfo(ctx, accountID, userID)
+	user, err := h.accountManager.GetCurrentUserInfo(ctx, userAuth)
 	if err != nil {
 		util.WriteError(r.Context(), err, w)
 		return
 	}
 
-	util.WriteJSONObject(r.Context(), w, toUserResponse(user, userID))
+	util.WriteJSONObject(r.Context(), w, toUserWithPermissionsResponse(user, userAuth.UserId))
+}
+
+func toUserWithPermissionsResponse(user *users.UserInfoWithPermissions, userID string) *api.User {
+	response := toUserResponse(user.UserInfo, userID)
+
+	// stringify modules and operations keys
+	modules := make(map[string]map[string]bool)
+	for module, operations := range user.Permissions {
+		modules[string(module)] = make(map[string]bool)
+		for op, val := range operations {
+			modules[string(module)][string(op)] = val
+		}
+	}
+
+	response.Permissions = &api.UserPermissions{
+		IsRestricted: user.Restricted,
+		Modules:      modules,
+	}
+
+	return response
 }
 
 func toUserResponse(user *types.UserInfo, currenUserID string) *api.User {
@@ -316,8 +335,5 @@ func toUserResponse(user *types.UserInfo, currenUserID string) *api.User {
 		IsBlocked:     user.IsBlocked,
 		LastLogin:     &user.LastLogin,
 		Issued:        &user.Issued,
-		Permissions: &api.UserPermissions{
-			DashboardView: (*api.UserPermissionsDashboardView)(&user.Permissions.DashboardView),
-		},
 	}
 }
diff --git a/management/server/http/middleware/auth_middleware.go b/management/server/http/middleware/auth_middleware.go
index a8e6790a..f2732fbf 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/migration/migration.go b/management/server/migration/migration.go
index d7abbad4..c8a852e0 100644
--- a/management/server/migration/migration.go
+++ b/management/server/migration/migration.go
@@ -352,3 +352,24 @@ func MigrateNewField[T any](ctx context.Context, db *gorm.DB, columnName string,
 	log.WithContext(ctx).Infof("Migration of empty %s to default value in table %s completed", columnName, tableName)
 	return nil
 }
+
+func DropIndex[T any](ctx context.Context, db *gorm.DB, indexName string) error {
+	var model T
+
+	if !db.Migrator().HasTable(&model) {
+		log.WithContext(ctx).Debugf("table for %T does not exist, no migration needed", model)
+		return nil
+	}
+
+	if !db.Migrator().HasIndex(&model, indexName) {
+		log.WithContext(ctx).Debugf("index %s does not exist in table %T, no migration needed", indexName, model)
+		return nil
+	}
+
+	if err := db.Migrator().DropIndex(&model, indexName); err != nil {
+		return fmt.Errorf("failed to drop index %s: %w", indexName, err)
+	}
+
+	log.WithContext(ctx).Infof("dropped index %s from table %T", indexName, model)
+	return nil
+}
diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go
index 870fe321..ed47d391 100644
--- a/management/server/mock_server/account_mock.go
+++ b/management/server/mock_server/account_mock.go
@@ -19,6 +19,7 @@ import (
 	"github.com/netbirdio/netbird/management/server/posture"
 	"github.com/netbirdio/netbird/management/server/store"
 	"github.com/netbirdio/netbird/management/server/types"
+	"github.com/netbirdio/netbird/management/server/users"
 	"github.com/netbirdio/netbird/route"
 )
 
@@ -83,7 +84,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)
@@ -112,10 +113,12 @@ type MockAccountManager struct {
 	DeleteSetupKeyFunc                  func(ctx context.Context, accountID, userID, keyID string) error
 	BuildUserInfosForAccountFunc        func(ctx context.Context, accountID, initiatorUserID string, accountUsers []*types.User) (map[string]*types.UserInfo, error)
 	GetStoreFunc                        func() store.Store
-	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)
+	GetCurrentUserInfoFunc              func(ctx context.Context, userAuth nbcontext.UserAuth) (*users.UserInfoWithPermissions, error)
+	GetAccountMetaFunc                  func(ctx context.Context, accountID, userID string) (*types.AccountMeta, error)
+
+	GetOrCreateAccountByPrivateDomainFunc func(ctx context.Context, initiatorId, domain string) (*types.Account, bool, error)
 }
 
 func (am *MockAccountManager) UpdateAccountPeers(ctx context.Context, accountID string) {
@@ -619,9 +622,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 ""
 }
@@ -803,6 +806,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 {
@@ -852,11 +863,11 @@ func (am *MockAccountManager) GetStore() store.Store {
 	return nil
 }
 
-func (am *MockAccountManager) CreateAccountByPrivateDomain(ctx context.Context, initiatorId, domain string) (*types.Account, error) {
-	if am.CreateAccountByPrivateDomainFunc != nil {
-		return am.CreateAccountByPrivateDomainFunc(ctx, initiatorId, domain)
+func (am *MockAccountManager) GetOrCreateAccountByPrivateDomain(ctx context.Context, initiatorId, domain string) (*types.Account, bool, error) {
+	if am.GetOrCreateAccountByPrivateDomainFunc != nil {
+		return am.GetOrCreateAccountByPrivateDomainFunc(ctx, initiatorId, domain)
 	}
-	return nil, status.Errorf(codes.Unimplemented, "method CreateAccountByPrivateDomain is not implemented")
+	return nil, false, status.Errorf(codes.Unimplemented, "method GetOrCreateAccountByPrivateDomainFunc is not implemented")
 }
 
 func (am *MockAccountManager) UpdateToPrimaryAccount(ctx context.Context, accountId string) (*types.Account, error) {
@@ -873,9 +884,9 @@ 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) {
+func (am *MockAccountManager) GetCurrentUserInfo(ctx context.Context, userAuth nbcontext.UserAuth) (*users.UserInfoWithPermissions, error) {
 	if am.GetCurrentUserInfoFunc != nil {
-		return am.GetCurrentUserInfoFunc(ctx, accountID, userID)
+		return am.GetCurrentUserInfoFunc(ctx, userAuth)
 	}
 	return nil, status.Errorf(codes.Unimplemented, "method GetCurrentUserInfo is not implemented")
 }
diff --git a/management/server/nameserver.go b/management/server/nameserver.go
index 797d7c11..18339335 100644
--- a/management/server/nameserver.go
+++ b/management/server/nameserver.go
@@ -18,7 +18,9 @@ import (
 	"github.com/netbirdio/netbird/management/server/types"
 )
 
-const domainPattern = `^(?i)[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,}$`
+const domainPattern = `^(?i)[a-z0-9]+([\-\.]{1}[a-z0-9]+)*[*.a-z]{1,}$`
+
+var invalidDomainName = errors.New("invalid domain name")
 
 // GetNameServerGroup gets a nameserver group object from account and nameserver group IDs
 func (am *DefaultAccountManager) GetNameServerGroup(ctx context.Context, accountID, userID, nsGroupID string) (*nbdns.NameServerGroup, error) {
@@ -319,13 +321,9 @@ func validateDomain(domain string) error {
 		return errors.New("domain should consists of only letters, numbers, and hyphens with no leading, trailing hyphens, or spaces")
 	}
 
-	labels, valid := dns.IsDomainName(domain)
+	_, valid := dns.IsDomainName(domain)
 	if !valid {
-		return errors.New("invalid domain name")
-	}
-
-	if labels < 2 {
-		return errors.New("domain should consists of a minimum of two labels")
+		return invalidDomainName
 	}
 
 	return nil
diff --git a/management/server/networks/resources/types/resource.go b/management/server/networks/resources/types/resource.go
index ecac0a72..04c63608 100644
--- a/management/server/networks/resources/types/resource.go
+++ b/management/server/networks/resources/types/resource.go
@@ -30,7 +30,7 @@ func (p NetworkResourceType) String() string {
 }
 
 type NetworkResource struct {
-	ID          string `gorm:"index"`
+	ID          string `gorm:"primaryKey"`
 	NetworkID   string `gorm:"index"`
 	AccountID   string `gorm:"index"`
 	Name        string
diff --git a/management/server/networks/routers/types/router.go b/management/server/networks/routers/types/router.go
index 5158ebb1..71465868 100644
--- a/management/server/networks/routers/types/router.go
+++ b/management/server/networks/routers/types/router.go
@@ -10,7 +10,7 @@ import (
 )
 
 type NetworkRouter struct {
-	ID         string `gorm:"index"`
+	ID         string `gorm:"primaryKey"`
 	NetworkID  string `gorm:"index"`
 	AccountID  string `gorm:"index"`
 	Peer       string
diff --git a/management/server/networks/types/network.go b/management/server/networks/types/network.go
index a4ba7b82..d1c7f2b3 100644
--- a/management/server/networks/types/network.go
+++ b/management/server/networks/types/network.go
@@ -7,7 +7,7 @@ import (
 )
 
 type Network struct {
-	ID          string `gorm:"index"`
+	ID          string `gorm:"primaryKey"`
 	AccountID   string `gorm:"index"`
 	Name        string
 	Description string
diff --git a/management/server/peer.go b/management/server/peer.go
index 27825a14..4a468a6c 100644
--- a/management/server/peer.go
+++ b/management/server/peer.go
@@ -17,6 +17,7 @@ import (
 
 	"github.com/netbirdio/netbird/management/domain"
 	"github.com/netbirdio/netbird/management/server/geolocation"
+	routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
 	"github.com/netbirdio/netbird/management/server/permissions/modules"
 	"github.com/netbirdio/netbird/management/server/permissions/operations"
 
@@ -49,20 +50,9 @@ func (am *DefaultAccountManager) GetPeers(ctx context.Context, accountID, userID
 		return nil, err
 	}
 
-	peers := make([]*nbpeer.Peer, 0)
-	peersMap := make(map[string]*nbpeer.Peer)
-
-	for _, peer := range accountPeers {
-		if user.IsRegularUser() && user.Id != peer.UserID {
-			// only display peers that belong to the current user if the current user is not an admin
-			continue
-		}
-		peers = append(peers, peer)
-		peersMap[peer.ID] = peer
-	}
-
+	// @note if the user has permission to read peers it shows all account peers
 	if allowed {
-		return peers, nil
+		return accountPeers, nil
 	}
 
 	settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
@@ -70,10 +60,22 @@ func (am *DefaultAccountManager) GetPeers(ctx context.Context, accountID, userID
 		return nil, fmt.Errorf("failed to get account settings: %w", err)
 	}
 
-	if settings.RegularUsersViewBlocked {
+	if user.IsRestrictable() && settings.RegularUsersViewBlocked {
 		return []*nbpeer.Peer{}, nil
 	}
 
+	// @note if it does not have permission read peers then only display it's own peers
+	peers := make([]*nbpeer.Peer, 0)
+	peersMap := make(map[string]*nbpeer.Peer)
+
+	for _, peer := range accountPeers {
+		if user.Id != peer.UserID {
+			continue
+		}
+		peers = append(peers, peer)
+		peersMap[peer.ID] = peer
+	}
+
 	return am.getUserAccessiblePeers(ctx, accountID, peersMap, peers)
 }
 
@@ -206,6 +208,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 +226,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 +281,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 +293,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 +305,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)
@@ -348,7 +353,7 @@ func (am *DefaultAccountManager) DeletePeer(ctx context.Context, accountID, peer
 			return err
 		}
 
-		if err = am.validatePeerDelete(ctx, accountID, peerID); err != nil {
+		if err = am.validatePeerDelete(ctx, transaction, accountID, peerID); err != nil {
 			return err
 		}
 
@@ -413,7 +418,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 {
@@ -552,6 +557,10 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s
 			return fmt.Errorf("failed to get free IP: %w", err)
 		}
 
+		if err := domain.ValidateDomainsList(peer.ExtraDNSLabels); err != nil {
+			return status.Errorf(status.InvalidArgument, "invalid extra DNS labels: %v", err)
+		}
+
 		registrationTime := time.Now().UTC()
 		newPeer = &nbpeer.Peer{
 			ID:                          xid.New().String(),
@@ -574,8 +583,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 +605,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)
@@ -762,10 +772,11 @@ func (am *DefaultAccountManager) handlePeerLoginNotFound(ctx context.Context, lo
 		// we couldn't find this peer by its public key which can mean that peer hasn't been registered yet.
 		// Try registering it.
 		newPeer := &nbpeer.Peer{
-			Key:      login.WireGuardPubKey,
-			Meta:     login.Meta,
-			SSHKey:   login.SSHKey,
-			Location: nbpeer.Location{ConnectionIP: login.ConnectionIP},
+			Key:            login.WireGuardPubKey,
+			Meta:           login.Meta,
+			SSHKey:         login.SSHKey,
+			Location:       nbpeer.Location{ConnectionIP: login.ConnectionIP},
+			ExtraDNSLabels: login.ExtraDNSLabels,
 		}
 
 		return am.AddPeer(ctx, login.SetupKey, login.UserID, newPeer)
@@ -870,16 +881,6 @@ func (am *DefaultAccountManager) LoginPeer(ctx context.Context, login types.Peer
 			return status.Errorf(status.PreconditionFailed, "couldn't login peer: setup key doesn't allow extra DNS labels")
 		}
 
-		extraLabels, err := domain.ValidateDomainsStrSlice(login.ExtraDNSLabels)
-		if err != nil {
-			return status.Errorf(status.InvalidArgument, "invalid extra DNS labels: %v", err)
-		}
-
-		if !slices.Equal(peer.ExtraDNSLabels, extraLabels) {
-			peer.ExtraDNSLabels = extraLabels
-			shouldStorePeer = true
-		}
-
 		if shouldStorePeer {
 			if err = transaction.SavePeer(ctx, store.LockingStrengthUpdate, accountID, peer); err != nil {
 				return err
@@ -1024,7 +1025,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 +1061,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 +1180,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 +1222,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, extraSetting)
 			am.peersUpdateManager.SendUpdate(ctx, p.ID, &UpdateMessage{Update: update, NetworkMap: remotePeerNetworkMap})
 		}(peer)
 	}
@@ -1270,7 +1277,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 +1307,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, extraSettings)
 	am.peersUpdateManager.SendUpdate(ctx, peer.ID, &UpdateMessage{Update: update, NetworkMap: remotePeerNetworkMap})
 }
 
@@ -1484,6 +1492,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 +1528,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))
 		})
 	}
 
@@ -1530,7 +1544,7 @@ func ConvertSliceToMap(existingLabels []string) map[string]struct{} {
 }
 
 // validatePeerDelete checks if the peer can be deleted.
-func (am *DefaultAccountManager) validatePeerDelete(ctx context.Context, accountId, peerId string) error {
+func (am *DefaultAccountManager) validatePeerDelete(ctx context.Context, transaction store.Store, accountId, peerId string) error {
 	linkedInIngressPorts, err := am.proxyController.IsPeerInIngressPorts(ctx, accountId, peerId)
 	if err != nil {
 		return err
@@ -1540,5 +1554,27 @@ func (am *DefaultAccountManager) validatePeerDelete(ctx context.Context, account
 		return status.Errorf(status.PreconditionFailed, "peer is linked to ingress ports: %s", peerId)
 	}
 
+	linked, router := isPeerLinkedToNetworkRouter(ctx, transaction, accountId, peerId)
+	if linked {
+		return status.Errorf(status.PreconditionFailed, "peer is linked to a network router: %s", router.ID)
+	}
+
 	return nil
 }
+
+// isPeerLinkedToNetworkRouter checks if a peer is linked to any network router in the account.
+func isPeerLinkedToNetworkRouter(ctx context.Context, transaction store.Store, accountID string, peerID string) (bool, *routerTypes.NetworkRouter) {
+	routers, err := transaction.GetNetworkRoutersByAccountID(ctx, store.LockingStrengthShare, accountID)
+	if err != nil {
+		log.WithContext(ctx).Errorf("error retrieving network routers while checking peer linkage: %v", err)
+		return false, nil
+	}
+
+	for _, router := range routers {
+		if router.Peer == peerID {
+			return true, router
+		}
+	}
+
+	return false, nil
+}
diff --git a/management/server/permissions/manager.go b/management/server/permissions/manager.go
index 50a44eb0..ebbce5d4 100644
--- a/management/server/permissions/manager.go
+++ b/management/server/permissions/manager.go
@@ -20,6 +20,8 @@ type Manager interface {
 	ValidateUserPermissions(ctx context.Context, accountID, userID string, module modules.Module, operation operations.Operation) (bool, error)
 	ValidateRoleModuleAccess(ctx context.Context, accountID string, role roles.RolePermissions, module modules.Module, operation operations.Operation) bool
 	ValidateAccountAccess(ctx context.Context, accountID string, user *types.User, allowOwnerAndAdmin bool) error
+
+	GetPermissionsByRole(ctx context.Context, role types.UserRole) (roles.Permissions, error)
 }
 
 type managerImpl struct {
@@ -96,3 +98,22 @@ func (m *managerImpl) ValidateAccountAccess(ctx context.Context, accountID strin
 	}
 	return nil
 }
+
+func (m *managerImpl) GetPermissionsByRole(ctx context.Context, role types.UserRole) (roles.Permissions, error) {
+	roleMap, ok := roles.RolesMap[role]
+	if !ok {
+		return roles.Permissions{}, status.NewUserRoleNotFoundError(string(role))
+	}
+
+	permissions := roles.Permissions{}
+
+	for k := range modules.All {
+		if rolePermissions, ok := roleMap.Permissions[k]; ok {
+			permissions[k] = rolePermissions
+			continue
+		}
+		permissions[k] = roleMap.AutoAllowNew
+	}
+
+	return permissions, nil
+}
diff --git a/management/server/permissions/manager_mock.go b/management/server/permissions/manager_mock.go
index 266a2427..fa115d62 100644
--- a/management/server/permissions/manager_mock.go
+++ b/management/server/permissions/manager_mock.go
@@ -38,6 +38,21 @@ func (m *MockManager) EXPECT() *MockManagerMockRecorder {
 	return m.recorder
 }
 
+// GetPermissionsByRole mocks base method.
+func (m *MockManager) GetPermissionsByRole(ctx context.Context, role types.UserRole) (roles.Permissions, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetPermissionsByRole", ctx, role)
+	ret0, _ := ret[0].(roles.Permissions)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetPermissionsByRole indicates an expected call of GetPermissionsByRole.
+func (mr *MockManagerMockRecorder) GetPermissionsByRole(ctx, role interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPermissionsByRole", reflect.TypeOf((*MockManager)(nil).GetPermissionsByRole), ctx, role)
+}
+
 // ValidateAccountAccess mocks base method.
 func (m *MockManager) ValidateAccountAccess(ctx context.Context, accountID string, user *types.User, allowOwnerAndAdmin bool) error {
 	m.ctrl.T.Helper()
diff --git a/management/server/permissions/modules/module.go b/management/server/permissions/modules/module.go
index 4c42b619..3d021a23 100644
--- a/management/server/permissions/modules/module.go
+++ b/management/server/permissions/modules/module.go
@@ -17,3 +17,19 @@ const (
 	SetupKeys   Module = "setup_keys"
 	Pats        Module = "pats"
 )
+
+var All = map[Module]struct{}{
+	Networks:    {},
+	Peers:       {},
+	Groups:      {},
+	Settings:    {},
+	Accounts:    {},
+	Dns:         {},
+	Nameservers: {},
+	Events:      {},
+	Policies:    {},
+	Routes:      {},
+	Users:       {},
+	SetupKeys:   {},
+	Pats:        {},
+}
diff --git a/management/server/permissions/roles/auditor.go b/management/server/permissions/roles/auditor.go
new file mode 100644
index 00000000..33d8651f
--- /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 00000000..e95d5838
--- /dev/null
+++ b/management/server/permissions/roles/network_admin.go
@@ -0,0 +1,97 @@
+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: true,
+			operations.Update: true,
+			operations.Delete: true,
+		},
+		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,
+		},
+		modules.Peers: {
+			operations.Read:   true,
+			operations.Create: false,
+			operations.Update: false,
+			operations.Delete: false,
+		},
+	},
+}
diff --git a/management/server/permissions/roles/role_permissions.go b/management/server/permissions/roles/role_permissions.go
index dda7e6b9..754e568f 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/route.go b/management/server/route.go
index 8b91e127..02755a70 100644
--- a/management/server/route.go
+++ b/management/server/route.go
@@ -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/store/sql_store.go b/management/server/store/sql_store.go
index aacb56ab..6c3104ef 100644
--- a/management/server/store/sql_store.go
+++ b/management/server/store/sql_store.go
@@ -66,7 +66,7 @@ type installation struct {
 type migrationFunc func(*gorm.DB) error
 
 // NewSqlStore creates a new SqlStore instance.
-func NewSqlStore(ctx context.Context, db *gorm.DB, storeEngine types.Engine, metrics telemetry.AppMetrics) (*SqlStore, error) {
+func NewSqlStore(ctx context.Context, db *gorm.DB, storeEngine types.Engine, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {
 	sql, err := db.DB()
 	if err != nil {
 		return nil, err
@@ -88,6 +88,11 @@ func NewSqlStore(ctx context.Context, db *gorm.DB, storeEngine types.Engine, met
 
 	log.WithContext(ctx).Infof("Set max open db connections to %d", conns)
 
+	if skipMigration {
+		log.WithContext(ctx).Infof("skipping migration")
+		return &SqlStore{db: db, storeEngine: storeEngine, metrics: metrics, installationPK: 1}, nil
+	}
+
 	if err := migrate(ctx, db); err != nil {
 		return nil, fmt.Errorf("migrate: %w", err)
 	}
@@ -443,12 +448,20 @@ func (s *SqlStore) SaveUser(ctx context.Context, lockStrength LockingStrength, u
 }
 
 // SaveGroups saves the given list of groups to the database.
-func (s *SqlStore) SaveGroups(ctx context.Context, lockStrength LockingStrength, groups []*types.Group) error {
+func (s *SqlStore) SaveGroups(ctx context.Context, lockStrength LockingStrength, accountID string, groups []*types.Group) error {
 	if len(groups) == 0 {
 		return nil
 	}
 
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}, clause.OnConflict{UpdateAll: true}).Create(&groups)
+	result := s.db.
+		Clauses(
+			clause.Locking{Strength: string(lockStrength)},
+			clause.OnConflict{
+				Where:     clause.Where{Exprs: []clause.Expression{clause.Eq{Column: "groups.account_id", Value: accountID}}},
+				UpdateAll: true,
+			},
+		).
+		Create(&groups)
 	if result.Error != nil {
 		return status.Errorf(status.Internal, "failed to save groups to store: %v", result.Error)
 	}
@@ -476,8 +489,13 @@ func (s *SqlStore) GetAccountByPrivateDomain(ctx context.Context, domain string)
 }
 
 func (s *SqlStore) GetAccountIDByPrivateDomain(ctx context.Context, lockStrength LockingStrength, domain string) (string, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var accountID string
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&types.Account{}).Select("id").
+	result := tx.Model(&types.Account{}).Select("id").
 		Where("domain = ? and is_domain_primary_account = ? and domain_category = ?",
 			strings.ToLower(domain), true, types.PrivateCategory,
 		).First(&accountID)
@@ -525,8 +543,13 @@ func (s *SqlStore) GetTokenIDByHashedToken(ctx context.Context, hashedToken stri
 }
 
 func (s *SqlStore) GetUserByPATID(ctx context.Context, lockStrength LockingStrength, patID string) (*types.User, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var user types.User
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).
+	result := tx.
 		Joins("JOIN personal_access_tokens ON personal_access_tokens.user_id = users.id").
 		Where("personal_access_tokens.id = ?", patID).First(&user)
 	if result.Error != nil {
@@ -541,8 +564,13 @@ func (s *SqlStore) GetUserByPATID(ctx context.Context, lockStrength LockingStren
 }
 
 func (s *SqlStore) GetUserByUserID(ctx context.Context, lockStrength LockingStrength, userID string) (*types.User, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var user types.User
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).First(&user, idQueryCondition, userID)
+	result := tx.First(&user, idQueryCondition, userID)
 	if result.Error != nil {
 		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
 			return nil, status.NewUserNotFoundError(userID)
@@ -573,8 +601,13 @@ func (s *SqlStore) DeleteUser(ctx context.Context, lockStrength LockingStrength,
 }
 
 func (s *SqlStore) GetAccountUsers(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.User, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var users []*types.User
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Find(&users, accountIDCondition, accountID)
+	result := tx.Find(&users, accountIDCondition, accountID)
 	if result.Error != nil {
 		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
 			return nil, status.Errorf(status.NotFound, "accountID not found: index lookup failed")
@@ -587,8 +620,13 @@ func (s *SqlStore) GetAccountUsers(ctx context.Context, lockStrength LockingStre
 }
 
 func (s *SqlStore) GetAccountOwner(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.User, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var user types.User
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).First(&user, "account_id = ? AND role = ?", accountID, types.UserRoleOwner)
+	result := tx.First(&user, "account_id = ? AND role = ?", accountID, types.UserRoleOwner)
 	if result.Error != nil {
 		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
 			return nil, status.Errorf(status.NotFound, "account owner not found: index lookup failed")
@@ -600,8 +638,13 @@ func (s *SqlStore) GetAccountOwner(ctx context.Context, lockStrength LockingStre
 }
 
 func (s *SqlStore) GetAccountGroups(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.Group, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var groups []*types.Group
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Find(&groups, accountIDCondition, accountID)
+	result := tx.Find(&groups, accountIDCondition, accountID)
 	if result.Error != nil {
 		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
 			return nil, status.Errorf(status.NotFound, "accountID not found: index lookup failed")
@@ -614,11 +657,16 @@ func (s *SqlStore) GetAccountGroups(ctx context.Context, lockStrength LockingStr
 }
 
 func (s *SqlStore) GetResourceGroups(ctx context.Context, lockStrength LockingStrength, accountID, resourceID string) ([]*types.Group, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var groups []*types.Group
 
 	likePattern := `%"ID":"` + resourceID + `"%`
 
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).
+	result := tx.
 		Where("resources LIKE ?", likePattern).
 		Find(&groups)
 
@@ -658,6 +706,26 @@ 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) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
+	var accountMeta types.AccountMeta
+	result := tx.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 +853,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").Order("created_at desc").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
@@ -800,8 +881,13 @@ func (s *SqlStore) GetAccountIDByPeerPubKey(ctx context.Context, peerKey string)
 }
 
 func (s *SqlStore) GetAccountIDByUserID(ctx context.Context, lockStrength LockingStrength, userID string) (string, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var accountID string
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&types.User{}).
+	result := tx.Model(&types.User{}).
 		Select("account_id").Where(idQueryCondition, userID).First(&accountID)
 	if result.Error != nil {
 		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
@@ -814,8 +900,13 @@ func (s *SqlStore) GetAccountIDByUserID(ctx context.Context, lockStrength Lockin
 }
 
 func (s *SqlStore) GetAccountIDByPeerID(ctx context.Context, lockStrength LockingStrength, peerID string) (string, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var accountID string
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&nbpeer.Peer{}).
+	result := tx.Model(&nbpeer.Peer{}).
 		Select("account_id").Where(idQueryCondition, peerID).First(&accountID)
 	if result.Error != nil {
 		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
@@ -846,10 +937,15 @@ func (s *SqlStore) GetAccountIDBySetupKey(ctx context.Context, setupKey string)
 }
 
 func (s *SqlStore) GetTakenIPs(ctx context.Context, lockStrength LockingStrength, accountID string) ([]net.IP, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var ipJSONStrings []string
 
 	// Fetch the IP addresses as JSON strings
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&nbpeer.Peer{}).
+	result := tx.Model(&nbpeer.Peer{}).
 		Where("account_id = ?", accountID).
 		Pluck("ip", &ipJSONStrings)
 	if result.Error != nil {
@@ -873,8 +969,13 @@ func (s *SqlStore) GetTakenIPs(ctx context.Context, lockStrength LockingStrength
 }
 
 func (s *SqlStore) GetPeerLabelsInAccount(ctx context.Context, lockStrength LockingStrength, accountID string) ([]string, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var labels []string
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&nbpeer.Peer{}).
+	result := tx.Model(&nbpeer.Peer{}).
 		Where("account_id = ?", accountID).
 		Pluck("dns_label", &labels)
 
@@ -890,8 +991,13 @@ func (s *SqlStore) GetPeerLabelsInAccount(ctx context.Context, lockStrength Lock
 }
 
 func (s *SqlStore) GetAccountNetwork(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.Network, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var accountNetwork types.AccountNetwork
-	if err := s.db.Model(&types.Account{}).Where(idQueryCondition, accountID).First(&accountNetwork).Error; err != nil {
+	if err := tx.Model(&types.Account{}).Where(idQueryCondition, accountID).First(&accountNetwork).Error; err != nil {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
 			return nil, status.NewAccountNotFoundError(accountID)
 		}
@@ -901,8 +1007,13 @@ func (s *SqlStore) GetAccountNetwork(ctx context.Context, lockStrength LockingSt
 }
 
 func (s *SqlStore) GetPeerByPeerPubKey(ctx context.Context, lockStrength LockingStrength, peerKey string) (*nbpeer.Peer, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var peer nbpeer.Peer
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).First(&peer, GetKeyQueryCondition(s), peerKey)
+	result := tx.First(&peer, GetKeyQueryCondition(s), peerKey)
 
 	if result.Error != nil {
 		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
@@ -915,8 +1026,13 @@ func (s *SqlStore) GetPeerByPeerPubKey(ctx context.Context, lockStrength Locking
 }
 
 func (s *SqlStore) GetAccountSettings(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.Settings, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var accountSettings types.AccountSettings
-	if err := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&types.Account{}).Where(idQueryCondition, accountID).First(&accountSettings).Error; err != nil {
+	if err := tx.Model(&types.Account{}).Where(idQueryCondition, accountID).First(&accountSettings).Error; err != nil {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
 			return nil, status.Errorf(status.NotFound, "settings not found")
 		}
@@ -926,8 +1042,13 @@ func (s *SqlStore) GetAccountSettings(ctx context.Context, lockStrength LockingS
 }
 
 func (s *SqlStore) GetAccountCreatedBy(ctx context.Context, lockStrength LockingStrength, accountID string) (string, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var createdBy string
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&types.Account{}).
+	result := tx.Model(&types.Account{}).
 		Select("created_by").First(&createdBy, idQueryCondition, accountID)
 	if result.Error != nil {
 		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
@@ -988,7 +1109,7 @@ func (s *SqlStore) GetStoreEngine() types.Engine {
 }
 
 // NewSqliteStore creates a new SQLite store.
-func NewSqliteStore(ctx context.Context, dataDir string, metrics telemetry.AppMetrics) (*SqlStore, error) {
+func NewSqliteStore(ctx context.Context, dataDir string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {
 	storeStr := fmt.Sprintf("%s?cache=shared", storeSqliteFileName)
 	if runtime.GOOS == "windows" {
 		// Vo avoid `The process cannot access the file because it is being used by another process` on Windows
@@ -1001,27 +1122,27 @@ func NewSqliteStore(ctx context.Context, dataDir string, metrics telemetry.AppMe
 		return nil, err
 	}
 
-	return NewSqlStore(ctx, db, types.SqliteStoreEngine, metrics)
+	return NewSqlStore(ctx, db, types.SqliteStoreEngine, metrics, skipMigration)
 }
 
 // NewPostgresqlStore creates a new Postgres store.
-func NewPostgresqlStore(ctx context.Context, dsn string, metrics telemetry.AppMetrics) (*SqlStore, error) {
+func NewPostgresqlStore(ctx context.Context, dsn string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {
 	db, err := gorm.Open(postgres.Open(dsn), getGormConfig())
 	if err != nil {
 		return nil, err
 	}
 
-	return NewSqlStore(ctx, db, types.PostgresStoreEngine, metrics)
+	return NewSqlStore(ctx, db, types.PostgresStoreEngine, metrics, skipMigration)
 }
 
 // NewMysqlStore creates a new MySQL store.
-func NewMysqlStore(ctx context.Context, dsn string, metrics telemetry.AppMetrics) (*SqlStore, error) {
+func NewMysqlStore(ctx context.Context, dsn string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {
 	db, err := gorm.Open(mysql.Open(dsn+"?charset=utf8&parseTime=True&loc=Local"), getGormConfig())
 	if err != nil {
 		return nil, err
 	}
 
-	return NewSqlStore(ctx, db, types.MysqlStoreEngine, metrics)
+	return NewSqlStore(ctx, db, types.MysqlStoreEngine, metrics, skipMigration)
 }
 
 func getGormConfig() *gorm.Config {
@@ -1032,26 +1153,26 @@ func getGormConfig() *gorm.Config {
 }
 
 // newPostgresStore initializes a new Postgres store.
-func newPostgresStore(ctx context.Context, metrics telemetry.AppMetrics) (Store, error) {
+func newPostgresStore(ctx context.Context, metrics telemetry.AppMetrics, skipMigration bool) (Store, error) {
 	dsn, ok := os.LookupEnv(postgresDsnEnv)
 	if !ok {
 		return nil, fmt.Errorf("%s is not set", postgresDsnEnv)
 	}
-	return NewPostgresqlStore(ctx, dsn, metrics)
+	return NewPostgresqlStore(ctx, dsn, metrics, skipMigration)
 }
 
 // newMysqlStore initializes a new MySQL store.
-func newMysqlStore(ctx context.Context, metrics telemetry.AppMetrics) (Store, error) {
+func newMysqlStore(ctx context.Context, metrics telemetry.AppMetrics, skipMigration bool) (Store, error) {
 	dsn, ok := os.LookupEnv(mysqlDsnEnv)
 	if !ok {
 		return nil, fmt.Errorf("%s is not set", mysqlDsnEnv)
 	}
-	return NewMysqlStore(ctx, dsn, metrics)
+	return NewMysqlStore(ctx, dsn, metrics, skipMigration)
 }
 
 // NewSqliteStoreFromFileStore restores a store from FileStore and stores SQLite DB in the file located in datadir.
-func NewSqliteStoreFromFileStore(ctx context.Context, fileStore *FileStore, dataDir string, metrics telemetry.AppMetrics) (*SqlStore, error) {
-	store, err := NewSqliteStore(ctx, dataDir, metrics)
+func NewSqliteStoreFromFileStore(ctx context.Context, fileStore *FileStore, dataDir string, metrics telemetry.AppMetrics, skipMigration bool) (*SqlStore, error) {
+	store, err := NewSqliteStore(ctx, dataDir, metrics, skipMigration)
 	if err != nil {
 		return nil, err
 	}
@@ -1080,7 +1201,7 @@ func NewSqliteStoreFromFileStore(ctx context.Context, fileStore *FileStore, data
 
 // NewPostgresqlStoreFromSqlStore restores a store from SqlStore and stores Postgres DB.
 func NewPostgresqlStoreFromSqlStore(ctx context.Context, sqliteStore *SqlStore, dsn string, metrics telemetry.AppMetrics) (*SqlStore, error) {
-	store, err := NewPostgresqlStore(ctx, dsn, metrics)
+	store, err := NewPostgresqlStore(ctx, dsn, metrics, false)
 	if err != nil {
 		return nil, err
 	}
@@ -1102,7 +1223,7 @@ func NewPostgresqlStoreFromSqlStore(ctx context.Context, sqliteStore *SqlStore,
 
 // NewMysqlStoreFromSqlStore restores a store from SqlStore and stores MySQL DB.
 func NewMysqlStoreFromSqlStore(ctx context.Context, sqliteStore *SqlStore, dsn string, metrics telemetry.AppMetrics) (*SqlStore, error) {
-	store, err := NewMysqlStore(ctx, dsn, metrics)
+	store, err := NewMysqlStore(ctx, dsn, metrics, false)
 	if err != nil {
 		return nil, err
 	}
@@ -1123,8 +1244,13 @@ func NewMysqlStoreFromSqlStore(ctx context.Context, sqliteStore *SqlStore, dsn s
 }
 
 func (s *SqlStore) GetSetupKeyBySecret(ctx context.Context, lockStrength LockingStrength, key string) (*types.SetupKey, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var setupKey types.SetupKey
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).
+	result := tx.
 		First(&setupKey, GetKeyQueryCondition(s), key)
 
 	if result.Error != nil {
@@ -1266,8 +1392,13 @@ func (s *SqlStore) RemoveResourceFromGroup(ctx context.Context, accountId string
 
 // GetPeerGroups retrieves all groups assigned to a specific peer in a given account.
 func (s *SqlStore) GetPeerGroups(ctx context.Context, lockStrength LockingStrength, accountId string, peerId string) ([]*types.Group, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var groups []*types.Group
-	query := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).
+	query := tx.
 		Find(&groups, "account_id = ? AND peers LIKE ?", accountId, fmt.Sprintf(`%%"%s"%%`, peerId))
 
 	if query.Error != nil {
@@ -1299,6 +1430,11 @@ func (s *SqlStore) GetAccountPeers(ctx context.Context, lockStrength LockingStre
 
 // GetUserPeers retrieves peers for a user.
 func (s *SqlStore) GetUserPeers(ctx context.Context, lockStrength LockingStrength, accountID, userID string) ([]*nbpeer.Peer, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var peers []*nbpeer.Peer
 
 	// Exclude peers added via setup keys, as they are not user-specific and have an empty user_id.
@@ -1306,7 +1442,7 @@ func (s *SqlStore) GetUserPeers(ctx context.Context, lockStrength LockingStrengt
 		return peers, nil
 	}
 
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).
+	result := tx.
 		Find(&peers, "account_id = ? AND user_id = ?", accountID, userID)
 	if err := result.Error; err != nil {
 		log.WithContext(ctx).Errorf("failed to get peers from the store: %s", err)
@@ -1326,8 +1462,13 @@ func (s *SqlStore) AddPeerToAccount(ctx context.Context, lockStrength LockingStr
 
 // GetPeerByID retrieves a peer by its ID and account ID.
 func (s *SqlStore) GetPeerByID(ctx context.Context, lockStrength LockingStrength, accountID, peerID string) (*nbpeer.Peer, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var peer *nbpeer.Peer
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).
+	result := tx.
 		First(&peer, accountAndIDQueryCondition, accountID, peerID)
 	if result.Error != nil {
 		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
@@ -1341,8 +1482,13 @@ func (s *SqlStore) GetPeerByID(ctx context.Context, lockStrength LockingStrength
 
 // GetPeersByIDs retrieves peers by their IDs and account ID.
 func (s *SqlStore) GetPeersByIDs(ctx context.Context, lockStrength LockingStrength, accountID string, peerIDs []string) (map[string]*nbpeer.Peer, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var peers []*nbpeer.Peer
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Find(&peers, accountAndIDsQueryCondition, accountID, peerIDs)
+	result := tx.Find(&peers, accountAndIDsQueryCondition, accountID, peerIDs)
 	if result.Error != nil {
 		log.WithContext(ctx).Errorf("failed to get peers by ID's from the store: %s", result.Error)
 		return nil, status.Errorf(status.Internal, "failed to get peers by ID's from the store")
@@ -1358,8 +1504,13 @@ func (s *SqlStore) GetPeersByIDs(ctx context.Context, lockStrength LockingStreng
 
 // GetAccountPeersWithExpiration retrieves a list of peers that have login expiration enabled and added by a user.
 func (s *SqlStore) GetAccountPeersWithExpiration(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*nbpeer.Peer, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var peers []*nbpeer.Peer
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).
+	result := tx.
 		Where("login_expiration_enabled = ? AND user_id IS NOT NULL AND user_id != ''", true).
 		Find(&peers, accountIDCondition, accountID)
 	if err := result.Error; err != nil {
@@ -1372,8 +1523,13 @@ func (s *SqlStore) GetAccountPeersWithExpiration(ctx context.Context, lockStreng
 
 // GetAccountPeersWithInactivity retrieves a list of peers that have login expiration enabled and added by a user.
 func (s *SqlStore) GetAccountPeersWithInactivity(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*nbpeer.Peer, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var peers []*nbpeer.Peer
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).
+	result := tx.
 		Where("inactivity_expiration_enabled = ? AND user_id IS NOT NULL AND user_id != ''", true).
 		Find(&peers, accountIDCondition, accountID)
 	if err := result.Error; err != nil {
@@ -1386,8 +1542,13 @@ func (s *SqlStore) GetAccountPeersWithInactivity(ctx context.Context, lockStreng
 
 // GetAllEphemeralPeers retrieves all peers with Ephemeral set to true across all accounts, optimized for batch processing.
 func (s *SqlStore) GetAllEphemeralPeers(ctx context.Context, lockStrength LockingStrength) ([]*nbpeer.Peer, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var allEphemeralPeers, batchPeers []*nbpeer.Peer
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).
+	result := tx.
 		Where("ephemeral = ?", true).
 		FindInBatches(&batchPeers, 1000, func(tx *gorm.DB, batch int) error {
 			allEphemeralPeers = append(allEphemeralPeers, batchPeers...)
@@ -1463,8 +1624,13 @@ func (s *SqlStore) GetDB() *gorm.DB {
 }
 
 func (s *SqlStore) GetAccountDNSSettings(ctx context.Context, lockStrength LockingStrength, accountID string) (*types.DNSSettings, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var accountDNSSettings types.AccountDNSSettings
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&types.Account{}).
+	result := tx.Model(&types.Account{}).
 		First(&accountDNSSettings, idQueryCondition, accountID)
 	if result.Error != nil {
 		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
@@ -1478,8 +1644,13 @@ func (s *SqlStore) GetAccountDNSSettings(ctx context.Context, lockStrength Locki
 
 // AccountExists checks whether an account exists by the given ID.
 func (s *SqlStore) AccountExists(ctx context.Context, lockStrength LockingStrength, id string) (bool, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var accountID string
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&types.Account{}).
+	result := tx.Model(&types.Account{}).
 		Select("id").First(&accountID, idQueryCondition, id)
 	if result.Error != nil {
 		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
@@ -1493,8 +1664,13 @@ func (s *SqlStore) AccountExists(ctx context.Context, lockStrength LockingStreng
 
 // GetAccountDomainAndCategory retrieves the Domain and DomainCategory fields for an account based on the given accountID.
 func (s *SqlStore) GetAccountDomainAndCategory(ctx context.Context, lockStrength LockingStrength, accountID string) (string, string, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var account types.Account
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Model(&types.Account{}).Select("domain", "domain_category").
+	result := tx.Model(&types.Account{}).Select("domain", "domain_category").
 		Where(idQueryCondition, accountID).First(&account)
 	if result.Error != nil {
 		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
@@ -1508,8 +1684,13 @@ func (s *SqlStore) GetAccountDomainAndCategory(ctx context.Context, lockStrength
 
 // GetGroupByID retrieves a group by ID and account ID.
 func (s *SqlStore) GetGroupByID(ctx context.Context, lockStrength LockingStrength, accountID, groupID string) (*types.Group, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var group *types.Group
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).First(&group, accountAndIDQueryCondition, accountID, groupID)
+	result := tx.First(&group, accountAndIDQueryCondition, accountID, groupID)
 	if err := result.Error; err != nil {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
 			return nil, status.NewGroupNotFoundError(groupID)
@@ -1523,11 +1704,16 @@ func (s *SqlStore) GetGroupByID(ctx context.Context, lockStrength LockingStrengt
 
 // GetGroupByName retrieves a group by name and account ID.
 func (s *SqlStore) GetGroupByName(ctx context.Context, lockStrength LockingStrength, accountID, groupName string) (*types.Group, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var group types.Group
 
 	// TODO: This fix is accepted for now, but if we need to handle this more frequently
 	// we may need to reconsider changing the types.
-	query := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Preload(clause.Associations)
+	query := tx.Preload(clause.Associations)
 
 	switch s.storeEngine {
 	case types.PostgresStoreEngine:
@@ -1551,8 +1737,13 @@ func (s *SqlStore) GetGroupByName(ctx context.Context, lockStrength LockingStren
 
 // GetGroupsByIDs retrieves groups by their IDs and account ID.
 func (s *SqlStore) GetGroupsByIDs(ctx context.Context, lockStrength LockingStrength, accountID string, groupIDs []string) (map[string]*types.Group, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var groups []*types.Group
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Find(&groups, accountAndIDsQueryCondition, accountID, groupIDs)
+	result := tx.Find(&groups, accountAndIDsQueryCondition, accountID, groupIDs)
 	if result.Error != nil {
 		log.WithContext(ctx).Errorf("failed to get groups by ID's from store: %s", result.Error)
 		return nil, status.Errorf(status.Internal, "failed to get groups by ID's from store")
@@ -1606,8 +1797,13 @@ func (s *SqlStore) DeleteGroups(ctx context.Context, strength LockingStrength, a
 
 // GetAccountPolicies retrieves policies for an account.
 func (s *SqlStore) GetAccountPolicies(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.Policy, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var policies []*types.Policy
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).
+	result := tx.
 		Preload(clause.Associations).Find(&policies, accountIDCondition, accountID)
 	if err := result.Error; err != nil {
 		log.WithContext(ctx).Errorf("failed to get policies from the store: %s", result.Error)
@@ -1619,8 +1815,14 @@ func (s *SqlStore) GetAccountPolicies(ctx context.Context, lockStrength LockingS
 
 // GetPolicyByID retrieves a policy by its ID and account ID.
 func (s *SqlStore) GetPolicyByID(ctx context.Context, lockStrength LockingStrength, accountID, policyID string) (*types.Policy, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var policy *types.Policy
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Preload(clause.Associations).
+
+	result := tx.Preload(clause.Associations).
 		First(&policy, accountAndIDQueryCondition, accountID, policyID)
 	if err := result.Error; err != nil {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -1655,24 +1857,37 @@ func (s *SqlStore) SavePolicy(ctx context.Context, lockStrength LockingStrength,
 }
 
 func (s *SqlStore) DeletePolicy(ctx context.Context, lockStrength LockingStrength, accountID, policyID string) error {
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).
-		Delete(&types.Policy{}, accountAndIDQueryCondition, accountID, policyID)
-	if err := result.Error; err != nil {
-		log.WithContext(ctx).Errorf("failed to delete policy from store: %s", err)
-		return status.Errorf(status.Internal, "failed to delete policy from store")
-	}
+	return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+		if err := tx.Where("policy_id = ?", policyID).Delete(&types.PolicyRule{}).Error; err != nil {
+			return fmt.Errorf("delete policy rules: %w", err)
+		}
 
-	if result.RowsAffected == 0 {
-		return status.NewPolicyNotFoundError(policyID)
-	}
+		result := tx.Clauses(clause.Locking{Strength: string(lockStrength)}).
+			Where(accountAndIDQueryCondition, accountID, policyID).
+			Delete(&types.Policy{})
 
-	return nil
+		if err := result.Error; err != nil {
+			log.WithContext(ctx).Errorf("failed to delete policy from store: %s", err)
+			return status.Errorf(status.Internal, "failed to delete policy from store")
+		}
+
+		if result.RowsAffected == 0 {
+			return status.NewPolicyNotFoundError(policyID)
+		}
+
+		return nil
+	})
 }
 
 // GetAccountPostureChecks retrieves posture checks for an account.
 func (s *SqlStore) GetAccountPostureChecks(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*posture.Checks, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var postureChecks []*posture.Checks
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Find(&postureChecks, accountIDCondition, accountID)
+	result := tx.Find(&postureChecks, accountIDCondition, accountID)
 	if result.Error != nil {
 		log.WithContext(ctx).Errorf("failed to get posture checks from store: %s", result.Error)
 		return nil, status.Errorf(status.Internal, "failed to get posture checks from store")
@@ -1683,8 +1898,13 @@ func (s *SqlStore) GetAccountPostureChecks(ctx context.Context, lockStrength Loc
 
 // GetPostureChecksByID retrieves posture checks by their ID and account ID.
 func (s *SqlStore) GetPostureChecksByID(ctx context.Context, lockStrength LockingStrength, accountID, postureChecksID string) (*posture.Checks, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var postureCheck *posture.Checks
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).
+	result := tx.
 		First(&postureCheck, accountAndIDQueryCondition, accountID, postureChecksID)
 	if result.Error != nil {
 		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
@@ -1699,8 +1919,13 @@ func (s *SqlStore) GetPostureChecksByID(ctx context.Context, lockStrength Lockin
 
 // GetPostureChecksByIDs retrieves posture checks by their IDs and account ID.
 func (s *SqlStore) GetPostureChecksByIDs(ctx context.Context, lockStrength LockingStrength, accountID string, postureChecksIDs []string) (map[string]*posture.Checks, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var postureChecks []*posture.Checks
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Find(&postureChecks, accountAndIDsQueryCondition, accountID, postureChecksIDs)
+	result := tx.Find(&postureChecks, accountAndIDsQueryCondition, accountID, postureChecksIDs)
 	if result.Error != nil {
 		log.WithContext(ctx).Errorf("failed to get posture checks by ID's from store: %s", result.Error)
 		return nil, status.Errorf(status.Internal, "failed to get posture checks by ID's from store")
@@ -1753,8 +1978,13 @@ func (s *SqlStore) GetRouteByID(ctx context.Context, lockStrength LockingStrengt
 
 // GetAccountSetupKeys retrieves setup keys for an account.
 func (s *SqlStore) GetAccountSetupKeys(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*types.SetupKey, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var setupKeys []*types.SetupKey
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).
+	result := tx.
 		Find(&setupKeys, accountIDCondition, accountID)
 	if err := result.Error; err != nil {
 		log.WithContext(ctx).Errorf("failed to get setup keys from the store: %s", err)
@@ -1766,8 +1996,13 @@ func (s *SqlStore) GetAccountSetupKeys(ctx context.Context, lockStrength Locking
 
 // GetSetupKeyByID retrieves a setup key by its ID and account ID.
 func (s *SqlStore) GetSetupKeyByID(ctx context.Context, lockStrength LockingStrength, accountID, setupKeyID string) (*types.SetupKey, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var setupKey *types.SetupKey
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).
+	result := tx.Clauses(clause.Locking{Strength: string(lockStrength)}).
 		First(&setupKey, accountAndIDQueryCondition, accountID, setupKeyID)
 	if err := result.Error; err != nil {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -1808,8 +2043,13 @@ func (s *SqlStore) DeleteSetupKey(ctx context.Context, lockStrength LockingStren
 
 // GetAccountNameServerGroups retrieves name server groups for an account.
 func (s *SqlStore) GetAccountNameServerGroups(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*nbdns.NameServerGroup, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var nsGroups []*nbdns.NameServerGroup
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Find(&nsGroups, accountIDCondition, accountID)
+	result := tx.Find(&nsGroups, accountIDCondition, accountID)
 	if err := result.Error; err != nil {
 		log.WithContext(ctx).Errorf("failed to get name server groups from the store: %s", err)
 		return nil, status.Errorf(status.Internal, "failed to get name server groups from store")
@@ -1820,8 +2060,13 @@ func (s *SqlStore) GetAccountNameServerGroups(ctx context.Context, lockStrength
 
 // GetNameServerGroupByID retrieves a name server group by its ID and account ID.
 func (s *SqlStore) GetNameServerGroupByID(ctx context.Context, lockStrength LockingStrength, accountID, nsGroupID string) (*nbdns.NameServerGroup, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var nsGroup *nbdns.NameServerGroup
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).
+	result := tx.
 		First(&nsGroup, accountAndIDQueryCondition, accountID, nsGroupID)
 	if err := result.Error; err != nil {
 		if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -1861,9 +2106,14 @@ func (s *SqlStore) DeleteNameServerGroup(ctx context.Context, lockStrength Locki
 
 // getRecords retrieves records from the database based on the account ID.
 func getRecords[T any](db *gorm.DB, lockStrength LockingStrength, accountID string) ([]T, error) {
+	tx := db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var record []T
 
-	result := db.Clauses(clause.Locking{Strength: string(lockStrength)}).Find(&record, accountIDCondition, accountID)
+	result := tx.Find(&record, accountIDCondition, accountID)
 	if err := result.Error; err != nil {
 		parts := strings.Split(fmt.Sprintf("%T", record), ".")
 		recordType := parts[len(parts)-1]
@@ -1876,9 +2126,14 @@ func getRecords[T any](db *gorm.DB, lockStrength LockingStrength, accountID stri
 
 // getRecordByID retrieves a record by its ID and account ID from the database.
 func getRecordByID[T any](db *gorm.DB, lockStrength LockingStrength, recordID, accountID string) (*T, error) {
+	tx := db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var record T
 
-	result := db.Clauses(clause.Locking{Strength: string(lockStrength)}).
+	result := tx.Clauses(clause.Locking{Strength: string(lockStrength)}).
 		First(&record, accountAndIDQueryCondition, accountID, recordID)
 	if err := result.Error; err != nil {
 		parts := strings.Split(fmt.Sprintf("%T", record), ".")
@@ -1909,8 +2164,13 @@ func (s *SqlStore) SaveDNSSettings(ctx context.Context, lockStrength LockingStre
 }
 
 func (s *SqlStore) GetAccountNetworks(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*networkTypes.Network, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var networks []*networkTypes.Network
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Find(&networks, accountIDCondition, accountID)
+	result := tx.Find(&networks, accountIDCondition, accountID)
 	if result.Error != nil {
 		log.WithContext(ctx).Errorf("failed to get networks from the store: %s", result.Error)
 		return nil, status.Errorf(status.Internal, "failed to get networks from store")
@@ -1920,8 +2180,13 @@ func (s *SqlStore) GetAccountNetworks(ctx context.Context, lockStrength LockingS
 }
 
 func (s *SqlStore) GetNetworkByID(ctx context.Context, lockStrength LockingStrength, accountID, networkID string) (*networkTypes.Network, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var network *networkTypes.Network
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).
+	result := tx.
 		First(&network, accountAndIDQueryCondition, accountID, networkID)
 	if result.Error != nil {
 		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
@@ -1961,8 +2226,13 @@ func (s *SqlStore) DeleteNetwork(ctx context.Context, lockStrength LockingStreng
 }
 
 func (s *SqlStore) GetNetworkRoutersByNetID(ctx context.Context, lockStrength LockingStrength, accountID, netID string) ([]*routerTypes.NetworkRouter, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var netRouters []*routerTypes.NetworkRouter
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).
+	result := tx.
 		Find(&netRouters, "account_id = ? AND network_id = ?", accountID, netID)
 	if result.Error != nil {
 		log.WithContext(ctx).Errorf("failed to get network routers from store: %v", result.Error)
@@ -1973,8 +2243,13 @@ func (s *SqlStore) GetNetworkRoutersByNetID(ctx context.Context, lockStrength Lo
 }
 
 func (s *SqlStore) GetNetworkRoutersByAccountID(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*routerTypes.NetworkRouter, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var netRouters []*routerTypes.NetworkRouter
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).
+	result := tx.
 		Find(&netRouters, accountIDCondition, accountID)
 	if result.Error != nil {
 		log.WithContext(ctx).Errorf("failed to get network routers from store: %v", result.Error)
@@ -1985,8 +2260,13 @@ func (s *SqlStore) GetNetworkRoutersByAccountID(ctx context.Context, lockStrengt
 }
 
 func (s *SqlStore) GetNetworkRouterByID(ctx context.Context, lockStrength LockingStrength, accountID, routerID string) (*routerTypes.NetworkRouter, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var netRouter *routerTypes.NetworkRouter
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).
+	result := tx.
 		First(&netRouter, accountAndIDQueryCondition, accountID, routerID)
 	if result.Error != nil {
 		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
@@ -2025,8 +2305,13 @@ func (s *SqlStore) DeleteNetworkRouter(ctx context.Context, lockStrength Locking
 }
 
 func (s *SqlStore) GetNetworkResourcesByNetID(ctx context.Context, lockStrength LockingStrength, accountID, networkID string) ([]*resourceTypes.NetworkResource, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var netResources []*resourceTypes.NetworkResource
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).
+	result := tx.
 		Find(&netResources, "account_id = ? AND network_id = ?", accountID, networkID)
 	if result.Error != nil {
 		log.WithContext(ctx).Errorf("failed to get network resources from store: %v", result.Error)
@@ -2037,8 +2322,13 @@ func (s *SqlStore) GetNetworkResourcesByNetID(ctx context.Context, lockStrength
 }
 
 func (s *SqlStore) GetNetworkResourcesByAccountID(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*resourceTypes.NetworkResource, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var netResources []*resourceTypes.NetworkResource
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).
+	result := tx.
 		Find(&netResources, accountIDCondition, accountID)
 	if result.Error != nil {
 		log.WithContext(ctx).Errorf("failed to get network resources from store: %v", result.Error)
@@ -2049,8 +2339,13 @@ func (s *SqlStore) GetNetworkResourcesByAccountID(ctx context.Context, lockStren
 }
 
 func (s *SqlStore) GetNetworkResourceByID(ctx context.Context, lockStrength LockingStrength, accountID, resourceID string) (*resourceTypes.NetworkResource, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var netResources *resourceTypes.NetworkResource
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).
+	result := tx.
 		First(&netResources, accountAndIDQueryCondition, accountID, resourceID)
 	if result.Error != nil {
 		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
@@ -2064,8 +2359,13 @@ func (s *SqlStore) GetNetworkResourceByID(ctx context.Context, lockStrength Lock
 }
 
 func (s *SqlStore) GetNetworkResourceByName(ctx context.Context, lockStrength LockingStrength, accountID, resourceName string) (*resourceTypes.NetworkResource, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var netResources *resourceTypes.NetworkResource
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).
+	result := tx.
 		First(&netResources, "account_id = ? AND name = ?", accountID, resourceName)
 	if result.Error != nil {
 		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
@@ -2105,8 +2405,13 @@ func (s *SqlStore) DeleteNetworkResource(ctx context.Context, lockStrength Locki
 
 // GetPATByHashedToken returns a PersonalAccessToken by its hashed token.
 func (s *SqlStore) GetPATByHashedToken(ctx context.Context, lockStrength LockingStrength, hashedToken string) (*types.PersonalAccessToken, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var pat types.PersonalAccessToken
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).First(&pat, "hashed_token = ?", hashedToken)
+	result := tx.First(&pat, "hashed_token = ?", hashedToken)
 	if result.Error != nil {
 		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
 			return nil, status.NewPATNotFoundError(hashedToken)
@@ -2120,8 +2425,13 @@ func (s *SqlStore) GetPATByHashedToken(ctx context.Context, lockStrength Locking
 
 // GetPATByID retrieves a personal access token by its ID and user ID.
 func (s *SqlStore) GetPATByID(ctx context.Context, lockStrength LockingStrength, userID string, patID string) (*types.PersonalAccessToken, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var pat types.PersonalAccessToken
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).
+	result := tx.
 		First(&pat, "id = ? AND user_id = ?", patID, userID)
 	if err := result.Error; err != nil {
 		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
@@ -2136,8 +2446,13 @@ func (s *SqlStore) GetPATByID(ctx context.Context, lockStrength LockingStrength,
 
 // GetUserPATs retrieves personal access tokens for a user.
 func (s *SqlStore) GetUserPATs(ctx context.Context, lockStrength LockingStrength, userID string) ([]*types.PersonalAccessToken, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	var pats []*types.PersonalAccessToken
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).Find(&pats, "user_id = ?", userID)
+	result := tx.Find(&pats, "user_id = ?", userID)
 	if err := result.Error; err != nil {
 		log.WithContext(ctx).Errorf("failed to get user pat's from the store: %s", err)
 		return nil, status.Errorf(status.Internal, "failed to get user pat's from store")
@@ -2195,10 +2510,15 @@ func (s *SqlStore) DeletePAT(ctx context.Context, lockStrength LockingStrength,
 }
 
 func (s *SqlStore) GetPeerByIP(ctx context.Context, lockStrength LockingStrength, accountID string, ip net.IP) (*nbpeer.Peer, error) {
+	tx := s.db
+	if lockStrength != LockingStrengthNone {
+		tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
+	}
+
 	jsonValue := fmt.Sprintf(`"%s"`, ip.String())
 
 	var peer nbpeer.Peer
-	result := s.db.Clauses(clause.Locking{Strength: string(lockStrength)}).
+	result := tx.
 		First(&peer, "account_id = ? AND ip = ?", accountID, jsonValue)
 	if result.Error != nil {
 		// no logging here
diff --git a/management/server/store/store.go b/management/server/store/store.go
index c13a8dfe..fff80924 100644
--- a/management/server/store/store.go
+++ b/management/server/store/store.go
@@ -44,16 +44,19 @@ const (
 	LockingStrengthShare       LockingStrength = "SHARE"         // Allows reading but prevents changes by other transactions.
 	LockingStrengthNoKeyUpdate LockingStrength = "NO KEY UPDATE" // Similar to UPDATE but allows changes to related rows.
 	LockingStrengthKeyShare    LockingStrength = "KEY SHARE"     // Protects against changes to primary/unique keys but allows other updates.
+	LockingStrengthNone        LockingStrength = "NONE"          // No locking, allowing all transactions to proceed without restrictions.
 )
 
 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)
@@ -95,7 +98,7 @@ type Store interface {
 	GetGroupByID(ctx context.Context, lockStrength LockingStrength, accountID, groupID string) (*types.Group, error)
 	GetGroupByName(ctx context.Context, lockStrength LockingStrength, groupName, accountID string) (*types.Group, error)
 	GetGroupsByIDs(ctx context.Context, lockStrength LockingStrength, accountID string, groupIDs []string) (map[string]*types.Group, error)
-	SaveGroups(ctx context.Context, lockStrength LockingStrength, groups []*types.Group) error
+	SaveGroups(ctx context.Context, lockStrength LockingStrength, accountID string, groups []*types.Group) error
 	SaveGroup(ctx context.Context, lockStrength LockingStrength, group *types.Group) error
 	DeleteGroup(ctx context.Context, lockStrength LockingStrength, accountID, groupID string) error
 	DeleteGroups(ctx context.Context, strength LockingStrength, accountID string, groupIDs []string) error
@@ -241,7 +244,7 @@ func getStoreEngine(ctx context.Context, dataDir string, kind types.Engine) type
 }
 
 // NewStore creates a new store based on the provided engine type, data directory, and telemetry metrics
-func NewStore(ctx context.Context, kind types.Engine, dataDir string, metrics telemetry.AppMetrics) (Store, error) {
+func NewStore(ctx context.Context, kind types.Engine, dataDir string, metrics telemetry.AppMetrics, skipMigration bool) (Store, error) {
 	kind = getStoreEngine(ctx, dataDir, kind)
 
 	if err := checkFileStoreEngine(kind, dataDir); err != nil {
@@ -251,13 +254,13 @@ func NewStore(ctx context.Context, kind types.Engine, dataDir string, metrics te
 	switch kind {
 	case types.SqliteStoreEngine:
 		log.WithContext(ctx).Info("using SQLite store engine")
-		return NewSqliteStore(ctx, dataDir, metrics)
+		return NewSqliteStore(ctx, dataDir, metrics, skipMigration)
 	case types.PostgresStoreEngine:
 		log.WithContext(ctx).Info("using Postgres store engine")
-		return newPostgresStore(ctx, metrics)
+		return newPostgresStore(ctx, metrics, skipMigration)
 	case types.MysqlStoreEngine:
 		log.WithContext(ctx).Info("using MySQL store engine")
-		return newMysqlStore(ctx, metrics)
+		return newMysqlStore(ctx, metrics, skipMigration)
 	default:
 		return nil, fmt.Errorf("unsupported kind of store: %s", kind)
 	}
@@ -313,6 +316,15 @@ func getMigrations(ctx context.Context) []migrationFunc {
 		func(db *gorm.DB) error {
 			return migration.MigrateNewField[routerTypes.NetworkRouter](ctx, db, "enabled", true)
 		},
+		func(db *gorm.DB) error {
+			return migration.DropIndex[networkTypes.Network](ctx, db, "idx_networks_id")
+		},
+		func(db *gorm.DB) error {
+			return migration.DropIndex[resourceTypes.NetworkResource](ctx, db, "idx_network_resources_id")
+		},
+		func(db *gorm.DB) error {
+			return migration.DropIndex[routerTypes.NetworkRouter](ctx, db, "idx_network_routers_id")
+		},
 	}
 }
 
@@ -343,7 +355,7 @@ func NewTestStoreFromSQL(ctx context.Context, filename string, dataDir string) (
 		}
 	}
 
-	store, err := NewSqlStore(ctx, db, types.SqliteStoreEngine, nil)
+	store, err := NewSqlStore(ctx, db, types.SqliteStoreEngine, nil, false)
 	if err != nil {
 		return nil, nil, fmt.Errorf("failed to create test store: %v", err)
 	}
@@ -353,11 +365,14 @@ func NewTestStoreFromSQL(ctx context.Context, filename string, dataDir string) (
 		return nil, nil, fmt.Errorf("failed to add all group to account: %v", err)
 	}
 
+	var sqlStore Store
+	var cleanup func()
+
 	maxRetries := 2
 	for i := 0; i < maxRetries; i++ {
-		sqlStore, cleanUp, err := getSqlStoreEngine(ctx, store, kind)
+		sqlStore, cleanup, err = getSqlStoreEngine(ctx, store, kind)
 		if err == nil {
-			return sqlStore, cleanUp, nil
+			return sqlStore, cleanup, nil
 		}
 		if i < maxRetries-1 {
 			time.Sleep(100 * time.Millisecond)
@@ -415,16 +430,16 @@ func getSqlStoreEngine(ctx context.Context, store *SqlStore, kind types.Engine)
 }
 
 func newReusedPostgresStore(ctx context.Context, store *SqlStore, kind types.Engine) (*SqlStore, func(), error) {
-	if envDsn, ok := os.LookupEnv(postgresDsnEnv); !ok || envDsn == "" {
+	dsn, ok := os.LookupEnv(postgresDsnEnv)
+	if !ok || dsn == "" {
 		var err error
-		_, err = testutil.CreatePostgresTestContainer()
+		_, dsn, err = testutil.CreatePostgresTestContainer()
 		if err != nil {
 			return nil, nil, err
 		}
 	}
 
-	dsn, ok := os.LookupEnv(postgresDsnEnv)
-	if !ok {
+	if dsn == "" {
 		return nil, nil, fmt.Errorf("%s is not set", postgresDsnEnv)
 	}
 
@@ -435,28 +450,28 @@ func newReusedPostgresStore(ctx context.Context, store *SqlStore, kind types.Eng
 
 	dsn, cleanup, err := createRandomDB(dsn, db, kind)
 	if err != nil {
-		return nil, cleanup, err
+		return nil, nil, err
 	}
 
 	store, err = NewPostgresqlStoreFromSqlStore(ctx, store, dsn, nil)
 	if err != nil {
-		return nil, cleanup, err
+		return nil, nil, err
 	}
 
 	return store, cleanup, nil
 }
 
 func newReusedMysqlStore(ctx context.Context, store *SqlStore, kind types.Engine) (*SqlStore, func(), error) {
-	if envDsn, ok := os.LookupEnv(mysqlDsnEnv); !ok || envDsn == "" {
+	dsn, ok := os.LookupEnv(mysqlDsnEnv)
+	if !ok || dsn == "" {
 		var err error
-		_, err = testutil.CreateMysqlTestContainer()
+		_, dsn, err = testutil.CreateMysqlTestContainer()
 		if err != nil {
 			return nil, nil, err
 		}
 	}
 
-	dsn, ok := os.LookupEnv(mysqlDsnEnv)
-	if !ok {
+	if dsn == "" {
 		return nil, nil, fmt.Errorf("%s is not set", mysqlDsnEnv)
 	}
 
@@ -467,7 +482,7 @@ func newReusedMysqlStore(ctx context.Context, store *SqlStore, kind types.Engine
 
 	dsn, cleanup, err := createRandomDB(dsn, db, kind)
 	if err != nil {
-		return nil, cleanup, err
+		return nil, nil, err
 	}
 
 	store, err = NewMysqlStoreFromSqlStore(ctx, store, dsn, nil)
@@ -552,7 +567,7 @@ func MigrateFileStoreToSqlite(ctx context.Context, dataDir string) error {
 	log.WithContext(ctx).Infof("%d account will be migrated from file store %s to sqlite store %s",
 		fsStoreAccounts, fileStorePath, sqlStorePath)
 
-	store, err := NewSqliteStoreFromFileStore(ctx, fstore, dataDir, nil)
+	store, err := NewSqliteStoreFromFileStore(ctx, fstore, dataDir, nil, true)
 	if err != nil {
 		return fmt.Errorf("failed creating file store: %s: %v", dataDir, err)
 	}
diff --git a/management/server/types/account.go b/management/server/types/account.go
index 68770999..8315f579 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/config.go b/management/server/types/config.go
index 7a16b20a..13e0f62b 100644
--- a/management/server/types/config.go
+++ b/management/server/types/config.go
@@ -3,6 +3,7 @@ package types
 import (
 	"net/netip"
 
+	"github.com/netbirdio/netbird/management/client/common"
 	"github.com/netbirdio/netbird/management/server/idp"
 	"github.com/netbirdio/netbird/util"
 )
@@ -156,6 +157,8 @@ type ProviderConfig struct {
 	RedirectURLs []string
 	// DisablePromptLogin makes the PKCE flow to not prompt the user for login
 	DisablePromptLogin bool
+	// LoginFlag is used to configure the PKCE flow login behavior
+	LoginFlag common.LoginFlag
 }
 
 // StoreConfig contains Store configuration
diff --git a/management/server/types/firewall_rule.go b/management/server/types/firewall_rule.go
index d98a5687..ef54abea 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/group.go b/management/server/types/group.go
index 00a28fa7..1b321387 100644
--- a/management/server/types/group.go
+++ b/management/server/types/group.go
@@ -14,7 +14,7 @@ const (
 // Group of the peers for ACL
 type Group struct {
 	// ID of the group
-	ID string
+	ID string `gorm:"primaryKey"`
 
 	// AccountID is a reference to Account that this object belongs
 	AccountID string `json:"-" gorm:"index"`
diff --git a/management/server/types/route_firewall_rule.go b/management/server/types/route_firewall_rule.go
index 5b752bc3..c09c64a3 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 7054ede8..a22a36b0 100644
--- a/management/server/types/settings.go
+++ b/management/server/types/settings.go
@@ -39,8 +39,14 @@ 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_"`
+
+	// LazyConnectionEnabled indicates if the experimental feature is enabled or disabled
+	LazyConnectionEnabled bool `gorm:"default:false"`
 }
 
 // Copy copies the Settings struct
@@ -58,6 +64,8 @@ func (s *Settings) Copy() *Settings {
 		PeerInactivityExpiration:        s.PeerInactivityExpiration,
 
 		RoutingPeerDNSResolutionEnabled: s.RoutingPeerDNSResolutionEnabled,
+		LazyConnectionEnabled:           s.LazyConnectionEnabled,
+		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 5f7a4f2c..783fe14d 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
 	}
@@ -59,11 +65,6 @@ type UserInfo struct {
 	LastLogin            time.Time                                  `json:"last_login"`
 	Issued               string                                     `json:"issued"`
 	IntegrationReference integration_reference.IntegrationReference `json:"-"`
-	Permissions          UserPermissions                            `json:"permissions"`
-}
-
-type UserPermissions struct {
-	DashboardView string `json:"dashboard_view"`
 }
 
 // User represents a user of the system
@@ -126,21 +127,18 @@ func (u *User) IsRegularUser() bool {
 	return !u.HasAdminPower() && !u.IsServiceUser
 }
 
+// IsRestrictable checks whether a user is in a restrictable role.
+func (u *User) IsRestrictable() bool {
+	return u.Role == UserRoleUser || u.Role == UserRoleBillingAdmin
+}
+
 // ToUserInfo converts a User object to a UserInfo object.
-func (u *User) ToUserInfo(userData *idp.UserData, settings *Settings) (*UserInfo, error) {
+func (u *User) ToUserInfo(userData *idp.UserData) (*UserInfo, error) {
 	autoGroups := u.AutoGroups
 	if autoGroups == nil {
 		autoGroups = []string{}
 	}
 
-	dashboardViewPermissions := "full"
-	if !u.HasAdminPower() {
-		dashboardViewPermissions = "limited"
-		if settings.RegularUsersViewBlocked {
-			dashboardViewPermissions = "blocked"
-		}
-	}
-
 	if userData == nil {
 		return &UserInfo{
 			ID:            u.Id,
@@ -153,9 +151,6 @@ func (u *User) ToUserInfo(userData *idp.UserData, settings *Settings) (*UserInfo
 			IsBlocked:     u.Blocked,
 			LastLogin:     u.GetLastLogin(),
 			Issued:        u.Issued,
-			Permissions: UserPermissions{
-				DashboardView: dashboardViewPermissions,
-			},
 		}, nil
 	}
 	if userData.ID != u.Id {
@@ -178,9 +173,6 @@ func (u *User) ToUserInfo(userData *idp.UserData, settings *Settings) (*UserInfo
 		IsBlocked:     u.Blocked,
 		LastLogin:     u.GetLastLogin(),
 		Issued:        u.Issued,
-		Permissions: UserPermissions{
-			DashboardView: dashboardViewPermissions,
-		},
 	}, nil
 }
 
diff --git a/management/server/user.go b/management/server/user.go
index 9ec16e72..6d780cda 100644
--- a/management/server/user.go
+++ b/management/server/user.go
@@ -12,6 +12,7 @@ import (
 
 	"github.com/netbirdio/netbird/management/server/activity"
 	nbContext "github.com/netbirdio/netbird/management/server/context"
+	nbcontext "github.com/netbirdio/netbird/management/server/context"
 	"github.com/netbirdio/netbird/management/server/idp"
 	nbpeer "github.com/netbirdio/netbird/management/server/peer"
 	"github.com/netbirdio/netbird/management/server/permissions/modules"
@@ -19,6 +20,7 @@ import (
 	"github.com/netbirdio/netbird/management/server/status"
 	"github.com/netbirdio/netbird/management/server/store"
 	"github.com/netbirdio/netbird/management/server/types"
+	"github.com/netbirdio/netbird/management/server/users"
 	"github.com/netbirdio/netbird/management/server/util"
 )
 
@@ -122,11 +124,6 @@ func (am *DefaultAccountManager) inviteNewUser(ctx context.Context, accountID, u
 		CreatedAt:            time.Now().UTC(),
 	}
 
-	settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
-	if err != nil {
-		return nil, err
-	}
-
 	if err = am.Store.SaveUser(ctx, store.LockingStrengthUpdate, newUser); err != nil {
 		return nil, err
 	}
@@ -138,7 +135,7 @@ func (am *DefaultAccountManager) inviteNewUser(ctx context.Context, accountID, u
 
 	am.StoreEvent(ctx, userID, newUser.Id, accountID, activity.UserInvited, nil)
 
-	return newUser.ToUserInfo(idpUser, settings)
+	return newUser.ToUserInfo(idpUser)
 }
 
 // createNewIdpUser validates the invite and creates a new user in the IdP
@@ -360,6 +357,7 @@ func (am *DefaultAccountManager) CreatePAT(ctx context.Context, accountID string
 		return nil, err
 	}
 
+	// @note this is essential to prevent non admin users with Pats create permission frpm creating one for a service user
 	if initiatorUserID != targetUserID && !(initiatorUser.HasAdminPower() && targetUser.IsServiceUser) {
 		return nil, status.NewAdminPermissionError()
 	}
@@ -533,9 +531,13 @@ func (am *DefaultAccountManager) SaveOrAddUsers(ctx context.Context, accountID,
 		groupsMap[group.ID] = group
 	}
 
-	initiatorUser, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID)
-	if err != nil {
-		return nil, err
+	var initiatorUser *types.User
+	if initiatorUserID != activity.SystemInitiator {
+		result, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID)
+		if err != nil {
+			return nil, err
+		}
+		initiatorUser = result
 	}
 
 	err = am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
@@ -545,7 +547,7 @@ func (am *DefaultAccountManager) SaveOrAddUsers(ctx context.Context, accountID,
 			}
 
 			userHadPeers, updatedUser, userPeersToExpire, userEvents, err := am.processUserUpdate(
-				ctx, transaction, groupsMap, accountID, initiatorUser, update, addIfNotExists, settings,
+				ctx, transaction, groupsMap, accountID, initiatorUserID, initiatorUser, update, addIfNotExists, settings,
 			)
 			if err != nil {
 				return fmt.Errorf("failed to process user update: %w", err)
@@ -631,7 +633,7 @@ func (am *DefaultAccountManager) prepareUserUpdateEvents(ctx context.Context, ac
 }
 
 func (am *DefaultAccountManager) processUserUpdate(ctx context.Context, transaction store.Store, groupsMap map[string]*types.Group,
-	accountID string, initiatorUser, update *types.User, addIfNotExists bool, settings *types.Settings) (bool, *types.User, []*nbpeer.Peer, []func(), error) {
+	accountID, initiatorUserId string, initiatorUser, update *types.User, addIfNotExists bool, settings *types.Settings) (bool, *types.User, []*nbpeer.Peer, []func(), error) {
 
 	if update == nil {
 		return false, nil, nil, nil, status.Errorf(status.InvalidArgument, "provided user update is nil")
@@ -655,10 +657,12 @@ func (am *DefaultAccountManager) processUserUpdate(ctx context.Context, transact
 	updatedUser.Issued = update.Issued
 	updatedUser.IntegrationReference = update.IntegrationReference
 
-	transferredOwnerRole, err := handleOwnerRoleTransfer(ctx, transaction, initiatorUser, update)
+	var transferredOwnerRole bool
+	result, err := handleOwnerRoleTransfer(ctx, transaction, initiatorUser, update)
 	if err != nil {
 		return false, nil, nil, nil, err
 	}
+	transferredOwnerRole = result
 
 	userPeers, err := transaction.GetUserPeers(ctx, store.LockingStrengthUpdate, updatedUser.AccountID, update.Id)
 	if err != nil {
@@ -678,13 +682,13 @@ func (am *DefaultAccountManager) processUserUpdate(ctx context.Context, transact
 			return false, nil, nil, nil, fmt.Errorf("error modifying user peers in groups: %w", err)
 		}
 
-		if err = transaction.SaveGroups(ctx, store.LockingStrengthUpdate, updatedGroups); err != nil {
+		if err = transaction.SaveGroups(ctx, store.LockingStrengthUpdate, accountID, updatedGroups); err != nil {
 			return false, nil, nil, nil, fmt.Errorf("error saving groups: %w", err)
 		}
 	}
 
 	updateAccountPeers := len(userPeers) > 0
-	userEventsToAdd := am.prepareUserUpdateEvents(ctx, updatedUser.AccountID, initiatorUser.Id, oldUser, updatedUser, transferredOwnerRole)
+	userEventsToAdd := am.prepareUserUpdateEvents(ctx, updatedUser.AccountID, initiatorUserId, oldUser, updatedUser, transferredOwnerRole)
 
 	return updateAccountPeers, updatedUser, peersToExpire, userEventsToAdd, nil
 }
@@ -711,7 +715,7 @@ func getUserOrCreateIfNotExists(ctx context.Context, transaction store.Store, ac
 }
 
 func handleOwnerRoleTransfer(ctx context.Context, transaction store.Store, initiatorUser, update *types.User) (bool, error) {
-	if initiatorUser.Role == types.UserRoleOwner && initiatorUser.Id != update.Id && update.Role == types.UserRoleOwner {
+	if initiatorUser != nil && initiatorUser.Role == types.UserRoleOwner && initiatorUser.Id != update.Id && update.Role == types.UserRoleOwner {
 		newInitiatorUser := initiatorUser.Copy()
 		newInitiatorUser.Role = types.UserRoleAdmin
 
@@ -727,23 +731,22 @@ func handleOwnerRoleTransfer(ctx context.Context, transaction store.Store, initi
 // If the AccountManager has a non-nil idpManager and the User is not a service user,
 // it will attempt to look up the UserData from the cache.
 func (am *DefaultAccountManager) getUserInfo(ctx context.Context, user *types.User, accountID string) (*types.UserInfo, error) {
-	settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
-	if err != nil {
-		return nil, err
-	}
-
 	if !isNil(am.idpManager) && !user.IsServiceUser {
 		userData, err := am.lookupUserInCache(ctx, user.Id, accountID)
 		if err != nil {
 			return nil, err
 		}
-		return user.ToUserInfo(userData, settings)
+		return user.ToUserInfo(userData)
 	}
-	return user.ToUserInfo(nil, settings)
+	return user.ToUserInfo(nil)
 }
 
 // validateUserUpdate validates the update operation for a user.
 func validateUserUpdate(groupsMap map[string]*types.Group, initiatorUser, oldUser, update *types.User) error {
+	if initiatorUser == nil {
+		return nil
+	}
+
 	// @todo double check these
 	if initiatorUser.HasAdminPower() && initiatorUser.Id == update.Id && oldUser.Blocked != update.Blocked {
 		return status.Errorf(status.PermissionDenied, "admins can't block or unblock themselves")
@@ -825,9 +828,13 @@ func (am *DefaultAccountManager) GetUsersFromAccount(ctx context.Context, accoun
 		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)
+	var user *types.User
+	if initiatorUserID != activity.SystemInitiator {
+		result, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, initiatorUserID)
+		if err != nil {
+			return nil, fmt.Errorf("failed to get user: %w", err)
+		}
+		user = result
 	}
 
 	accountUsers := []*types.User{}
@@ -837,7 +844,7 @@ func (am *DefaultAccountManager) GetUsersFromAccount(ctx context.Context, accoun
 		if err != nil {
 			return nil, err
 		}
-	case user.AccountID == accountID:
+	case user != nil && user.AccountID == accountID:
 		accountUsers = append(accountUsers, user)
 	default:
 		return map[string]*types.UserInfo{}, nil
@@ -879,17 +886,12 @@ func (am *DefaultAccountManager) BuildUserInfosForAccount(ctx context.Context, a
 		queriedUsers = append(queriedUsers, usersFromIntegration...)
 	}
 
-	settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
-	if err != nil {
-		return nil, err
-	}
-
 	userInfosMap := make(map[string]*types.UserInfo)
 
 	// in case of self-hosted, or IDP doesn't return anything, we will return the locally stored userInfo
 	if len(queriedUsers) == 0 {
 		for _, accountUser := range accountUsers {
-			info, err := accountUser.ToUserInfo(nil, settings)
+			info, err := accountUser.ToUserInfo(nil)
 			if err != nil {
 				return nil, err
 			}
@@ -902,7 +904,7 @@ func (am *DefaultAccountManager) BuildUserInfosForAccount(ctx context.Context, a
 	for _, localUser := range accountUsers {
 		var info *types.UserInfo
 		if queriedUser, contains := findUserInIDPUserdata(localUser.Id, queriedUsers); contains {
-			info, err = localUser.ToUserInfo(queriedUser, settings)
+			info, err = localUser.ToUserInfo(queriedUser)
 			if err != nil {
 				return nil, err
 			}
@@ -912,14 +914,6 @@ func (am *DefaultAccountManager) BuildUserInfosForAccount(ctx context.Context, a
 				name = localUser.ServiceUserName
 			}
 
-			dashboardViewPermissions := "full"
-			if !localUser.HasAdminPower() {
-				dashboardViewPermissions = "limited"
-				if settings.RegularUsersViewBlocked {
-					dashboardViewPermissions = "blocked"
-				}
-			}
-
 			info = &types.UserInfo{
 				ID:            localUser.Id,
 				Email:         "",
@@ -929,7 +923,6 @@ func (am *DefaultAccountManager) BuildUserInfosForAccount(ctx context.Context, a
 				Status:        string(types.UserStatusActive),
 				IsServiceUser: localUser.IsServiceUser,
 				NonDeletable:  localUser.NonDeletable,
-				Permissions:   types.UserPermissions{DashboardView: dashboardViewPermissions},
 			}
 		}
 		userInfosMap[info.ID] = info
@@ -940,6 +933,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
@@ -957,7 +956,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),
 		)
 	}
 
@@ -1233,8 +1232,10 @@ 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) {
+// GetCurrentUserInfo retrieves the account's current user info and permissions
+func (am *DefaultAccountManager) GetCurrentUserInfo(ctx context.Context, userAuth nbcontext.UserAuth) (*users.UserInfoWithPermissions, error) {
+	accountID, userID := userAuth.AccountId, userAuth.UserId
+
 	user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthShare, userID)
 	if err != nil {
 		return nil, err
@@ -1252,10 +1253,25 @@ func (am *DefaultAccountManager) GetCurrentUserInfo(ctx context.Context, account
 		return nil, err
 	}
 
+	settings, err := am.Store.GetAccountSettings(ctx, store.LockingStrengthShare, accountID)
+	if err != nil {
+		return nil, err
+	}
+
 	userInfo, err := am.getUserInfo(ctx, user, accountID)
 	if err != nil {
 		return nil, err
 	}
 
-	return userInfo, nil
+	userWithPermissions := &users.UserInfoWithPermissions{
+		UserInfo:   userInfo,
+		Restricted: !userAuth.IsChild && user.IsRestrictable() && settings.RegularUsersViewBlocked,
+	}
+
+	permissions, err := am.permissionsManager.GetPermissionsByRole(ctx, user.Role)
+	if err == nil {
+		userWithPermissions.Permissions = permissions
+	}
+
+	return userWithPermissions, nil
 }
diff --git a/management/server/users/user.go b/management/server/users/user.go
new file mode 100644
index 00000000..2f278827
--- /dev/null
+++ b/management/server/users/user.go
@@ -0,0 +1,14 @@
+package users
+
+import (
+	"github.com/netbirdio/netbird/management/server/permissions/roles"
+	"github.com/netbirdio/netbird/management/server/types"
+)
+
+// Wrapped UserInfo with Role Permissions
+type UserInfoWithPermissions struct {
+	*types.UserInfo
+
+	Permissions roles.Permissions
+	Restricted  bool
+}
diff --git a/relay/client/dialer/quic/quic.go b/relay/client/dialer/quic/quic.go
index 7fd486f8..3fd48fb1 100644
--- a/relay/client/dialer/quic/quic.go
+++ b/relay/client/dialer/quic/quic.go
@@ -28,6 +28,16 @@ func (d Dialer) Dial(ctx context.Context, address string) (net.Conn, error) {
 		return nil, err
 	}
 
+	// Get the base TLS config
+	tlsClientConfig := quictls.ClientQUICTLSConfig()
+
+	// Set ServerName to hostname if not an IP address
+	host, _, splitErr := net.SplitHostPort(quicURL)
+	if splitErr == nil && net.ParseIP(host) == nil {
+		// It's a hostname, not an IP - modify directly
+		tlsClientConfig.ServerName = host
+	}
+
 	quicConfig := &quic.Config{
 		KeepAlivePeriod:   30 * time.Second,
 		MaxIdleTimeout:    4 * time.Minute,
@@ -47,7 +57,7 @@ func (d Dialer) Dial(ctx context.Context, address string) (net.Conn, error) {
 		return nil, err
 	}
 
-	session, err := quic.Dial(ctx, udpConn, udpAddr, quictls.ClientQUICTLSConfig(), quicConfig)
+	session, err := quic.Dial(ctx, udpConn, udpAddr, tlsClientConfig, quicConfig)
 	if err != nil {
 		if errors.Is(err, context.Canceled) {
 			return nil, err
@@ -61,12 +71,29 @@ func (d Dialer) Dial(ctx context.Context, address string) (net.Conn, error) {
 }
 
 func prepareURL(address string) (string, error) {
-	if !strings.HasPrefix(address, "rel://") && !strings.HasPrefix(address, "rels://") {
+	var host string
+	var defaultPort string
+
+	switch {
+	case strings.HasPrefix(address, "rels://"):
+		host = address[7:]
+		defaultPort = "443"
+	case strings.HasPrefix(address, "rel://"):
+		host = address[6:]
+		defaultPort = "80"
+	default:
 		return "", fmt.Errorf("unsupported scheme: %s", address)
 	}
 
-	if strings.HasPrefix(address, "rels://") {
-		return address[7:], nil
+	finalHost, finalPort, err := net.SplitHostPort(host)
+	if err != nil {
+		if strings.Contains(err.Error(), "missing port") {
+			return host + ":" + defaultPort, nil
+		}
+
+		// return any other split error as is
+		return "", err
 	}
-	return address[6:], nil
+
+	return finalHost + ":" + finalPort, nil
 }
diff --git a/release_files/install.sh b/release_files/install.sh
index e5a61dcf..da5c613d 100755
--- a/release_files/install.sh
+++ b/release_files/install.sh
@@ -199,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"
@@ -209,16 +224,22 @@ check_use_bin_variable() {
 
 install_netbird() {
     if [ -x "$(command -v netbird)" ]; then
-        status_output=$(netbird status)
-        if echo "$status_output" | grep -q 'Management: Connected' && echo "$status_output" | grep -q 'Signal: Connected'; then
-            echo "NetBird service is running, please stop it before proceeding"
-            exit 1
-        fi
+      status_output="$(netbird status 2>&1 || true)"
+
+      if echo "$status_output" | grep -q 'failed to connect to daemon error: context deadline exceeded'; then
+          echo "Warning: could not reach NetBird daemon (timeout), proceeding anyway"
+      else
+          if echo "$status_output" | grep -q 'Management: Connected' && \
+              echo "$status_output" | grep -q 'Signal: Connected'; then
+              echo "NetBird service is running, please stop it before proceeding"
+              exit 1
+          fi
 
-        if [ -n "$status_output" ]; then
-            echo "NetBird seems to be installed already, please remove it before proceeding"
-            exit 1
-        fi
+          if [ -n "$status_output" ]; then
+              echo "NetBird seems to be installed already, please remove it before proceeding"
+              exit 1
+          fi
+      fi
     fi
 
     # Run the installation, if a desktop environment is not detected
@@ -265,6 +286,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
@@ -274,7 +305,7 @@ install_netbird() {
             netbird service stop
             netbird service uninstall
 
-            # Unlik the app
+            # Unlink the app
             brew unlink netbird
         fi
 
@@ -312,7 +343,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
@@ -451,9 +482,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
@@ -471,4 +501,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 4d952beb..06460817 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 f7bf3ea8..722dacc2 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/sharedsock/sock_linux.go b/sharedsock/sock_linux.go
index 74ac6c16..1c22e786 100644
--- a/sharedsock/sock_linux.go
+++ b/sharedsock/sock_linux.go
@@ -234,7 +234,7 @@ func (s *SharedSocket) read(receiver receiver) {
 }
 
 // ReadFrom reads packets received in the packetDemux channel
-func (s *SharedSocket) ReadFrom(b []byte) (n int, addr net.Addr, err error) {
+func (s *SharedSocket) ReadFrom(b []byte) (int, net.Addr, error) {
 	var pkt rcvdPacket
 	select {
 	case <-s.ctx.Done():
@@ -263,8 +263,7 @@ func (s *SharedSocket) ReadFrom(b []byte) (n int, addr net.Addr, err error) {
 
 	decodedLayers := make([]gopacket.LayerType, 0, 3)
 
-	err = parser.DecodeLayers(pkt.buf, &decodedLayers)
-	if err != nil {
+	if err := parser.DecodeLayers(pkt.buf, &decodedLayers); err != nil {
 		return 0, nil, err
 	}
 
@@ -273,8 +272,8 @@ func (s *SharedSocket) ReadFrom(b []byte) (n int, addr net.Addr, err error) {
 		Port: int(udp.SrcPort),
 	}
 
-	copy(b, payload)
-	return int(udp.Length), remoteAddr, nil
+	n := copy(b, payload)
+	return n, remoteAddr, nil
 }
 
 // WriteTo builds a UDP packet and writes it using the specific IP version writer
diff --git a/signal/peer/peer.go b/signal/peer/peer.go
index ed2360d6..2252e877 100644
--- a/signal/peer/peer.go
+++ b/signal/peer/peer.go
@@ -79,7 +79,7 @@ func (registry *Registry) Register(peer *Peer) {
 	p, loaded := registry.Peers.LoadOrStore(peer.Id, peer)
 	if loaded {
 		pp := p.(*Peer)
-		log.Warnf("peer [%s] is already registered [new streamID %d, previous StreamID %d]. Will override stream.",
+		log.Tracef("peer [%s] is already registered [new streamID %d, previous StreamID %d]. Will override stream.",
 			peer.Id, peer.StreamID, pp.StreamID)
 		registry.Peers.Store(peer.Id, peer)
 		return
@@ -104,7 +104,7 @@ func (registry *Registry) Deregister(peer *Peer) {
 		pp := p.(*Peer)
 		if peer.StreamID < pp.StreamID {
 			registry.Peers.Store(peer.Id, p)
-			log.Warnf("attempted to remove newer registered stream of a peer [%s] [newer streamID %d, previous StreamID %d]. Ignoring.",
+			log.Debugf("attempted to remove newer registered stream of a peer [%s] [newer streamID %d, previous StreamID %d]. Ignoring.",
 				peer.Id, pp.StreamID, peer.StreamID)
 			return
 		}
diff --git a/signal/server/signal.go b/signal/server/signal.go
index 3cae7e86..9dfba779 100644
--- a/signal/server/signal.go
+++ b/signal/server/signal.go
@@ -3,10 +3,8 @@ package server
 import (
 	"context"
 	"fmt"
-	"io"
 	"time"
 
-	"github.com/netbirdio/signal-dispatcher/dispatcher"
 	log "github.com/sirupsen/logrus"
 	"go.opentelemetry.io/otel/attribute"
 	"go.opentelemetry.io/otel/metric"
@@ -15,6 +13,8 @@ import (
 	"google.golang.org/grpc/status"
 	gproto "google.golang.org/protobuf/proto"
 
+	"github.com/netbirdio/signal-dispatcher/dispatcher"
+
 	"github.com/netbirdio/netbird/signal/metrics"
 	"github.com/netbirdio/netbird/signal/peer"
 	"github.com/netbirdio/netbird/signal/proto"
@@ -28,10 +28,11 @@ const (
 	labelTypeStream        = "stream"
 	labelTypeMessage       = "message"
 
-	labelError             = "error"
-	labelErrorMissingId    = "missing_id"
-	labelErrorMissingMeta  = "missing_meta"
-	labelErrorFailedHeader = "failed_header"
+	labelError                   = "error"
+	labelErrorMissingId          = "missing_id"
+	labelErrorMissingMeta        = "missing_meta"
+	labelErrorFailedHeader       = "failed_header"
+	labelErrorFailedRegistration = "failed_registration"
 
 	labelRegistrationStatus   = "status"
 	labelRegistrationFound    = "found"
@@ -69,7 +70,7 @@ func NewServer(ctx context.Context, meter metric.Meter) (*Server, error) {
 
 // Send forwards a message to the signal peer
 func (s *Server) Send(ctx context.Context, msg *proto.EncryptedMessage) (*proto.EncryptedMessage, error) {
-	log.Debugf("received a new message to send from peer [%s] to peer [%s]", msg.Key, msg.RemoteKey)
+	log.Tracef("received a new message to send from peer [%s] to peer [%s]", msg.Key, msg.RemoteKey)
 
 	if _, found := s.registry.Get(msg.RemoteKey); found {
 		s.forwardMessageToPeer(ctx, msg)
@@ -98,28 +99,9 @@ func (s *Server) ConnectStream(stream proto.SignalExchange_ConnectStreamServer)
 
 	log.Debugf("peer connected [%s] [streamID %d] ", p.Id, p.StreamID)
 
-	for {
-		select {
-		case <-stream.Context().Done():
-			log.Debugf("stream closed for peer [%s] [streamID %d] due to context cancellation", p.Id, p.StreamID)
-			return stream.Context().Err()
-		default:
-			// read incoming messages
-			msg, err := stream.Recv()
-			if err == io.EOF {
-				break
-			} else if err != nil {
-				return err
-			}
-
-			log.Debugf("Received a response from peer [%s] to peer [%s]", msg.Key, msg.RemoteKey)
-
-			_, err = s.dispatcher.SendMessage(stream.Context(), msg)
-			if err != nil {
-				log.Debugf("error while sending message from peer [%s] to peer [%s] %v", msg.Key, msg.RemoteKey, err)
-			}
-		}
-	}
+	<-stream.Context().Done()
+	log.Debugf("peer stream closing [%s] [streamID %d] ", p.Id, p.StreamID)
+	return nil
 }
 
 func (s *Server) RegisterPeer(stream proto.SignalExchange_ConnectStreamServer) (*peer.Peer, error) {
@@ -138,7 +120,12 @@ func (s *Server) RegisterPeer(stream proto.SignalExchange_ConnectStreamServer) (
 
 	p := peer.NewPeer(id[0], stream)
 	s.registry.Register(p)
-	s.dispatcher.ListenForMessages(stream.Context(), p.Id, s.forwardMessageToPeer)
+	err := s.dispatcher.ListenForMessages(stream.Context(), p.Id, s.forwardMessageToPeer)
+	if err != nil {
+		s.metrics.RegistrationFailures.Add(stream.Context(), 1, metric.WithAttributes(attribute.String(labelError, labelErrorFailedRegistration)))
+		log.Errorf("error while registering message listener for peer [%s] %v", p.Id, err)
+		return nil, status.Errorf(codes.Internal, "error while registering message listener")
+	}
 	return p, nil
 }
 
@@ -149,7 +136,7 @@ func (s *Server) DeregisterPeer(p *peer.Peer) {
 }
 
 func (s *Server) forwardMessageToPeer(ctx context.Context, msg *proto.EncryptedMessage) {
-	log.Debugf("forwarding a new message from peer [%s] to peer [%s]", msg.Key, msg.RemoteKey)
+	log.Tracef("forwarding a new message from peer [%s] to peer [%s]", msg.Key, msg.RemoteKey)
 	getRegistrationStart := time.Now()
 
 	// lookup the target peer where the message is going to
@@ -168,7 +155,7 @@ func (s *Server) forwardMessageToPeer(ctx context.Context, msg *proto.EncryptedM
 
 	// forward the message to the target peer
 	if err := dstPeer.Stream.Send(msg); err != nil {
-		log.Warnf("error while forwarding message from peer [%s] to peer [%s] %v", msg.Key, msg.RemoteKey, err)
+		log.Tracef("error while forwarding message from peer [%s] to peer [%s] %v", msg.Key, msg.RemoteKey, err)
 		// todo respond to the sender?
 		s.metrics.MessageForwardFailures.Add(ctx, 1, metric.WithAttributes(attribute.String(labelType, labelTypeError)))
 		return
diff --git a/upload-server/Dockerfile b/upload-server/Dockerfile
new file mode 100644
index 00000000..a38c6fbb
--- /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 00000000..dcfb35cd
--- /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 00000000..f12c472d
--- /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/s3.go b/upload-server/server/s3.go
new file mode 100644
index 00000000..c0976acb
--- /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/server.go b/upload-server/server/server.go
new file mode 100644
index 00000000..29ef7273
--- /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 00000000..327c28e7
--- /dev/null
+++ b/upload-server/types/upload.go
@@ -0,0 +1,18 @@
+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"
+
+	DefaultBundleURL = "https://upload.debug.netbird.io" + GetURLPath
+)
+
+// GetURLResponse is the response for the GetURL request
+type GetURLResponse struct {
+	URL string
+	Key string
+}
diff --git a/util/net/net.go b/util/net/net.go
index b573f9ae..fdcf4ee6 100644
--- a/util/net/net.go
+++ b/util/net/net.go
@@ -1,8 +1,10 @@
 package net
 
 import (
+	"fmt"
 	"math/big"
 	"net"
+	"net/netip"
 
 	"github.com/google/uuid"
 )
@@ -54,11 +56,13 @@ func GenerateConnID() ConnectionID {
 	return ConnectionID(uuid.NewString())
 }
 
-func GetLastIPFromNetwork(network *net.IPNet, fromEnd int) net.IP {
-	// Calculate the last IP in the CIDR range
+func GetLastIPFromNetwork(network netip.Prefix, fromEnd int) (netip.Addr, error) {
 	var endIP net.IP
-	for i := 0; i < len(network.IP); i++ {
-		endIP = append(endIP, network.IP[i]|^network.Mask[i])
+	addr := network.Addr().AsSlice()
+	mask := net.CIDRMask(network.Bits(), len(addr)*8)
+
+	for i := 0; i < len(addr); i++ {
+		endIP = append(endIP, addr[i]|^mask[i])
 	}
 
 	// convert to big.Int
@@ -70,5 +74,10 @@ func GetLastIPFromNetwork(network *net.IPNet, fromEnd int) net.IP {
 	resultInt := big.NewInt(0)
 	resultInt.Sub(endInt, fromEndBig)
 
-	return resultInt.Bytes()
+	ip, ok := netip.AddrFromSlice(resultInt.Bytes())
+	if !ok {
+		return netip.Addr{}, fmt.Errorf("invalid IP address from network %s", network)
+	}
+
+	return ip.Unmap(), nil
 }
diff --git a/version/update.go b/version/update.go
index 1de60ea9..3f548ea8 100644
--- a/version/update.go
+++ b/version/update.go
@@ -93,23 +93,25 @@ func (u *Update) SetOnUpdateListener(updateFn func()) {
 }
 
 func (u *Update) startFetcher() {
-	changed := u.fetchVersion()
-	if changed {
+	if changed := u.fetchVersion(); changed {
 		u.checkUpdate()
 	}
 
-	select {
-	case <-u.fetchDone:
-		return
-	case <-u.fetchTicker.C:
-		changed := u.fetchVersion()
-		if changed {
-			u.checkUpdate()
+	for {
+		select {
+		case <-u.fetchDone:
+			return
+		case <-u.fetchTicker.C:
+			if changed := u.fetchVersion(); changed {
+				u.checkUpdate()
+			}
 		}
 	}
 }
 
 func (u *Update) fetchVersion() bool {
+	log.Debugf("fetching version info from %s", versionURL)
+
 	resp, err := http.Get(versionURL)
 	if err != nil {
 		log.Errorf("failed to fetch version info: %s", err)



Test patch:
diff --git a/client/cmd/testutil_test.go b/client/cmd/testutil_test.go
index 70abe4ab..258a8daf 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/firewall/iptables/manager_linux_test.go b/client/firewall/iptables/manager_linux_test.go
index af9f5dd2..30f391a6 100644
--- a/client/firewall/iptables/manager_linux_test.go
+++ b/client/firewall/iptables/manager_linux_test.go
@@ -2,7 +2,7 @@ package iptables
 
 import (
 	"fmt"
-	"net"
+	"net/netip"
 	"testing"
 	"time"
 
@@ -19,11 +19,8 @@ var ifaceMock = &iFaceMock{
 	},
 	AddressFunc: func() wgaddr.Address {
 		return wgaddr.Address{
-			IP: net.ParseIP("10.20.0.1"),
-			Network: &net.IPNet{
-				IP:   net.ParseIP("10.20.0.0"),
-				Mask: net.IPv4Mask(255, 255, 255, 0),
-			},
+			IP:      netip.MustParseAddr("10.20.0.1"),
+			Network: netip.MustParsePrefix("10.20.0.0/24"),
 		}
 	},
 }
@@ -70,12 +67,12 @@ func TestIptablesManager(t *testing.T) {
 
 	var rule2 []fw.Rule
 	t.Run("add second rule", func(t *testing.T) {
-		ip := net.ParseIP("10.20.0.3")
+		ip := netip.MustParseAddr("10.20.0.3")
 		port := &fw.Port{
 			IsRange: true,
 			Values:  []uint16{8043, 8046},
 		}
-		rule2, err = manager.AddPeerFiltering(nil, ip, "tcp", port, nil, fw.ActionAccept, "")
+		rule2, err = manager.AddPeerFiltering(nil, ip.AsSlice(), "tcp", port, nil, fw.ActionAccept, "")
 		require.NoError(t, err, "failed to add rule")
 
 		for _, r := range rule2 {
@@ -95,9 +92,9 @@ func TestIptablesManager(t *testing.T) {
 
 	t.Run("reset check", func(t *testing.T) {
 		// add second rule
-		ip := net.ParseIP("10.20.0.3")
+		ip := netip.MustParseAddr("10.20.0.3")
 		port := &fw.Port{Values: []uint16{5353}}
-		_, err = manager.AddPeerFiltering(nil, ip, "udp", nil, port, fw.ActionAccept, "")
+		_, err = manager.AddPeerFiltering(nil, ip.AsSlice(), "udp", nil, port, fw.ActionAccept, "")
 		require.NoError(t, err, "failed to add rule")
 
 		err = manager.Close(nil)
@@ -119,11 +116,8 @@ func TestIptablesManagerIPSet(t *testing.T) {
 		},
 		AddressFunc: func() wgaddr.Address {
 			return wgaddr.Address{
-				IP: net.ParseIP("10.20.0.1"),
-				Network: &net.IPNet{
-					IP:   net.ParseIP("10.20.0.0"),
-					Mask: net.IPv4Mask(255, 255, 255, 0),
-				},
+				IP:      netip.MustParseAddr("10.20.0.1"),
+				Network: netip.MustParsePrefix("10.20.0.0/24"),
 			}
 		},
 	}
@@ -144,11 +138,11 @@ func TestIptablesManagerIPSet(t *testing.T) {
 
 	var rule2 []fw.Rule
 	t.Run("add second rule", func(t *testing.T) {
-		ip := net.ParseIP("10.20.0.3")
+		ip := netip.MustParseAddr("10.20.0.3")
 		port := &fw.Port{
 			Values: []uint16{443},
 		}
-		rule2, err = manager.AddPeerFiltering(nil, ip, "tcp", port, nil, fw.ActionAccept, "default")
+		rule2, err = manager.AddPeerFiltering(nil, ip.AsSlice(), "tcp", port, nil, fw.ActionAccept, "default")
 		for _, r := range rule2 {
 			require.NoError(t, err, "failed to add rule")
 			require.Equal(t, r.(*Rule).ipsetName, "default-sport", "ipset name must be set")
@@ -186,11 +180,8 @@ func TestIptablesCreatePerformance(t *testing.T) {
 		},
 		AddressFunc: func() wgaddr.Address {
 			return wgaddr.Address{
-				IP: net.ParseIP("10.20.0.1"),
-				Network: &net.IPNet{
-					IP:   net.ParseIP("10.20.0.0"),
-					Mask: net.IPv4Mask(255, 255, 255, 0),
-				},
+				IP:      netip.MustParseAddr("10.20.0.1"),
+				Network: netip.MustParsePrefix("10.20.0.0/24"),
 			}
 		},
 	}
@@ -212,11 +203,11 @@ func TestIptablesCreatePerformance(t *testing.T) {
 
 			require.NoError(t, err)
 
-			ip := net.ParseIP("10.20.0.100")
+			ip := netip.MustParseAddr("10.20.0.100")
 			start := time.Now()
 			for i := 0; i < testMax; i++ {
 				port := &fw.Port{Values: []uint16{uint16(1000 + i)}}
-				_, err = manager.AddPeerFiltering(nil, ip, "tcp", nil, port, fw.ActionAccept, "")
+				_, err = manager.AddPeerFiltering(nil, ip.AsSlice(), "tcp", nil, port, fw.ActionAccept, "")
 
 				require.NoError(t, err, "failed to add rule")
 			}
diff --git a/client/firewall/iptables/router_linux_test.go b/client/firewall/iptables/router_linux_test.go
index dad77dee..e9eeff86 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_test.go b/client/firewall/manager/firewall_test.go
index 3f47d667..18034690 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/nftables/manager_linux_test.go b/client/firewall/nftables/manager_linux_test.go
index 373743a0..1dd3e918 100644
--- a/client/firewall/nftables/manager_linux_test.go
+++ b/client/firewall/nftables/manager_linux_test.go
@@ -3,7 +3,6 @@ package nftables
 import (
 	"bytes"
 	"fmt"
-	"net"
 	"net/netip"
 	"os/exec"
 	"testing"
@@ -25,11 +24,8 @@ var ifaceMock = &iFaceMock{
 	},
 	AddressFunc: func() wgaddr.Address {
 		return wgaddr.Address{
-			IP: net.ParseIP("100.96.0.1"),
-			Network: &net.IPNet{
-				IP:   net.ParseIP("100.96.0.0"),
-				Mask: net.IPv4Mask(255, 255, 255, 0),
-			},
+			IP:      netip.MustParseAddr("100.96.0.1"),
+			Network: netip.MustParsePrefix("100.96.0.0/16"),
 		}
 	},
 }
@@ -70,11 +66,11 @@ func TestNftablesManager(t *testing.T) {
 		time.Sleep(time.Second)
 	}()
 
-	ip := net.ParseIP("100.96.0.1")
+	ip := netip.MustParseAddr("100.96.0.1").Unmap()
 
 	testClient := &nftables.Conn{}
 
-	rule, err := manager.AddPeerFiltering(nil, ip, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{53}}, fw.ActionDrop, "")
+	rule, err := manager.AddPeerFiltering(nil, ip.AsSlice(), fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{53}}, fw.ActionDrop, "")
 	require.NoError(t, err, "failed to add rule")
 
 	err = manager.Flush()
@@ -109,8 +105,6 @@ func TestNftablesManager(t *testing.T) {
 	}
 	compareExprsIgnoringCounters(t, rules[0].Exprs, expectedExprs1)
 
-	ipToAdd, _ := netip.AddrFromSlice(ip)
-	add := ipToAdd.Unmap()
 	expectedExprs2 := []expr.Any{
 		&expr.Payload{
 			DestRegister: 1,
@@ -132,7 +126,7 @@ func TestNftablesManager(t *testing.T) {
 		&expr.Cmp{
 			Op:       expr.CmpOpEq,
 			Register: 1,
-			Data:     add.AsSlice(),
+			Data:     ip.AsSlice(),
 		},
 		&expr.Payload{
 			DestRegister: 1,
@@ -173,11 +167,8 @@ func TestNFtablesCreatePerformance(t *testing.T) {
 		},
 		AddressFunc: func() wgaddr.Address {
 			return wgaddr.Address{
-				IP: net.ParseIP("100.96.0.1"),
-				Network: &net.IPNet{
-					IP:   net.ParseIP("100.96.0.0"),
-					Mask: net.IPv4Mask(255, 255, 255, 0),
-				},
+				IP:      netip.MustParseAddr("100.96.0.1"),
+				Network: netip.MustParsePrefix("100.96.0.0/16"),
 			}
 		},
 	}
@@ -197,11 +188,11 @@ func TestNFtablesCreatePerformance(t *testing.T) {
 				time.Sleep(time.Second)
 			}()
 
-			ip := net.ParseIP("10.20.0.100")
+			ip := netip.MustParseAddr("10.20.0.100")
 			start := time.Now()
 			for i := 0; i < testMax; i++ {
 				port := &fw.Port{Values: []uint16{uint16(1000 + i)}}
-				_, err = manager.AddPeerFiltering(nil, ip, "tcp", nil, port, fw.ActionAccept, "")
+				_, err = manager.AddPeerFiltering(nil, ip.AsSlice(), "tcp", nil, port, fw.ActionAccept, "")
 				require.NoError(t, err, "failed to add rule")
 
 				if i%100 == 0 {
@@ -282,14 +273,14 @@ func TestNftablesManagerCompatibilityWithIptables(t *testing.T) {
 		verifyIptablesOutput(t, stdout, stderr)
 	})
 
-	ip := net.ParseIP("100.96.0.1")
-	_, err = manager.AddPeerFiltering(nil, ip, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionAccept, "")
+	ip := netip.MustParseAddr("100.96.0.1")
+	_, err = manager.AddPeerFiltering(nil, ip.AsSlice(), fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionAccept, "")
 	require.NoError(t, err, "failed to add peer filtering rule")
 
 	_, 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 +289,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_test.go b/client/firewall/nftables/router_linux_test.go
index 28baef4d..4fdbf350 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 267e93ef..59a370a9 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/localip_test.go b/client/firewall/uspfilter/localip_test.go
index 0104c960..45ac912c 100644
--- a/client/firewall/uspfilter/localip_test.go
+++ b/client/firewall/uspfilter/localip_test.go
@@ -20,11 +20,8 @@ func TestLocalIPManager(t *testing.T) {
 		{
 			name: "Localhost range",
 			setupAddr: wgaddr.Address{
-				IP: net.ParseIP("192.168.1.1"),
-				Network: &net.IPNet{
-					IP:   net.ParseIP("192.168.1.0"),
-					Mask: net.CIDRMask(24, 32),
-				},
+				IP:      netip.MustParseAddr("192.168.1.1"),
+				Network: netip.MustParsePrefix("192.168.1.0/24"),
 			},
 			testIP:   netip.MustParseAddr("127.0.0.2"),
 			expected: true,
@@ -32,11 +29,8 @@ func TestLocalIPManager(t *testing.T) {
 		{
 			name: "Localhost standard address",
 			setupAddr: wgaddr.Address{
-				IP: net.ParseIP("192.168.1.1"),
-				Network: &net.IPNet{
-					IP:   net.ParseIP("192.168.1.0"),
-					Mask: net.CIDRMask(24, 32),
-				},
+				IP:      netip.MustParseAddr("192.168.1.1"),
+				Network: netip.MustParsePrefix("192.168.1.0/24"),
 			},
 			testIP:   netip.MustParseAddr("127.0.0.1"),
 			expected: true,
@@ -44,11 +38,8 @@ func TestLocalIPManager(t *testing.T) {
 		{
 			name: "Localhost range edge",
 			setupAddr: wgaddr.Address{
-				IP: net.ParseIP("192.168.1.1"),
-				Network: &net.IPNet{
-					IP:   net.ParseIP("192.168.1.0"),
-					Mask: net.CIDRMask(24, 32),
-				},
+				IP:      netip.MustParseAddr("192.168.1.1"),
+				Network: netip.MustParsePrefix("192.168.1.0/24"),
 			},
 			testIP:   netip.MustParseAddr("127.255.255.255"),
 			expected: true,
@@ -56,11 +47,8 @@ func TestLocalIPManager(t *testing.T) {
 		{
 			name: "Local IP matches",
 			setupAddr: wgaddr.Address{
-				IP: net.ParseIP("192.168.1.1"),
-				Network: &net.IPNet{
-					IP:   net.ParseIP("192.168.1.0"),
-					Mask: net.CIDRMask(24, 32),
-				},
+				IP:      netip.MustParseAddr("192.168.1.1"),
+				Network: netip.MustParsePrefix("192.168.1.0/24"),
 			},
 			testIP:   netip.MustParseAddr("192.168.1.1"),
 			expected: true,
@@ -68,11 +56,8 @@ func TestLocalIPManager(t *testing.T) {
 		{
 			name: "Local IP doesn't match",
 			setupAddr: wgaddr.Address{
-				IP: net.ParseIP("192.168.1.1"),
-				Network: &net.IPNet{
-					IP:   net.ParseIP("192.168.1.0"),
-					Mask: net.CIDRMask(24, 32),
-				},
+				IP:      netip.MustParseAddr("192.168.1.1"),
+				Network: netip.MustParsePrefix("192.168.1.0/24"),
 			},
 			testIP:   netip.MustParseAddr("192.168.1.2"),
 			expected: false,
@@ -80,11 +65,8 @@ func TestLocalIPManager(t *testing.T) {
 		{
 			name: "Local IP doesn't match - addresses 32 apart",
 			setupAddr: wgaddr.Address{
-				IP: net.ParseIP("192.168.1.1"),
-				Network: &net.IPNet{
-					IP:   net.ParseIP("192.168.1.0"),
-					Mask: net.CIDRMask(24, 32),
-				},
+				IP:      netip.MustParseAddr("192.168.1.1"),
+				Network: netip.MustParsePrefix("192.168.1.0/24"),
 			},
 			testIP:   netip.MustParseAddr("192.168.1.33"),
 			expected: false,
@@ -92,11 +74,8 @@ func TestLocalIPManager(t *testing.T) {
 		{
 			name: "IPv6 address",
 			setupAddr: wgaddr.Address{
-				IP: net.ParseIP("fe80::1"),
-				Network: &net.IPNet{
-					IP:   net.ParseIP("fe80::"),
-					Mask: net.CIDRMask(64, 128),
-				},
+				IP:      netip.MustParseAddr("fe80::1"),
+				Network: netip.MustParsePrefix("192.168.1.0/24"),
 			},
 			testIP:   netip.MustParseAddr("fe80::1"),
 			expected: false,
diff --git a/client/firewall/uspfilter/tracer_test.go b/client/firewall/uspfilter/tracer_test.go
index 48b0ec44..46c11578 100644
--- a/client/firewall/uspfilter/tracer_test.go
+++ b/client/firewall/uspfilter/tracer_test.go
@@ -38,11 +38,8 @@ func TestTracePacket(t *testing.T) {
 			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),
-					},
+					IP:      netip.MustParseAddr("100.10.0.100"),
+					Network: netip.MustParsePrefix("100.10.0.0/16"),
 				}
 			},
 		}
@@ -198,12 +195,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 +219,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 +242,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 +260,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 +422,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_bench_test.go b/client/firewall/uspfilter/uspfilter_bench_test.go
index beb5b933..c03e6064 100644
--- a/client/firewall/uspfilter/uspfilter_bench_test.go
+++ b/client/firewall/uspfilter/uspfilter_bench_test.go
@@ -174,11 +174,6 @@ func BenchmarkCoreFiltering(b *testing.B) {
 					require.NoError(b, manager.Close(nil))
 				})
 
-				manager.wgNetwork = &net.IPNet{
-					IP:   net.ParseIP("100.64.0.0"),
-					Mask: net.CIDRMask(10, 32),
-				}
-
 				// Apply scenario-specific setup
 				sc.setupFunc(manager)
 
@@ -219,11 +214,6 @@ func BenchmarkStateScaling(b *testing.B) {
 				require.NoError(b, manager.Close(nil))
 			})
 
-			manager.wgNetwork = &net.IPNet{
-				IP:   net.ParseIP("100.64.0.0"),
-				Mask: net.CIDRMask(10, 32),
-			}
-
 			// Pre-populate connection table
 			srcIPs := generateRandomIPs(count)
 			dstIPs := generateRandomIPs(count)
@@ -267,11 +257,6 @@ func BenchmarkEstablishmentOverhead(b *testing.B) {
 				require.NoError(b, manager.Close(nil))
 			})
 
-			manager.wgNetwork = &net.IPNet{
-				IP:   net.ParseIP("100.64.0.0"),
-				Mask: net.CIDRMask(10, 32),
-			}
-
 			srcIP := generateRandomIPs(1)[0]
 			dstIP := generateRandomIPs(1)[0]
 			outbound := generatePacket(b, srcIP, dstIP, 1024, 80, layers.IPProtocolTCP)
@@ -304,10 +289,6 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) {
 			proto: layers.IPProtocolTCP,
 			state: "new",
 			setupFunc: func(m *Manager) {
-				m.wgNetwork = &net.IPNet{
-					IP:   net.ParseIP("100.64.0.0"),
-					Mask: net.CIDRMask(10, 32),
-				}
 				b.Setenv("NB_DISABLE_CONNTRACK", "1")
 			},
 			genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) {
@@ -321,10 +302,6 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) {
 			proto: layers.IPProtocolTCP,
 			state: "established",
 			setupFunc: func(m *Manager) {
-				m.wgNetwork = &net.IPNet{
-					IP:   net.ParseIP("100.64.0.0"),
-					Mask: net.CIDRMask(10, 32),
-				}
 				b.Setenv("NB_DISABLE_CONNTRACK", "1")
 			},
 			genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) {
@@ -339,10 +316,6 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) {
 			proto: layers.IPProtocolUDP,
 			state: "new",
 			setupFunc: func(m *Manager) {
-				m.wgNetwork = &net.IPNet{
-					IP:   net.ParseIP("100.64.0.0"),
-					Mask: net.CIDRMask(10, 32),
-				}
 				b.Setenv("NB_DISABLE_CONNTRACK", "1")
 			},
 			genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) {
@@ -356,10 +329,6 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) {
 			proto: layers.IPProtocolUDP,
 			state: "established",
 			setupFunc: func(m *Manager) {
-				m.wgNetwork = &net.IPNet{
-					IP:   net.ParseIP("100.64.0.0"),
-					Mask: net.CIDRMask(10, 32),
-				}
 				b.Setenv("NB_DISABLE_CONNTRACK", "1")
 			},
 			genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) {
@@ -373,10 +342,6 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) {
 			proto: layers.IPProtocolTCP,
 			state: "new",
 			setupFunc: func(m *Manager) {
-				m.wgNetwork = &net.IPNet{
-					IP:   net.ParseIP("0.0.0.0"),
-					Mask: net.CIDRMask(0, 32),
-				}
 				require.NoError(b, os.Unsetenv("NB_DISABLE_CONNTRACK"))
 			},
 			genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) {
@@ -390,10 +355,6 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) {
 			proto: layers.IPProtocolTCP,
 			state: "established",
 			setupFunc: func(m *Manager) {
-				m.wgNetwork = &net.IPNet{
-					IP:   net.ParseIP("0.0.0.0"),
-					Mask: net.CIDRMask(0, 32),
-				}
 				require.NoError(b, os.Unsetenv("NB_DISABLE_CONNTRACK"))
 			},
 			genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) {
@@ -408,10 +369,6 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) {
 			proto: layers.IPProtocolTCP,
 			state: "post_handshake",
 			setupFunc: func(m *Manager) {
-				m.wgNetwork = &net.IPNet{
-					IP:   net.ParseIP("0.0.0.0"),
-					Mask: net.CIDRMask(0, 32),
-				}
 				require.NoError(b, os.Unsetenv("NB_DISABLE_CONNTRACK"))
 			},
 			genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) {
@@ -426,10 +383,6 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) {
 			proto: layers.IPProtocolUDP,
 			state: "new",
 			setupFunc: func(m *Manager) {
-				m.wgNetwork = &net.IPNet{
-					IP:   net.ParseIP("0.0.0.0"),
-					Mask: net.CIDRMask(0, 32),
-				}
 				require.NoError(b, os.Unsetenv("NB_DISABLE_CONNTRACK"))
 			},
 			genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) {
@@ -443,10 +396,6 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) {
 			proto: layers.IPProtocolUDP,
 			state: "established",
 			setupFunc: func(m *Manager) {
-				m.wgNetwork = &net.IPNet{
-					IP:   net.ParseIP("0.0.0.0"),
-					Mask: net.CIDRMask(0, 32),
-				}
 				require.NoError(b, os.Unsetenv("NB_DISABLE_CONNTRACK"))
 			},
 			genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) {
@@ -593,11 +542,6 @@ func BenchmarkLongLivedConnections(b *testing.B) {
 				require.NoError(b, manager.Close(nil))
 			})
 
-			manager.SetNetwork(&net.IPNet{
-				IP:   net.ParseIP("100.64.0.0"),
-				Mask: net.CIDRMask(10, 32),
-			})
-
 			// Setup initial state based on scenario
 			if sc.rules {
 				// Single rule to allow all return traffic from port 80
@@ -681,11 +625,6 @@ func BenchmarkShortLivedConnections(b *testing.B) {
 				require.NoError(b, manager.Close(nil))
 			})
 
-			manager.SetNetwork(&net.IPNet{
-				IP:   net.ParseIP("100.64.0.0"),
-				Mask: net.CIDRMask(10, 32),
-			})
-
 			// Setup initial state based on scenario
 			if sc.rules {
 				// Single rule to allow all return traffic from port 80
@@ -797,11 +736,6 @@ func BenchmarkParallelLongLivedConnections(b *testing.B) {
 				require.NoError(b, manager.Close(nil))
 			})
 
-			manager.SetNetwork(&net.IPNet{
-				IP:   net.ParseIP("100.64.0.0"),
-				Mask: net.CIDRMask(10, 32),
-			})
-
 			// Setup initial state based on scenario
 			if sc.rules {
 				_, err := manager.AddPeerFiltering(nil, net.ParseIP("0.0.0.0"), fw.ProtocolTCP, &fw.Port{Values: []uint16{80}}, nil, fw.ActionAccept, "")
@@ -882,11 +816,6 @@ func BenchmarkParallelShortLivedConnections(b *testing.B) {
 				require.NoError(b, manager.Close(nil))
 			})
 
-			manager.SetNetwork(&net.IPNet{
-				IP:   net.ParseIP("100.64.0.0"),
-				Mask: net.CIDRMask(10, 32),
-			})
-
 			if sc.rules {
 				_, err := manager.AddPeerFiltering(nil, net.ParseIP("0.0.0.0"), fw.ProtocolTCP, &fw.Port{Values: []uint16{80}}, nil, fw.ActionAccept, "")
 				require.NoError(b, err)
@@ -1032,7 +961,8 @@ func BenchmarkRouteACLs(b *testing.B) {
 	}
 
 	for _, r := range rules {
-		_, err := manager.AddRouteFiltering(nil, r.sources, r.dest, r.proto, nil, r.port, fw.ActionAccept)
+		dst := fw.Network{Prefix: r.dest}
+		_, err := manager.AddRouteFiltering(nil, r.sources, dst, r.proto, nil, r.port, fw.ActionAccept)
 		if err != nil {
 			b.Fatal(err)
 		}
diff --git a/client/firewall/uspfilter/uspfilter_filter_test.go b/client/firewall/uspfilter/uspfilter_filter_test.go
index ba97c264..318f86a8 100644
--- a/client/firewall/uspfilter/uspfilter_filter_test.go
+++ b/client/firewall/uspfilter/uspfilter_filter_test.go
@@ -15,15 +15,12 @@ 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) {
-	localIP := net.ParseIP("100.10.0.100")
-	wgNet := &net.IPNet{
-		IP:   net.ParseIP("100.10.0.0"),
-		Mask: net.CIDRMask(16, 32),
-	}
-
+	localIP := netip.MustParseAddr("100.10.0.100")
+	wgNet := netip.MustParsePrefix("100.10.0.0/16")
 	ifaceMock := &IFaceMock{
 		SetFilterFunc: func(device.PacketFilter) error { return nil },
 		AddressFunc: func() wgaddr.Address {
@@ -42,8 +39,6 @@ func TestPeerACLFiltering(t *testing.T) {
 		require.NoError(t, manager.Close(nil))
 	})
 
-	manager.wgNetwork = wgNet
-
 	err = manager.UpdateLocalIPs()
 	require.NoError(t, err)
 
@@ -188,6 +183,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 +468,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),
@@ -283,14 +575,13 @@ func setupRoutedManager(tb testing.TB, network string) *Manager {
 	dev := mocks.NewMockDevice(ctrl)
 	dev.EXPECT().MTU().Return(1500, nil).AnyTimes()
 
-	localIP, wgNet, err := net.ParseCIDR(network)
-	require.NoError(tb, err)
+	wgNet := netip.MustParsePrefix(network)
 
 	ifaceMock := &IFaceMock{
 		SetFilterFunc: func(device.PacketFilter) error { return nil },
 		AddressFunc: func() wgaddr.Address {
 			return wgaddr.Address{
-				IP:      localIP,
+				IP:      wgNet.Addr(),
 				Network: wgNet,
 			}
 		},
@@ -303,8 +594,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 +612,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 +638,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 +654,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 +670,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 +686,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 +700,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 +715,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 +731,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 +747,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 +763,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 +779,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 +798,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 +812,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 +827,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 +843,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 +859,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 +875,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 +892,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 +911,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 +930,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 +949,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 +971,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 +990,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 +1009,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 +1028,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 +1044,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 +1062,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 +1248,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 +1269,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 +1278,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 +1286,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 +1324,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 +1333,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 +1348,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 +1427,50 @@ 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:      netip.MustParseAddr("100.10.0.100"),
+				Network: netip.MustParsePrefix("100.10.0.0/16"),
+			}
+		},
+	}
+
+	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 a48a483f..88de1ddc 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())
@@ -270,11 +271,8 @@ func TestNotMatchByIP(t *testing.T) {
 		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),
-				},
+				IP:      netip.MustParseAddr("100.10.0.100"),
+				Network: netip.MustParsePrefix("100.10.0.0/16"),
 			}
 		},
 	}
@@ -284,10 +282,6 @@ func TestNotMatchByIP(t *testing.T) {
 		t.Errorf("failed to create Manager: %v", err)
 		return
 	}
-	m.wgNetwork = &net.IPNet{
-		IP:   net.ParseIP("100.10.0.0"),
-		Mask: net.CIDRMask(16, 32),
-	}
 
 	ip := net.ParseIP("0.0.0.0")
 	proto := fw.ProtocolUDP
@@ -395,10 +389,6 @@ func TestProcessOutgoingHooks(t *testing.T) {
 	}, false, flowLogger)
 	require.NoError(t, err)
 
-	manager.wgNetwork = &net.IPNet{
-		IP:   net.ParseIP("100.10.0.0"),
-		Mask: net.CIDRMask(16, 32),
-	}
 	manager.udpTracker.Close()
 	manager.udpTracker = conntrack.NewUDPTracker(100*time.Millisecond, logger, flowLogger)
 	defer func() {
@@ -508,11 +498,6 @@ func TestStatefulFirewall_UDPTracking(t *testing.T) {
 	}, false, flowLogger)
 	require.NoError(t, err)
 
-	manager.wgNetwork = &net.IPNet{
-		IP:   net.ParseIP("100.10.0.0"),
-		Mask: net.CIDRMask(16, 32),
-	}
-
 	manager.udpTracker.Close() // Close the existing tracker
 	manager.udpTracker = conntrack.NewUDPTracker(200*time.Millisecond, logger, flowLogger)
 	manager.decoders = sync.Pool{
@@ -711,3 +696,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/configurer/usp_test.go b/client/iface/configurer/usp_test.go
index 775339f2..e32491c5 100644
--- a/client/iface/configurer/usp_test.go
+++ b/client/iface/configurer/usp_test.go
@@ -2,10 +2,8 @@ package configurer
 
 import (
 	"encoding/hex"
-	"fmt"
 	"testing"
 
-	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
 )
@@ -34,58 +32,35 @@ errno=0
 
 `
 
-func Test_findPeerInfo(t *testing.T) {
+func Test_parseTransfers(t *testing.T) {
 	tests := []struct {
-		name       string
-		peerKey    string
-		searchKeys []string
-		want       map[string]string
-		wantErr    bool
+		name    string
+		peerKey string
+		want    WGStats
 	}{
 		{
-			name:       "single",
-			peerKey:    "58402e695ba1772b1cc9309755f043251ea77fdcf10fbe63989ceb7e19321376",
-			searchKeys: []string{"tx_bytes"},
-			want: map[string]string{
-				"tx_bytes": "38333",
+			name:    "single",
+			peerKey: "b85996fecc9c7f1fc6d2572a76eda11d59bcd20be8e543b15ce4bd85a8e75a33",
+			want: WGStats{
+				TxBytes: 0,
+				RxBytes: 0,
 			},
-			wantErr: false,
 		},
 		{
-			name:       "multiple",
-			peerKey:    "58402e695ba1772b1cc9309755f043251ea77fdcf10fbe63989ceb7e19321376",
-			searchKeys: []string{"tx_bytes", "rx_bytes"},
-			want: map[string]string{
-				"tx_bytes": "38333",
-				"rx_bytes": "2224",
+			name:    "multiple",
+			peerKey: "58402e695ba1772b1cc9309755f043251ea77fdcf10fbe63989ceb7e19321376",
+			want: WGStats{
+				TxBytes: 38333,
+				RxBytes: 2224,
 			},
-			wantErr: false,
 		},
 		{
-			name:       "lastpeer",
-			peerKey:    "662e14fd594556f522604703340351258903b64f35553763f19426ab2a515c58",
-			searchKeys: []string{"tx_bytes", "rx_bytes"},
-			want: map[string]string{
-				"tx_bytes": "1212111",
-				"rx_bytes": "1929999999",
+			name:    "lastpeer",
+			peerKey: "662e14fd594556f522604703340351258903b64f35553763f19426ab2a515c58",
+			want: WGStats{
+				TxBytes: 1212111,
+				RxBytes: 1929999999,
 			},
-			wantErr: false,
-		},
-		{
-			name:       "peer not found",
-			peerKey:    "1111111111111111111111111111111111111111111111111111111111111111",
-			searchKeys: nil,
-			want:       nil,
-			wantErr:    true,
-		},
-		{
-			name:       "key not found",
-			peerKey:    "662e14fd594556f522604703340351258903b64f35553763f19426ab2a515c58",
-			searchKeys: []string{"tx_bytes", "unknown_key"},
-			want: map[string]string{
-				"tx_bytes": "1212111",
-			},
-			wantErr: true,
 		},
 	}
 	for _, tt := range tests {
@@ -96,9 +71,19 @@ func Test_findPeerInfo(t *testing.T) {
 			key, err := wgtypes.NewKey(res)
 			require.NoError(t, err)
 
-			got, err := findPeerInfo(ipcFixture, key.String(), tt.searchKeys)
-			assert.Equalf(t, tt.wantErr, err != nil, fmt.Sprintf("findPeerInfo(%v, %v, %v)", ipcFixture, key.String(), tt.searchKeys))
-			assert.Equalf(t, tt.want, got, "findPeerInfo(%v, %v, %v)", ipcFixture, key.String(), tt.searchKeys)
+			stats, err := parseTransfers(ipcFixture)
+			if err != nil {
+				require.NoError(t, err)
+				return
+			}
+
+			stat, ok := stats[key.String()]
+			if !ok {
+				require.True(t, ok)
+				return
+			}
+
+			require.Equal(t, tt.want, stat)
 		})
 	}
 }
diff --git a/client/internal/acl/manager_test.go b/client/internal/acl/manager_test.go
index 9488d33a..16620033 100644
--- a/client/internal/acl/manager_test.go
+++ b/client/internal/acl/manager_test.go
@@ -1,13 +1,14 @@
 package acl
 
 import (
-	"net"
+	"net/netip"
 	"testing"
 
 	"github.com/golang/mock/gomock"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 
 	"github.com/netbirdio/netbird/client/firewall"
-	"github.com/netbirdio/netbird/client/firewall/manager"
 	"github.com/netbirdio/netbird/client/iface/wgaddr"
 	"github.com/netbirdio/netbird/client/internal/acl/mocks"
 	"github.com/netbirdio/netbird/client/internal/netflow"
@@ -42,35 +43,31 @@ func TestDefaultManager(t *testing.T) {
 	ifaceMock := mocks.NewMockIFaceMapper(ctrl)
 	ifaceMock.EXPECT().IsUserspaceBind().Return(true).AnyTimes()
 	ifaceMock.EXPECT().SetFilter(gomock.Any())
-	ip, network, err := net.ParseCIDR("172.0.0.1/32")
-	if err != nil {
-		t.Fatalf("failed to parse IP address: %v", err)
-	}
+	network := netip.MustParsePrefix("172.0.0.1/32")
 
 	ifaceMock.EXPECT().Name().Return("lo").AnyTimes()
 	ifaceMock.EXPECT().Address().Return(wgaddr.Address{
-		IP:      ip,
+		IP:      network.Addr(),
 		Network: network,
 	}).AnyTimes()
 	ifaceMock.EXPECT().GetWGDevice().Return(nil).AnyTimes()
 
-	// we receive one rule from the management so for testing purposes ignore it
 	fw, err := firewall.NewFirewall(ifaceMock, nil, flowLogger, false)
-	if err != nil {
-		t.Errorf("create firewall: %v", err)
-		return
-	}
-	defer func(fw manager.Manager) {
-		_ = fw.Close(nil)
-	}(fw)
+	require.NoError(t, err)
+	defer func() {
+		err = fw.Close(nil)
+		require.NoError(t, err)
+	}()
+
 	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)
-			return
+		if fw.IsStateful() {
+			assert.Equal(t, 0, len(acl.peerRulesPairs))
+		} else {
+			assert.Equal(t, 2, len(acl.peerRulesPairs))
 		}
 	})
 
@@ -92,14 +89,15 @@ 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 {
-			t.Errorf("firewall rules not applied")
-			return
+		expectedRules := 2
+		if fw.IsStateful() {
+			expectedRules = 1 // only the inbound rule
 		}
 
+		assert.Equal(t, expectedRules, len(acl.peerRulesPairs))
+
 		// check that old rule was removed
 		previousCount := 0
 		for id := range acl.peerRulesPairs {
@@ -107,26 +105,86 @@ func TestDefaultManager(t *testing.T) {
 				previousCount++
 			}
 		}
-		if previousCount != 1 {
-			t.Errorf("old rule was not removed")
+
+		expectedPreviousCount := 0
+		if !fw.IsStateful() {
+			expectedPreviousCount = 1
 		}
+		assert.Equal(t, expectedPreviousCount, previousCount)
 	})
 
 	t.Run("handle default rules", func(t *testing.T) {
 		networkMap.FirewallRules = networkMap.FirewallRules[:0]
 
 		networkMap.FirewallRulesIsEmpty = true
-		if acl.ApplyFiltering(networkMap); len(acl.peerRulesPairs) != 0 {
-			t.Errorf("rules should be empty if FirewallRulesIsEmpty is set, got: %v", len(acl.peerRulesPairs))
-			return
-		}
+		acl.ApplyFiltering(networkMap, false)
+		assert.Equal(t, 0, len(acl.peerRulesPairs))
 
 		networkMap.FirewallRulesIsEmpty = false
-		acl.ApplyFiltering(networkMap)
-		if len(acl.peerRulesPairs) != 1 {
-			t.Errorf("rules should contain 1 rules if FirewallRulesIsEmpty is not set, got: %v", len(acl.peerRulesPairs))
-			return
+		acl.ApplyFiltering(networkMap, false)
+
+		expectedRules := 1
+		if fw.IsStateful() {
+			expectedRules = 1 // only inbound allow-all rule
 		}
+		assert.Equal(t, expectedRules, len(acl.peerRulesPairs))
+	})
+}
+
+func TestDefaultManagerStateless(t *testing.T) {
+	// stateless currently only in userspace, so we have to disable kernel
+	t.Setenv("NB_WG_KERNEL_DISABLED", "true")
+	t.Setenv("NB_DISABLE_CONNTRACK", "true")
+
+	networkMap := &mgmProto.NetworkMap{
+		FirewallRules: []*mgmProto.FirewallRule{
+			{
+				PeerIP:    "10.93.0.1",
+				Direction: mgmProto.RuleDirection_OUT,
+				Action:    mgmProto.RuleAction_ACCEPT,
+				Protocol:  mgmProto.RuleProtocol_TCP,
+				Port:      "80",
+			},
+			{
+				PeerIP:    "10.93.0.2",
+				Direction: mgmProto.RuleDirection_IN,
+				Action:    mgmProto.RuleAction_ACCEPT,
+				Protocol:  mgmProto.RuleProtocol_UDP,
+				Port:      "53",
+			},
+		},
+	}
+
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	ifaceMock := mocks.NewMockIFaceMapper(ctrl)
+	ifaceMock.EXPECT().IsUserspaceBind().Return(true).AnyTimes()
+	ifaceMock.EXPECT().SetFilter(gomock.Any())
+	network := netip.MustParsePrefix("172.0.0.1/32")
+
+	ifaceMock.EXPECT().Name().Return("lo").AnyTimes()
+	ifaceMock.EXPECT().Address().Return(wgaddr.Address{
+		IP:      network.Addr(),
+		Network: network,
+	}).AnyTimes()
+	ifaceMock.EXPECT().GetWGDevice().Return(nil).AnyTimes()
+
+	fw, err := firewall.NewFirewall(ifaceMock, nil, flowLogger, false)
+	require.NoError(t, err)
+	defer func() {
+		err = fw.Close(nil)
+		require.NoError(t, err)
+	}()
+
+	acl := NewDefaultManager(fw)
+
+	t.Run("stateless firewall creates outbound rules", func(t *testing.T) {
+		acl.ApplyFiltering(networkMap, false)
+
+		// In stateless mode, we should have both inbound and outbound rules
+		assert.False(t, fw.IsStateful())
+		assert.Equal(t, 2, len(acl.peerRulesPairs))
 	})
 }
 
@@ -192,42 +250,19 @@ func TestDefaultManagerSquashRules(t *testing.T) {
 
 	manager := &DefaultManager{}
 	rules, _ := manager.squashAcceptRules(networkMap)
-	if len(rules) != 2 {
-		t.Errorf("rules should contain 2, got: %v", rules)
-		return
-	}
+	assert.Equal(t, 2, len(rules))
 
 	r := rules[0]
-	switch {
-	case r.PeerIP != "0.0.0.0":
-		t.Errorf("IP should be 0.0.0.0, got: %v", r.PeerIP)
-		return
-	case r.Direction != mgmProto.RuleDirection_IN:
-		t.Errorf("direction should be IN, got: %v", r.Direction)
-		return
-	case r.Protocol != mgmProto.RuleProtocol_ALL:
-		t.Errorf("protocol should be ALL, got: %v", r.Protocol)
-		return
-	case r.Action != mgmProto.RuleAction_ACCEPT:
-		t.Errorf("action should be ACCEPT, got: %v", r.Action)
-		return
-	}
+	assert.Equal(t, "0.0.0.0", r.PeerIP)
+	assert.Equal(t, mgmProto.RuleDirection_IN, r.Direction)
+	assert.Equal(t, mgmProto.RuleProtocol_ALL, r.Protocol)
+	assert.Equal(t, mgmProto.RuleAction_ACCEPT, r.Action)
 
 	r = rules[1]
-	switch {
-	case r.PeerIP != "0.0.0.0":
-		t.Errorf("IP should be 0.0.0.0, got: %v", r.PeerIP)
-		return
-	case r.Direction != mgmProto.RuleDirection_OUT:
-		t.Errorf("direction should be OUT, got: %v", r.Direction)
-		return
-	case r.Protocol != mgmProto.RuleProtocol_ALL:
-		t.Errorf("protocol should be ALL, got: %v", r.Protocol)
-		return
-	case r.Action != mgmProto.RuleAction_ACCEPT:
-		t.Errorf("action should be ACCEPT, got: %v", r.Action)
-		return
-	}
+	assert.Equal(t, "0.0.0.0", r.PeerIP)
+	assert.Equal(t, mgmProto.RuleDirection_OUT, r.Direction)
+	assert.Equal(t, mgmProto.RuleProtocol_ALL, r.Protocol)
+	assert.Equal(t, mgmProto.RuleAction_ACCEPT, r.Action)
 }
 
 func TestDefaultManagerSquashRulesNoAffect(t *testing.T) {
@@ -291,9 +326,8 @@ func TestDefaultManagerSquashRulesNoAffect(t *testing.T) {
 	}
 
 	manager := &DefaultManager{}
-	if rules, _ := manager.squashAcceptRules(networkMap); len(rules) != len(networkMap.FirewallRules) {
-		t.Errorf("we should get the same amount of rules as output, got %v", len(rules))
-	}
+	rules, _ := manager.squashAcceptRules(networkMap)
+	assert.Equal(t, len(networkMap.FirewallRules), len(rules))
 }
 
 func TestDefaultManagerEnableSSHRules(t *testing.T) {
@@ -336,33 +370,29 @@ func TestDefaultManagerEnableSSHRules(t *testing.T) {
 	ifaceMock := mocks.NewMockIFaceMapper(ctrl)
 	ifaceMock.EXPECT().IsUserspaceBind().Return(true).AnyTimes()
 	ifaceMock.EXPECT().SetFilter(gomock.Any())
-	ip, network, err := net.ParseCIDR("172.0.0.1/32")
-	if err != nil {
-		t.Fatalf("failed to parse IP address: %v", err)
-	}
+	network := netip.MustParsePrefix("172.0.0.1/32")
 
 	ifaceMock.EXPECT().Name().Return("lo").AnyTimes()
 	ifaceMock.EXPECT().Address().Return(wgaddr.Address{
-		IP:      ip,
+		IP:      network.Addr(),
 		Network: network,
 	}).AnyTimes()
 	ifaceMock.EXPECT().GetWGDevice().Return(nil).AnyTimes()
 
-	// we receive one rule from the management so for testing purposes ignore it
 	fw, err := firewall.NewFirewall(ifaceMock, nil, flowLogger, false)
-	if err != nil {
-		t.Errorf("create firewall: %v", err)
-		return
-	}
-	defer func(fw manager.Manager) {
-		_ = fw.Close(nil)
-	}(fw)
+	require.NoError(t, err)
+	defer func() {
+		err = fw.Close(nil)
+		require.NoError(t, err)
+	}()
+
 	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))
-		return
+	expectedRules := 3
+	if fw.IsStateful() {
+		expectedRules = 3 // 2 inbound rules + SSH rule
 	}
+	assert.Equal(t, expectedRules, len(acl.peerRulesPairs))
 }
diff --git a/client/internal/auth/pkce_flow_test.go b/client/internal/auth/pkce_flow_test.go
index 4510ed33..f42cce51 100644
--- a/client/internal/auth/pkce_flow_test.go
+++ b/client/internal/auth/pkce_flow_test.go
@@ -7,15 +7,36 @@ import (
 	"github.com/stretchr/testify/require"
 
 	"github.com/netbirdio/netbird/client/internal"
+	mgm "github.com/netbirdio/netbird/management/client/common"
 )
 
 func TestPromptLogin(t *testing.T) {
+	const (
+		promptLogin = "prompt=login"
+		maxAge0     = "max_age=0"
+	)
+
 	tt := []struct {
-		name   string
-		prompt bool
+		name               string
+		loginFlag          mgm.LoginFlag
+		disablePromptLogin bool
+		expect             string
 	}{
-		{"PromptLogin", true},
-		{"NoPromptLogin", false},
+		{
+			name:      "Prompt login",
+			loginFlag: mgm.LoginFlagPrompt,
+			expect:    promptLogin,
+		},
+		{
+			name:      "Max age 0 login",
+			loginFlag: mgm.LoginFlagMaxAge0,
+			expect:    maxAge0,
+		},
+		{
+			name:               "Disable prompt login",
+			loginFlag:          mgm.LoginFlagPrompt,
+			disablePromptLogin: true,
+		},
 	}
 
 	for _, tc := range tt {
@@ -28,7 +49,7 @@ func TestPromptLogin(t *testing.T) {
 				AuthorizationEndpoint: "https://test-auth-endpoint.com/authorize",
 				RedirectURLs:          []string{"http://127.0.0.1:33992/"},
 				UseIDToken:            true,
-				DisablePromptLogin:    !tc.prompt,
+				LoginFlag:             tc.loginFlag,
 			}
 			pkce, err := NewPKCEAuthorizationFlow(config)
 			if err != nil {
@@ -38,11 +59,12 @@ func TestPromptLogin(t *testing.T) {
 			if err != nil {
 				t.Fatalf("Failed to request auth info: %v", err)
 			}
-			pattern := "prompt=login"
-			if tc.prompt {
-				require.Contains(t, authInfo.VerificationURIComplete, pattern)
+
+			if !tc.disablePromptLogin {
+				require.Contains(t, authInfo.VerificationURIComplete, tc.expect)
 			} else {
-				require.NotContains(t, authInfo.VerificationURIComplete, pattern)
+				require.Contains(t, authInfo.VerificationURIComplete, promptLogin)
+				require.NotContains(t, authInfo.VerificationURIComplete, maxAge0)
 			}
 		})
 	}
diff --git a/client/internal/dns/handler_chain_test.go b/client/internal/dns/handler_chain_test.go
index 4c910a95..5f03e075 100644
--- a/client/internal/dns/handler_chain_test.go
+++ b/client/internal/dns/handler_chain_test.go
@@ -1,7 +1,6 @@
 package dns_test
 
 import (
-	"net"
 	"testing"
 
 	"github.com/miekg/dns"
@@ -9,6 +8,7 @@ import (
 	"github.com/stretchr/testify/mock"
 
 	nbdns "github.com/netbirdio/netbird/client/internal/dns"
+	"github.com/netbirdio/netbird/client/internal/dns/test"
 )
 
 // TestHandlerChain_ServeDNS_Priorities tests that handlers are executed in priority order
@@ -30,7 +30,7 @@ func TestHandlerChain_ServeDNS_Priorities(t *testing.T) {
 	r.SetQuestion("example.com.", dns.TypeA)
 
 	// Create test writer
-	w := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
+	w := &nbdns.ResponseWriterChain{ResponseWriter: &test.MockResponseWriter{}}
 
 	// Setup expectations - only highest priority handler should be called
 	dnsRouteHandler.On("ServeDNS", mock.Anything, r).Once()
@@ -142,7 +142,7 @@ func TestHandlerChain_ServeDNS_DomainMatching(t *testing.T) {
 
 			r := new(dns.Msg)
 			r.SetQuestion(tt.queryDomain, dns.TypeA)
-			w := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
+			w := &nbdns.ResponseWriterChain{ResponseWriter: &test.MockResponseWriter{}}
 
 			chain.ServeDNS(w, r)
 
@@ -259,7 +259,7 @@ func TestHandlerChain_ServeDNS_OverlappingDomains(t *testing.T) {
 			// Create and execute request
 			r := new(dns.Msg)
 			r.SetQuestion(tt.queryDomain, dns.TypeA)
-			w := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
+			w := &nbdns.ResponseWriterChain{ResponseWriter: &test.MockResponseWriter{}}
 			chain.ServeDNS(w, r)
 
 			// Verify expectations
@@ -316,7 +316,7 @@ func TestHandlerChain_ServeDNS_ChainContinuation(t *testing.T) {
 	}).Once()
 
 	// Execute
-	w := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
+	w := &nbdns.ResponseWriterChain{ResponseWriter: &test.MockResponseWriter{}}
 	chain.ServeDNS(w, r)
 
 	// Verify all handlers were called in order
@@ -325,20 +325,6 @@ func TestHandlerChain_ServeDNS_ChainContinuation(t *testing.T) {
 	handler3.AssertExpectations(t)
 }
 
-// mockResponseWriter implements dns.ResponseWriter for testing
-type mockResponseWriter struct {
-	mock.Mock
-}
-
-func (m *mockResponseWriter) LocalAddr() net.Addr       { return nil }
-func (m *mockResponseWriter) RemoteAddr() net.Addr      { return nil }
-func (m *mockResponseWriter) WriteMsg(*dns.Msg) error   { return nil }
-func (m *mockResponseWriter) Write([]byte) (int, error) { return 0, nil }
-func (m *mockResponseWriter) Close() error              { return nil }
-func (m *mockResponseWriter) TsigStatus() error         { return nil }
-func (m *mockResponseWriter) TsigTimersOnly(bool)       {}
-func (m *mockResponseWriter) Hijack()                   {}
-
 func TestHandlerChain_PriorityDeregistration(t *testing.T) {
 	tests := []struct {
 		name string
@@ -425,7 +411,7 @@ func TestHandlerChain_PriorityDeregistration(t *testing.T) {
 			// Create test request
 			r := new(dns.Msg)
 			r.SetQuestion(tt.query, dns.TypeA)
-			w := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
+			w := &nbdns.ResponseWriterChain{ResponseWriter: &test.MockResponseWriter{}}
 
 			// Setup expectations
 			for priority, handler := range handlers {
@@ -471,7 +457,7 @@ func TestHandlerChain_MultiPriorityHandling(t *testing.T) {
 	chain.AddHandler(testDomain, matchHandler, nbdns.PriorityMatchDomain)
 
 	// Test 1: Initial state
-	w1 := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
+	w1 := &nbdns.ResponseWriterChain{ResponseWriter: &test.MockResponseWriter{}}
 	// Highest priority handler (routeHandler) should be called
 	routeHandler.On("ServeDNS", mock.Anything, r).Return().Once()
 	matchHandler.On("ServeDNS", mock.Anything, r).Maybe()   // Ensure others are not expected yet
@@ -490,7 +476,7 @@ func TestHandlerChain_MultiPriorityHandling(t *testing.T) {
 	// Test 2: Remove highest priority handler
 	chain.RemoveHandler(testDomain, nbdns.PriorityDNSRoute)
 
-	w2 := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
+	w2 := &nbdns.ResponseWriterChain{ResponseWriter: &test.MockResponseWriter{}}
 	// Now middle priority handler (matchHandler) should be called
 	matchHandler.On("ServeDNS", mock.Anything, r).Return().Once()
 	defaultHandler.On("ServeDNS", mock.Anything, r).Maybe() // Ensure default is not expected yet
@@ -506,7 +492,7 @@ func TestHandlerChain_MultiPriorityHandling(t *testing.T) {
 	// Test 3: Remove middle priority handler
 	chain.RemoveHandler(testDomain, nbdns.PriorityMatchDomain)
 
-	w3 := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
+	w3 := &nbdns.ResponseWriterChain{ResponseWriter: &test.MockResponseWriter{}}
 	// Now lowest priority handler (defaultHandler) should be called
 	defaultHandler.On("ServeDNS", mock.Anything, r).Return().Once()
 
@@ -519,7 +505,7 @@ func TestHandlerChain_MultiPriorityHandling(t *testing.T) {
 	// Test 4: Remove last handler
 	chain.RemoveHandler(testDomain, nbdns.PriorityDefault)
 
-	w4 := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
+	w4 := &nbdns.ResponseWriterChain{ResponseWriter: &test.MockResponseWriter{}}
 	chain.ServeDNS(w4, r) // Call ServeDNS on the now empty chain for this domain
 
 	for _, m := range mocks {
@@ -675,7 +661,7 @@ func TestHandlerChain_CaseSensitivity(t *testing.T) {
 			// Execute request
 			r := new(dns.Msg)
 			r.SetQuestion(tt.query, dns.TypeA)
-			chain.ServeDNS(&mockResponseWriter{}, r)
+			chain.ServeDNS(&test.MockResponseWriter{}, r)
 
 			// Verify each handler was called exactly as expected
 			for _, h := range tt.addHandlers {
@@ -819,7 +805,7 @@ func TestHandlerChain_DomainSpecificityOrdering(t *testing.T) {
 
 			r := new(dns.Msg)
 			r.SetQuestion(tt.query, dns.TypeA)
-			w := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
+			w := &nbdns.ResponseWriterChain{ResponseWriter: &test.MockResponseWriter{}}
 
 			// Setup handler expectations
 			for pattern, handler := range handlers {
@@ -969,7 +955,7 @@ func TestHandlerChain_AddRemoveRoundtrip(t *testing.T) {
 			handler := &nbdns.MockHandler{}
 			r := new(dns.Msg)
 			r.SetQuestion(tt.queryPattern, dns.TypeA)
-			w := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
+			w := &nbdns.ResponseWriterChain{ResponseWriter: &test.MockResponseWriter{}}
 
 			// First verify no handler is called before adding any
 			chain.ServeDNS(w, r)
diff --git a/client/internal/dns/local/local_test.go b/client/internal/dns/local/local_test.go
new file mode 100644
index 00000000..1d38191e
--- /dev/null
+++ b/client/internal/dns/local/local_test.go
@@ -0,0 +1,472 @@
+package local
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/miekg/dns"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/netbirdio/netbird/client/internal/dns/test"
+	nbdns "github.com/netbirdio/netbird/dns"
+)
+
+func TestLocalResolver_ServeDNS(t *testing.T) {
+	recordA := nbdns.SimpleRecord{
+		Name:  "peera.netbird.cloud.",
+		Type:  1,
+		Class: nbdns.DefaultClass,
+		TTL:   300,
+		RData: "1.2.3.4",
+	}
+
+	recordCNAME := nbdns.SimpleRecord{
+		Name:  "peerb.netbird.cloud.",
+		Type:  5,
+		Class: nbdns.DefaultClass,
+		TTL:   300,
+		RData: "www.netbird.io",
+	}
+
+	testCases := []struct {
+		name                string
+		inputRecord         nbdns.SimpleRecord
+		inputMSG            *dns.Msg
+		responseShouldBeNil bool
+	}{
+		{
+			name:        "Should Resolve A Record",
+			inputRecord: recordA,
+			inputMSG:    new(dns.Msg).SetQuestion(recordA.Name, dns.TypeA),
+		},
+		{
+			name:        "Should Resolve CNAME Record",
+			inputRecord: recordCNAME,
+			inputMSG:    new(dns.Msg).SetQuestion(recordCNAME.Name, dns.TypeCNAME),
+		},
+		{
+			name:                "Should Not Write When Not Found A Record",
+			inputRecord:         recordA,
+			inputMSG:            new(dns.Msg).SetQuestion("not.found.com", dns.TypeA),
+			responseShouldBeNil: true,
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+			resolver := NewResolver()
+			_ = resolver.RegisterRecord(testCase.inputRecord)
+			var responseMSG *dns.Msg
+			responseWriter := &test.MockResponseWriter{
+				WriteMsgFunc: func(m *dns.Msg) error {
+					responseMSG = m
+					return nil
+				},
+			}
+
+			resolver.ServeDNS(responseWriter, testCase.inputMSG)
+
+			if responseMSG == nil || len(responseMSG.Answer) == 0 {
+				if testCase.responseShouldBeNil {
+					return
+				}
+				t.Fatalf("should write a response message")
+			}
+
+			answerString := responseMSG.Answer[0].String()
+			if !strings.Contains(answerString, testCase.inputRecord.Name) {
+				t.Fatalf("answer doesn't contain the same domain name: \nWant: %s\nGot:%s", testCase.name, answerString)
+			}
+			if !strings.Contains(answerString, dns.Type(testCase.inputRecord.Type).String()) {
+				t.Fatalf("answer doesn't contain the correct type: \nWant: %s\nGot:%s", dns.Type(testCase.inputRecord.Type).String(), answerString)
+			}
+			if !strings.Contains(answerString, testCase.inputRecord.RData) {
+				t.Fatalf("answer doesn't contain the same address: \nWant: %s\nGot:%s", testCase.inputRecord.RData, answerString)
+			}
+		})
+	}
+}
+
+// TestLocalResolver_Update_StaleRecord verifies that updating
+// a record correctly replaces the old one, preventing stale entries.
+func TestLocalResolver_Update_StaleRecord(t *testing.T) {
+	recordName := "host.example.com."
+	recordType := dns.TypeA
+	recordClass := dns.ClassINET
+
+	record1 := nbdns.SimpleRecord{
+		Name: recordName, Type: int(recordType), Class: nbdns.DefaultClass, TTL: 300, RData: "1.1.1.1",
+	}
+	record2 := nbdns.SimpleRecord{
+		Name: recordName, Type: int(recordType), Class: nbdns.DefaultClass, TTL: 300, RData: "2.2.2.2",
+	}
+
+	recordKey := dns.Question{Name: recordName, Qtype: uint16(recordClass), Qclass: recordType}
+
+	resolver := NewResolver()
+
+	update1 := []nbdns.SimpleRecord{record1}
+	update2 := []nbdns.SimpleRecord{record2}
+
+	// Apply first update
+	resolver.Update(update1)
+
+	// Verify first update
+	resolver.mu.RLock()
+	rrSlice1, found1 := resolver.records[recordKey]
+	resolver.mu.RUnlock()
+
+	require.True(t, found1, "Record key %s not found after first update", recordKey)
+	require.Len(t, rrSlice1, 1, "Should have exactly 1 record after first update")
+	assert.Contains(t, rrSlice1[0].String(), record1.RData, "Record after first update should be %s", record1.RData)
+
+	// Apply second update
+	resolver.Update(update2)
+
+	// Verify second update
+	resolver.mu.RLock()
+	rrSlice2, found2 := resolver.records[recordKey]
+	resolver.mu.RUnlock()
+
+	require.True(t, found2, "Record key %s not found after second update", recordKey)
+	require.Len(t, rrSlice2, 1, "Should have exactly 1 record after update overwriting the key")
+	assert.Contains(t, rrSlice2[0].String(), record2.RData, "The single record should be the updated one (%s)", record2.RData)
+	assert.NotContains(t, rrSlice2[0].String(), record1.RData, "The stale record (%s) should not be present", record1.RData)
+}
+
+// TestLocalResolver_MultipleRecords_SameQuestion verifies that multiple records
+// with the same question are stored properly
+func TestLocalResolver_MultipleRecords_SameQuestion(t *testing.T) {
+	resolver := NewResolver()
+
+	recordName := "multi.example.com."
+	recordType := dns.TypeA
+
+	// Create two records with the same name and type but different IPs
+	record1 := nbdns.SimpleRecord{
+		Name: recordName, Type: int(recordType), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1",
+	}
+	record2 := nbdns.SimpleRecord{
+		Name: recordName, Type: int(recordType), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.2",
+	}
+
+	update := []nbdns.SimpleRecord{record1, record2}
+
+	// Apply update with both records
+	resolver.Update(update)
+
+	// Create question that matches both records
+	question := dns.Question{
+		Name:   recordName,
+		Qtype:  recordType,
+		Qclass: dns.ClassINET,
+	}
+
+	// Verify both records are stored
+	resolver.mu.RLock()
+	records, found := resolver.records[question]
+	resolver.mu.RUnlock()
+
+	require.True(t, found, "Records for question %v not found", question)
+	require.Len(t, records, 2, "Should have exactly 2 records for the same question")
+
+	// Verify both record data values are present
+	recordStrings := []string{records[0].String(), records[1].String()}
+	assert.Contains(t, recordStrings[0]+recordStrings[1], record1.RData, "First record data should be present")
+	assert.Contains(t, recordStrings[0]+recordStrings[1], record2.RData, "Second record data should be present")
+}
+
+// TestLocalResolver_RecordRotation verifies that records are rotated in a round-robin fashion
+func TestLocalResolver_RecordRotation(t *testing.T) {
+	resolver := NewResolver()
+
+	recordName := "rotation.example.com."
+	recordType := dns.TypeA
+
+	// Create three records with the same name and type but different IPs
+	record1 := nbdns.SimpleRecord{
+		Name: recordName, Type: int(recordType), Class: nbdns.DefaultClass, TTL: 300, RData: "192.168.1.1",
+	}
+	record2 := nbdns.SimpleRecord{
+		Name: recordName, Type: int(recordType), Class: nbdns.DefaultClass, TTL: 300, RData: "192.168.1.2",
+	}
+	record3 := nbdns.SimpleRecord{
+		Name: recordName, Type: int(recordType), Class: nbdns.DefaultClass, TTL: 300, RData: "192.168.1.3",
+	}
+
+	update := []nbdns.SimpleRecord{record1, record2, record3}
+
+	// Apply update with all three records
+	resolver.Update(update)
+
+	msg := new(dns.Msg).SetQuestion(recordName, recordType)
+
+	// First lookup - should return the records in original order
+	var responses [3]*dns.Msg
+
+	// Perform three lookups to verify rotation
+	for i := 0; i < 3; i++ {
+		responseWriter := &test.MockResponseWriter{
+			WriteMsgFunc: func(m *dns.Msg) error {
+				responses[i] = m
+				return nil
+			},
+		}
+
+		resolver.ServeDNS(responseWriter, msg)
+	}
+
+	// Verify all three responses contain answers
+	for i, resp := range responses {
+		require.NotNil(t, resp, "Response %d should not be nil", i)
+		require.Len(t, resp.Answer, 3, "Response %d should have 3 answers", i)
+	}
+
+	// Verify the first record in each response is different due to rotation
+	firstRecordIPs := []string{
+		responses[0].Answer[0].String(),
+		responses[1].Answer[0].String(),
+		responses[2].Answer[0].String(),
+	}
+
+	// Each record should be different (rotated)
+	assert.NotEqual(t, firstRecordIPs[0], firstRecordIPs[1], "First lookup should differ from second lookup due to rotation")
+	assert.NotEqual(t, firstRecordIPs[1], firstRecordIPs[2], "Second lookup should differ from third lookup due to rotation")
+	assert.NotEqual(t, firstRecordIPs[0], firstRecordIPs[2], "First lookup should differ from third lookup due to rotation")
+
+	// After three rotations, we should have cycled through all records
+	assert.Contains(t, firstRecordIPs[0]+firstRecordIPs[1]+firstRecordIPs[2], record1.RData)
+	assert.Contains(t, firstRecordIPs[0]+firstRecordIPs[1]+firstRecordIPs[2], record2.RData)
+	assert.Contains(t, firstRecordIPs[0]+firstRecordIPs[1]+firstRecordIPs[2], record3.RData)
+}
+
+// TestLocalResolver_CaseInsensitiveMatching verifies that DNS record lookups are case-insensitive
+func TestLocalResolver_CaseInsensitiveMatching(t *testing.T) {
+	resolver := NewResolver()
+
+	// Create record with lowercase name
+	lowerCaseRecord := nbdns.SimpleRecord{
+		Name:  "lower.example.com.",
+		Type:  int(dns.TypeA),
+		Class: nbdns.DefaultClass,
+		TTL:   300,
+		RData: "10.10.10.10",
+	}
+
+	// Create record with mixed case name
+	mixedCaseRecord := nbdns.SimpleRecord{
+		Name:  "MiXeD.ExAmPlE.CoM.",
+		Type:  int(dns.TypeA),
+		Class: nbdns.DefaultClass,
+		TTL:   300,
+		RData: "20.20.20.20",
+	}
+
+	// Update resolver with the records
+	resolver.Update([]nbdns.SimpleRecord{lowerCaseRecord, mixedCaseRecord})
+
+	testCases := []struct {
+		name          string
+		queryName     string
+		expectedRData string
+		shouldResolve bool
+	}{
+		{
+			name:          "Query lowercase with lowercase record",
+			queryName:     "lower.example.com.",
+			expectedRData: "10.10.10.10",
+			shouldResolve: true,
+		},
+		{
+			name:          "Query uppercase with lowercase record",
+			queryName:     "LOWER.EXAMPLE.COM.",
+			expectedRData: "10.10.10.10",
+			shouldResolve: true,
+		},
+		{
+			name:          "Query mixed case with lowercase record",
+			queryName:     "LoWeR.eXaMpLe.CoM.",
+			expectedRData: "10.10.10.10",
+			shouldResolve: true,
+		},
+		{
+			name:          "Query lowercase with mixed case record",
+			queryName:     "mixed.example.com.",
+			expectedRData: "20.20.20.20",
+			shouldResolve: true,
+		},
+		{
+			name:          "Query uppercase with mixed case record",
+			queryName:     "MIXED.EXAMPLE.COM.",
+			expectedRData: "20.20.20.20",
+			shouldResolve: true,
+		},
+		{
+			name:          "Query with different casing pattern",
+			queryName:     "mIxEd.ExaMpLe.cOm.",
+			expectedRData: "20.20.20.20",
+			shouldResolve: true,
+		},
+		{
+			name:          "Query non-existent domain",
+			queryName:     "nonexistent.example.com.",
+			shouldResolve: false,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			var responseMSG *dns.Msg
+
+			// Create DNS query with the test case name
+			msg := new(dns.Msg).SetQuestion(tc.queryName, dns.TypeA)
+
+			// Create mock response writer to capture the response
+			responseWriter := &test.MockResponseWriter{
+				WriteMsgFunc: func(m *dns.Msg) error {
+					responseMSG = m
+					return nil
+				},
+			}
+
+			// Perform DNS query
+			resolver.ServeDNS(responseWriter, msg)
+
+			// Check if we expect a successful resolution
+			if !tc.shouldResolve {
+				if responseMSG == nil || len(responseMSG.Answer) == 0 {
+					// Expected no answer, test passes
+					return
+				}
+				t.Fatalf("Expected no resolution for %s, but got answer: %v", tc.queryName, responseMSG.Answer)
+			}
+
+			// Verify we got a response
+			require.NotNil(t, responseMSG, "Should have received a response message")
+			require.Greater(t, len(responseMSG.Answer), 0, "Response should contain at least one answer")
+
+			// Verify the response contains the expected data
+			answerString := responseMSG.Answer[0].String()
+			assert.Contains(t, answerString, tc.expectedRData,
+				"Answer should contain the expected IP address %s, got: %s",
+				tc.expectedRData, answerString)
+		})
+	}
+}
+
+// TestLocalResolver_CNAMEFallback verifies that the resolver correctly falls back
+// to checking for CNAME records when the requested record type isn't found
+func TestLocalResolver_CNAMEFallback(t *testing.T) {
+	resolver := NewResolver()
+
+	// Create a CNAME record (but no A record for this name)
+	cnameRecord := nbdns.SimpleRecord{
+		Name:  "alias.example.com.",
+		Type:  int(dns.TypeCNAME),
+		Class: nbdns.DefaultClass,
+		TTL:   300,
+		RData: "target.example.com.",
+	}
+
+	// Create an A record for the CNAME target
+	targetRecord := nbdns.SimpleRecord{
+		Name:  "target.example.com.",
+		Type:  int(dns.TypeA),
+		Class: nbdns.DefaultClass,
+		TTL:   300,
+		RData: "192.168.100.100",
+	}
+
+	// Update resolver with both records
+	resolver.Update([]nbdns.SimpleRecord{cnameRecord, targetRecord})
+
+	testCases := []struct {
+		name          string
+		queryName     string
+		queryType     uint16
+		expectedType  string
+		expectedRData string
+		shouldResolve bool
+	}{
+		{
+			name:          "Directly query CNAME record",
+			queryName:     "alias.example.com.",
+			queryType:     dns.TypeCNAME,
+			expectedType:  "CNAME",
+			expectedRData: "target.example.com.",
+			shouldResolve: true,
+		},
+		{
+			name:          "Query A record but get CNAME fallback",
+			queryName:     "alias.example.com.",
+			queryType:     dns.TypeA,
+			expectedType:  "CNAME",
+			expectedRData: "target.example.com.",
+			shouldResolve: true,
+		},
+		{
+			name:          "Query AAAA record but get CNAME fallback",
+			queryName:     "alias.example.com.",
+			queryType:     dns.TypeAAAA,
+			expectedType:  "CNAME",
+			expectedRData: "target.example.com.",
+			shouldResolve: true,
+		},
+		{
+			name:          "Query direct A record",
+			queryName:     "target.example.com.",
+			queryType:     dns.TypeA,
+			expectedType:  "A",
+			expectedRData: "192.168.100.100",
+			shouldResolve: true,
+		},
+		{
+			name:          "Query non-existent name",
+			queryName:     "nonexistent.example.com.",
+			queryType:     dns.TypeA,
+			shouldResolve: false,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			var responseMSG *dns.Msg
+
+			// Create DNS query with the test case parameters
+			msg := new(dns.Msg).SetQuestion(tc.queryName, tc.queryType)
+
+			// Create mock response writer to capture the response
+			responseWriter := &test.MockResponseWriter{
+				WriteMsgFunc: func(m *dns.Msg) error {
+					responseMSG = m
+					return nil
+				},
+			}
+
+			// Perform DNS query
+			resolver.ServeDNS(responseWriter, msg)
+
+			// Check if we expect a successful resolution
+			if !tc.shouldResolve {
+				if responseMSG == nil || len(responseMSG.Answer) == 0 || responseMSG.Rcode != dns.RcodeSuccess {
+					// Expected no resolution, test passes
+					return
+				}
+				t.Fatalf("Expected no resolution for %s, but got answer: %v", tc.queryName, responseMSG.Answer)
+			}
+
+			// Verify we got a successful response
+			require.NotNil(t, responseMSG, "Should have received a response message")
+			require.Equal(t, dns.RcodeSuccess, responseMSG.Rcode, "Response should have success status code")
+			require.Greater(t, len(responseMSG.Answer), 0, "Response should contain at least one answer")
+
+			// Verify the response contains the expected data
+			answerString := responseMSG.Answer[0].String()
+			assert.Contains(t, answerString, tc.expectedType,
+				"Answer should be of type %s, got: %s", tc.expectedType, answerString)
+			assert.Contains(t, answerString, tc.expectedRData,
+				"Answer should contain the expected data %s, got: %s", tc.expectedRData, answerString)
+		})
+	}
+}
diff --git a/client/internal/dns/local_test.go b/client/internal/dns/local_test.go
deleted file mode 100644
index 0a42b321..00000000
--- a/client/internal/dns/local_test.go
+++ /dev/null
@@ -1,88 +0,0 @@
-package dns
-
-import (
-	"strings"
-	"testing"
-
-	"github.com/miekg/dns"
-
-	nbdns "github.com/netbirdio/netbird/dns"
-)
-
-func TestLocalResolver_ServeDNS(t *testing.T) {
-	recordA := nbdns.SimpleRecord{
-		Name:  "peera.netbird.cloud.",
-		Type:  1,
-		Class: nbdns.DefaultClass,
-		TTL:   300,
-		RData: "1.2.3.4",
-	}
-
-	recordCNAME := nbdns.SimpleRecord{
-		Name:  "peerb.netbird.cloud.",
-		Type:  5,
-		Class: nbdns.DefaultClass,
-		TTL:   300,
-		RData: "www.netbird.io",
-	}
-
-	testCases := []struct {
-		name                string
-		inputRecord         nbdns.SimpleRecord
-		inputMSG            *dns.Msg
-		responseShouldBeNil bool
-	}{
-		{
-			name:        "Should Resolve A Record",
-			inputRecord: recordA,
-			inputMSG:    new(dns.Msg).SetQuestion(recordA.Name, dns.TypeA),
-		},
-		{
-			name:        "Should Resolve CNAME Record",
-			inputRecord: recordCNAME,
-			inputMSG:    new(dns.Msg).SetQuestion(recordCNAME.Name, dns.TypeCNAME),
-		},
-		{
-			name:                "Should Not Write When Not Found A Record",
-			inputRecord:         recordA,
-			inputMSG:            new(dns.Msg).SetQuestion("not.found.com", dns.TypeA),
-			responseShouldBeNil: true,
-		},
-	}
-
-	for _, testCase := range testCases {
-		t.Run(testCase.name, func(t *testing.T) {
-			resolver := &localResolver{
-				registeredMap: make(registrationMap),
-			}
-			_, _ = resolver.registerRecord(testCase.inputRecord)
-			var responseMSG *dns.Msg
-			responseWriter := &mockResponseWriter{
-				WriteMsgFunc: func(m *dns.Msg) error {
-					responseMSG = m
-					return nil
-				},
-			}
-
-			resolver.ServeDNS(responseWriter, testCase.inputMSG)
-
-			if responseMSG == nil || len(responseMSG.Answer) == 0 {
-				if testCase.responseShouldBeNil {
-					return
-				}
-				t.Fatalf("should write a response message")
-			}
-
-			answerString := responseMSG.Answer[0].String()
-			if !strings.Contains(answerString, testCase.inputRecord.Name) {
-				t.Fatalf("answer doesn't contain the same domain name: \nWant: %s\nGot:%s", testCase.name, answerString)
-			}
-			if !strings.Contains(answerString, dns.Type(testCase.inputRecord.Type).String()) {
-				t.Fatalf("answer doesn't contain the correct type: \nWant: %s\nGot:%s", dns.Type(testCase.inputRecord.Type).String(), answerString)
-			}
-			if !strings.Contains(answerString, testCase.inputRecord.RData) {
-				t.Fatalf("answer doesn't contain the same address: \nWant: %s\nGot:%s", testCase.inputRecord.RData, answerString)
-			}
-		})
-	}
-}
diff --git a/client/internal/dns/mock_test.go b/client/internal/dns/mock_test.go
deleted file mode 100644
index d52ae24d..00000000
--- a/client/internal/dns/mock_test.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package dns
-
-import (
-	"net"
-
-	"github.com/miekg/dns"
-)
-
-type mockResponseWriter struct {
-	WriteMsgFunc func(m *dns.Msg) error
-}
-
-func (rw *mockResponseWriter) WriteMsg(m *dns.Msg) error {
-	if rw.WriteMsgFunc != nil {
-		return rw.WriteMsgFunc(m)
-	}
-	return nil
-}
-
-func (rw *mockResponseWriter) LocalAddr() net.Addr       { return nil }
-func (rw *mockResponseWriter) RemoteAddr() net.Addr      { return nil }
-func (rw *mockResponseWriter) Write([]byte) (int, error) { return 0, nil }
-func (rw *mockResponseWriter) Close() error              { return nil }
-func (rw *mockResponseWriter) TsigStatus() error         { return nil }
-func (rw *mockResponseWriter) TsigTimersOnly(bool)       {}
-func (rw *mockResponseWriter) Hijack()                   {}
diff --git a/client/internal/dns/server_test.go b/client/internal/dns/server_test.go
index ed69b0e9..e55b2791 100644
--- a/client/internal/dns/server_test.go
+++ b/client/internal/dns/server_test.go
@@ -23,6 +23,9 @@ import (
 	"github.com/netbirdio/netbird/client/iface/device"
 	pfmock "github.com/netbirdio/netbird/client/iface/mocks"
 	"github.com/netbirdio/netbird/client/iface/wgaddr"
+	"github.com/netbirdio/netbird/client/internal/dns/local"
+	"github.com/netbirdio/netbird/client/internal/dns/test"
+	"github.com/netbirdio/netbird/client/internal/dns/types"
 	"github.com/netbirdio/netbird/client/internal/netflow"
 	"github.com/netbirdio/netbird/client/internal/peer"
 	"github.com/netbirdio/netbird/client/internal/statemanager"
@@ -43,10 +46,9 @@ func (w *mocWGIface) Name() string {
 }
 
 func (w *mocWGIface) Address() wgaddr.Address {
-	ip, network, _ := net.ParseCIDR("100.66.100.0/24")
 	return wgaddr.Address{
-		IP:      ip,
-		Network: network,
+		IP:      netip.MustParseAddr("100.66.100.1"),
+		Network: netip.MustParsePrefix("100.66.100.0/24"),
 	}
 }
 
@@ -107,6 +109,7 @@ func generateDummyHandler(domain string, servers []nbdns.NameServer) *upstreamRe
 }
 
 func TestUpdateDNSServer(t *testing.T) {
+
 	nameServers := []nbdns.NameServer{
 		{
 			IP:     netip.MustParseAddr("8.8.8.8"),
@@ -120,22 +123,21 @@ func TestUpdateDNSServer(t *testing.T) {
 		},
 	}
 
-	dummyHandler := &localResolver{}
+	dummyHandler := local.NewResolver()
 
 	testCases := []struct {
 		name                string
 		initUpstreamMap     registeredHandlerMap
-		initLocalMap        registrationMap
+		initLocalRecords    []nbdns.SimpleRecord
 		initSerial          uint64
 		inputSerial         uint64
 		inputUpdate         nbdns.Config
 		shouldFail          bool
 		expectedUpstreamMap registeredHandlerMap
-		expectedLocalMap    registrationMap
+		expectedLocalQs     []dns.Question
 	}{
 		{
 			name:            "Initial Config Should Succeed",
-			initLocalMap:    make(registrationMap),
 			initUpstreamMap: make(registeredHandlerMap),
 			initSerial:      0,
 			inputSerial:     1,
@@ -159,30 +161,30 @@ func TestUpdateDNSServer(t *testing.T) {
 				},
 			},
 			expectedUpstreamMap: registeredHandlerMap{
-				generateDummyHandler("netbird.io", nameServers).id(): handlerWrapper{
+				generateDummyHandler("netbird.io", nameServers).ID(): handlerWrapper{
 					domain:   "netbird.io",
 					handler:  dummyHandler,
 					priority: PriorityMatchDomain,
 				},
-				dummyHandler.id(): handlerWrapper{
+				dummyHandler.ID(): handlerWrapper{
 					domain:   "netbird.cloud",
 					handler:  dummyHandler,
 					priority: PriorityMatchDomain,
 				},
-				generateDummyHandler(".", nameServers).id(): handlerWrapper{
+				generateDummyHandler(".", nameServers).ID(): handlerWrapper{
 					domain:   nbdns.RootZone,
 					handler:  dummyHandler,
 					priority: PriorityDefault,
 				},
 			},
-			expectedLocalMap: registrationMap{buildRecordKey(zoneRecords[0].Name, 1, 1): struct{}{}},
+			expectedLocalQs: []dns.Question{{Name: "peera.netbird.cloud.", Qtype: dns.TypeA, Qclass: dns.ClassINET}},
 		},
 		{
-			name:         "New Config Should Succeed",
-			initLocalMap: registrationMap{"netbird.cloud": struct{}{}},
+			name:             "New Config Should Succeed",
+			initLocalRecords: []nbdns.SimpleRecord{{Name: "netbird.cloud", Type: 1, Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"}},
 			initUpstreamMap: registeredHandlerMap{
-				generateDummyHandler(zoneRecords[0].Name, nameServers).id(): handlerWrapper{
-					domain:   buildRecordKey(zoneRecords[0].Name, 1, 1),
+				generateDummyHandler(zoneRecords[0].Name, nameServers).ID(): handlerWrapper{
+					domain:   "netbird.cloud",
 					handler:  dummyHandler,
 					priority: PriorityMatchDomain,
 				},
@@ -205,7 +207,7 @@ func TestUpdateDNSServer(t *testing.T) {
 				},
 			},
 			expectedUpstreamMap: registeredHandlerMap{
-				generateDummyHandler("netbird.io", nameServers).id(): handlerWrapper{
+				generateDummyHandler("netbird.io", nameServers).ID(): handlerWrapper{
 					domain:   "netbird.io",
 					handler:  dummyHandler,
 					priority: PriorityMatchDomain,
@@ -216,22 +218,22 @@ func TestUpdateDNSServer(t *testing.T) {
 					priority: PriorityMatchDomain,
 				},
 			},
-			expectedLocalMap: registrationMap{buildRecordKey(zoneRecords[0].Name, 1, 1): struct{}{}},
+			expectedLocalQs: []dns.Question{{Name: zoneRecords[0].Name, Qtype: 1, Qclass: 1}},
 		},
 		{
-			name:            "Smaller Config Serial Should Be Skipped",
-			initLocalMap:    make(registrationMap),
-			initUpstreamMap: make(registeredHandlerMap),
-			initSerial:      2,
-			inputSerial:     1,
-			shouldFail:      true,
+			name:             "Smaller Config Serial Should Be Skipped",
+			initLocalRecords: []nbdns.SimpleRecord{},
+			initUpstreamMap:  make(registeredHandlerMap),
+			initSerial:       2,
+			inputSerial:      1,
+			shouldFail:       true,
 		},
 		{
-			name:            "Empty NS Group Domain Or Not Primary Element Should Fail",
-			initLocalMap:    make(registrationMap),
-			initUpstreamMap: make(registeredHandlerMap),
-			initSerial:      0,
-			inputSerial:     1,
+			name:             "Empty NS Group Domain Or Not Primary Element Should Fail",
+			initLocalRecords: []nbdns.SimpleRecord{},
+			initUpstreamMap:  make(registeredHandlerMap),
+			initSerial:       0,
+			inputSerial:      1,
 			inputUpdate: nbdns.Config{
 				ServiceEnable: true,
 				CustomZones: []nbdns.CustomZone{
@@ -249,11 +251,11 @@ func TestUpdateDNSServer(t *testing.T) {
 			shouldFail: true,
 		},
 		{
-			name:            "Invalid NS Group Nameservers list Should Fail",
-			initLocalMap:    make(registrationMap),
-			initUpstreamMap: make(registeredHandlerMap),
-			initSerial:      0,
-			inputSerial:     1,
+			name:             "Invalid NS Group Nameservers list Should Fail",
+			initLocalRecords: []nbdns.SimpleRecord{},
+			initUpstreamMap:  make(registeredHandlerMap),
+			initSerial:       0,
+			inputSerial:      1,
 			inputUpdate: nbdns.Config{
 				ServiceEnable: true,
 				CustomZones: []nbdns.CustomZone{
@@ -271,11 +273,11 @@ func TestUpdateDNSServer(t *testing.T) {
 			shouldFail: true,
 		},
 		{
-			name:            "Invalid Custom Zone Records list Should Skip",
-			initLocalMap:    make(registrationMap),
-			initUpstreamMap: make(registeredHandlerMap),
-			initSerial:      0,
-			inputSerial:     1,
+			name:             "Invalid Custom Zone Records list Should Skip",
+			initLocalRecords: []nbdns.SimpleRecord{},
+			initUpstreamMap:  make(registeredHandlerMap),
+			initSerial:       0,
+			inputSerial:      1,
 			inputUpdate: nbdns.Config{
 				ServiceEnable: true,
 				CustomZones: []nbdns.CustomZone{
@@ -290,17 +292,17 @@ func TestUpdateDNSServer(t *testing.T) {
 					},
 				},
 			},
-			expectedUpstreamMap: registeredHandlerMap{generateDummyHandler(".", nameServers).id(): handlerWrapper{
+			expectedUpstreamMap: registeredHandlerMap{generateDummyHandler(".", nameServers).ID(): handlerWrapper{
 				domain:   ".",
 				handler:  dummyHandler,
 				priority: PriorityDefault,
 			}},
 		},
 		{
-			name:         "Empty Config Should Succeed and Clean Maps",
-			initLocalMap: registrationMap{"netbird.cloud": struct{}{}},
+			name:             "Empty Config Should Succeed and Clean Maps",
+			initLocalRecords: []nbdns.SimpleRecord{{Name: "netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"}},
 			initUpstreamMap: registeredHandlerMap{
-				generateDummyHandler(zoneRecords[0].Name, nameServers).id(): handlerWrapper{
+				generateDummyHandler(zoneRecords[0].Name, nameServers).ID(): handlerWrapper{
 					domain:   zoneRecords[0].Name,
 					handler:  dummyHandler,
 					priority: PriorityMatchDomain,
@@ -310,13 +312,13 @@ func TestUpdateDNSServer(t *testing.T) {
 			inputSerial:         1,
 			inputUpdate:         nbdns.Config{ServiceEnable: true},
 			expectedUpstreamMap: make(registeredHandlerMap),
-			expectedLocalMap:    make(registrationMap),
+			expectedLocalQs:     []dns.Question{},
 		},
 		{
-			name:         "Disabled Service Should clean map",
-			initLocalMap: registrationMap{"netbird.cloud": struct{}{}},
+			name:             "Disabled Service Should clean map",
+			initLocalRecords: []nbdns.SimpleRecord{{Name: "netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"}},
 			initUpstreamMap: registeredHandlerMap{
-				generateDummyHandler(zoneRecords[0].Name, nameServers).id(): handlerWrapper{
+				generateDummyHandler(zoneRecords[0].Name, nameServers).ID(): handlerWrapper{
 					domain:   zoneRecords[0].Name,
 					handler:  dummyHandler,
 					priority: PriorityMatchDomain,
@@ -326,7 +328,7 @@ func TestUpdateDNSServer(t *testing.T) {
 			inputSerial:         1,
 			inputUpdate:         nbdns.Config{ServiceEnable: false},
 			expectedUpstreamMap: make(registeredHandlerMap),
-			expectedLocalMap:    make(registrationMap),
+			expectedLocalQs:     []dns.Question{},
 		},
 	}
 
@@ -377,7 +379,7 @@ func TestUpdateDNSServer(t *testing.T) {
 			}()
 
 			dnsServer.dnsMuxMap = testCase.initUpstreamMap
-			dnsServer.localResolver.registeredMap = testCase.initLocalMap
+			dnsServer.localResolver.Update(testCase.initLocalRecords)
 			dnsServer.updateSerial = testCase.initSerial
 
 			err = dnsServer.UpdateDNSServer(testCase.inputSerial, testCase.inputUpdate)
@@ -399,15 +401,23 @@ func TestUpdateDNSServer(t *testing.T) {
 				}
 			}
 
-			if len(dnsServer.localResolver.registeredMap) != len(testCase.expectedLocalMap) {
-				t.Fatalf("update local failed, registered map size is different than expected, want %d, got %d", len(testCase.expectedLocalMap), len(dnsServer.localResolver.registeredMap))
+			var responseMSG *dns.Msg
+			responseWriter := &test.MockResponseWriter{
+				WriteMsgFunc: func(m *dns.Msg) error {
+					responseMSG = m
+					return nil
+				},
+			}
+			for _, q := range testCase.expectedLocalQs {
+				dnsServer.localResolver.ServeDNS(responseWriter, &dns.Msg{
+					Question: []dns.Question{q},
+				})
 			}
 
-			for key := range testCase.expectedLocalMap {
-				_, found := dnsServer.localResolver.registeredMap[key]
-				if !found {
-					t.Fatalf("update local failed, key %s was not found in the localResolver.registeredMap: %#v", key, dnsServer.localResolver.registeredMap)
-				}
+			if len(testCase.expectedLocalQs) > 0 {
+				assert.NotNil(t, responseMSG, "response message should not be nil")
+				assert.Equal(t, dns.RcodeSuccess, responseMSG.Rcode, "response code should be success")
+				assert.NotEmpty(t, responseMSG.Answer, "response message should have answers")
 			}
 		})
 	}
@@ -453,17 +463,10 @@ func TestDNSFakeResolverHandleUpdates(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 
-	_, ipNet, err := net.ParseCIDR("100.66.100.1/32")
-	if err != nil {
-		t.Errorf("parse CIDR: %v", err)
-		return
-	}
-
 	packetfilter := pfmock.NewMockPacketFilter(ctrl)
 	packetfilter.EXPECT().DropOutgoing(gomock.Any(), gomock.Any()).AnyTimes()
 	packetfilter.EXPECT().AddUDPPacketHook(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
 	packetfilter.EXPECT().RemovePacketHook(gomock.Any())
-	packetfilter.EXPECT().SetNetwork(ipNet)
 
 	if err := wgIface.SetFilter(packetfilter); err != nil {
 		t.Errorf("set packet filter: %v", err)
@@ -491,11 +494,12 @@ func TestDNSFakeResolverHandleUpdates(t *testing.T) {
 	dnsServer.dnsMuxMap = registeredHandlerMap{
 		"id1": handlerWrapper{
 			domain:   zoneRecords[0].Name,
-			handler:  &localResolver{},
+			handler:  &local.Resolver{},
 			priority: PriorityMatchDomain,
 		},
 	}
-	dnsServer.localResolver.registeredMap = registrationMap{"netbird.cloud": struct{}{}}
+	//dnsServer.localResolver.RegisteredMap = local.RegistrationMap{local.BuildRecordKey("netbird.cloud", dns.ClassINET, dns.TypeA): struct{}{}}
+	dnsServer.localResolver.Update([]nbdns.SimpleRecord{{Name: "netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"}})
 	dnsServer.updateSerial = 0
 
 	nameServers := []nbdns.NameServer{
@@ -582,7 +586,7 @@ func TestDNSServerStartStop(t *testing.T) {
 			}
 			time.Sleep(100 * time.Millisecond)
 			defer dnsServer.Stop()
-			_, err = dnsServer.localResolver.registerRecord(zoneRecords[0])
+			err = dnsServer.localResolver.RegisterRecord(zoneRecords[0])
 			if err != nil {
 				t.Error(err)
 			}
@@ -630,13 +634,11 @@ func TestDNSServerStartStop(t *testing.T) {
 func TestDNSServerUpstreamDeactivateCallback(t *testing.T) {
 	hostManager := &mockHostConfigurator{}
 	server := DefaultServer{
-		ctx:     context.Background(),
-		service: NewServiceViaMemory(&mocWGIface{}),
-		localResolver: &localResolver{
-			registeredMap: make(registrationMap),
-		},
-		handlerChain: NewHandlerChain(),
-		hostManager:  hostManager,
+		ctx:           context.Background(),
+		service:       NewServiceViaMemory(&mocWGIface{}),
+		localResolver: local.NewResolver(),
+		handlerChain:  NewHandlerChain(),
+		hostManager:   hostManager,
 		currentConfig: HostDNSConfig{
 			Domains: []DomainConfig{
 				{false, "domain0", false},
@@ -1004,7 +1006,7 @@ func TestHandlerChain_DomainPriorities(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			r := new(dns.Msg)
 			r.SetQuestion(tc.query, dns.TypeA)
-			w := &ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
+			w := &ResponseWriterChain{ResponseWriter: &test.MockResponseWriter{}}
 
 			if mh, ok := tc.expectedHandler.(*MockHandler); ok {
 				mh.On("ServeDNS", mock.Anything, r).Once()
@@ -1037,9 +1039,9 @@ type mockHandler struct {
 }
 
 func (m *mockHandler) ServeDNS(dns.ResponseWriter, *dns.Msg) {}
-func (m *mockHandler) stop()                                 {}
-func (m *mockHandler) probeAvailability()                    {}
-func (m *mockHandler) id() handlerID                         { return handlerID(m.Id) }
+func (m *mockHandler) Stop()                                 {}
+func (m *mockHandler) ProbeAvailability()                    {}
+func (m *mockHandler) ID() types.HandlerID                   { return types.HandlerID(m.Id) }
 
 type mockService struct{}
 
@@ -1113,7 +1115,7 @@ func TestDefaultServer_UpdateMux(t *testing.T) {
 		name             string
 		initialHandlers  registeredHandlerMap
 		updates          []handlerWrapper
-		expectedHandlers map[string]string // map[handlerID]domain
+		expectedHandlers map[string]string // map[HandlerID]domain
 		description      string
 	}{
 		{
@@ -1409,7 +1411,7 @@ func TestDefaultServer_UpdateMux(t *testing.T) {
 
 			// Check each expected handler
 			for id, expectedDomain := range tt.expectedHandlers {
-				handler, exists := server.dnsMuxMap[handlerID(id)]
+				handler, exists := server.dnsMuxMap[types.HandlerID(id)]
 				assert.True(t, exists, "Expected handler %s not found", id)
 				if exists {
 					assert.Equal(t, expectedDomain, handler.domain,
@@ -1418,9 +1420,9 @@ func TestDefaultServer_UpdateMux(t *testing.T) {
 			}
 
 			// Verify no unexpected handlers exist
-			for handlerID := range server.dnsMuxMap {
-				_, expected := tt.expectedHandlers[string(handlerID)]
-				assert.True(t, expected, "Unexpected handler found: %s", handlerID)
+			for HandlerID := range server.dnsMuxMap {
+				_, expected := tt.expectedHandlers[string(HandlerID)]
+				assert.True(t, expected, "Unexpected handler found: %s", HandlerID)
 			}
 
 			// Verify the handlerChain state and order
@@ -1696,7 +1698,7 @@ func TestExtraDomains(t *testing.T) {
 				handlerChain:   NewHandlerChain(),
 				wgInterface:    &mocWGIface{},
 				hostManager:    mockHostConfig,
-				localResolver:  &localResolver{},
+				localResolver:  &local.Resolver{},
 				service:        mockSvc,
 				statusRecorder: peer.NewRecorder("test"),
 				extraDomains:   make(map[domain.Domain]int),
@@ -1781,7 +1783,7 @@ func TestExtraDomainsRefCounting(t *testing.T) {
 		ctx:            context.Background(),
 		handlerChain:   NewHandlerChain(),
 		hostManager:    mockHostConfig,
-		localResolver:  &localResolver{},
+		localResolver:  &local.Resolver{},
 		service:        mockSvc,
 		statusRecorder: peer.NewRecorder("test"),
 		extraDomains:   make(map[domain.Domain]int),
@@ -1833,7 +1835,7 @@ func TestUpdateConfigWithExistingExtraDomains(t *testing.T) {
 		ctx:            context.Background(),
 		handlerChain:   NewHandlerChain(),
 		hostManager:    mockHostConfig,
-		localResolver:  &localResolver{},
+		localResolver:  &local.Resolver{},
 		service:        mockSvc,
 		statusRecorder: peer.NewRecorder("test"),
 		extraDomains:   make(map[domain.Domain]int),
@@ -1916,7 +1918,7 @@ func TestDomainCaseHandling(t *testing.T) {
 		ctx:            context.Background(),
 		handlerChain:   NewHandlerChain(),
 		hostManager:    mockHostConfig,
-		localResolver:  &localResolver{},
+		localResolver:  &local.Resolver{},
 		service:        mockSvc,
 		statusRecorder: peer.NewRecorder("test"),
 		extraDomains:   make(map[domain.Domain]int),
diff --git a/client/internal/dns/service_memory_test.go b/client/internal/dns/service_memory_test.go
deleted file mode 100644
index 244adfae..00000000
--- a/client/internal/dns/service_memory_test.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package dns
-
-import (
-	"net"
-	"testing"
-
-	nbnet "github.com/netbirdio/netbird/util/net"
-)
-
-func TestGetLastIPFromNetwork(t *testing.T) {
-	tests := []struct {
-		addr string
-		ip   string
-	}{
-		{"2001:db8::/32", "2001:db8:ffff:ffff:ffff:ffff:ffff:fffe"},
-		{"192.168.0.0/30", "192.168.0.2"},
-		{"192.168.0.0/16", "192.168.255.254"},
-		{"192.168.0.0/24", "192.168.0.254"},
-	}
-
-	for _, tt := range tests {
-		_, ipnet, err := net.ParseCIDR(tt.addr)
-		if err != nil {
-			t.Errorf("Error parsing CIDR: %v", err)
-			return
-		}
-
-		lastIP := nbnet.GetLastIPFromNetwork(ipnet, 1).String()
-		if lastIP != tt.ip {
-			t.Errorf("wrong IP address, expected %s: got %s", tt.ip, lastIP)
-		}
-	}
-}
diff --git a/client/internal/dns/test/mock.go b/client/internal/dns/test/mock.go
new file mode 100644
index 00000000..1db45280
--- /dev/null
+++ b/client/internal/dns/test/mock.go
@@ -0,0 +1,26 @@
+package test
+
+import (
+	"net"
+
+	"github.com/miekg/dns"
+)
+
+type MockResponseWriter struct {
+	WriteMsgFunc func(m *dns.Msg) error
+}
+
+func (rw *MockResponseWriter) WriteMsg(m *dns.Msg) error {
+	if rw.WriteMsgFunc != nil {
+		return rw.WriteMsgFunc(m)
+	}
+	return nil
+}
+
+func (rw *MockResponseWriter) LocalAddr() net.Addr       { return nil }
+func (rw *MockResponseWriter) RemoteAddr() net.Addr      { return nil }
+func (rw *MockResponseWriter) Write([]byte) (int, error) { return 0, nil }
+func (rw *MockResponseWriter) Close() error              { return nil }
+func (rw *MockResponseWriter) TsigStatus() error         { return nil }
+func (rw *MockResponseWriter) TsigTimersOnly(bool)       {}
+func (rw *MockResponseWriter) Hijack()                   {}
diff --git a/client/internal/dns/upstream_test.go b/client/internal/dns/upstream_test.go
index 5dbcc9f7..e440995d 100644
--- a/client/internal/dns/upstream_test.go
+++ b/client/internal/dns/upstream_test.go
@@ -2,12 +2,14 @@ package dns
 
 import (
 	"context"
-	"net"
+	"net/netip"
 	"strings"
 	"testing"
 	"time"
 
 	"github.com/miekg/dns"
+
+	"github.com/netbirdio/netbird/client/internal/dns/test"
 )
 
 func TestUpstreamResolver_ServeDNS(t *testing.T) {
@@ -56,7 +58,7 @@ func TestUpstreamResolver_ServeDNS(t *testing.T) {
 	for _, testCase := range testCases {
 		t.Run(testCase.name, func(t *testing.T) {
 			ctx, cancel := context.WithCancel(context.TODO())
-			resolver, _ := newUpstreamResolver(ctx, "", net.IP{}, &net.IPNet{}, nil, nil, ".")
+			resolver, _ := newUpstreamResolver(ctx, "", netip.Addr{}, netip.Prefix{}, nil, nil, ".")
 			resolver.upstreamServers = testCase.InputServers
 			resolver.upstreamTimeout = testCase.timeout
 			if testCase.cancelCTX {
@@ -66,7 +68,7 @@ func TestUpstreamResolver_ServeDNS(t *testing.T) {
 			}
 
 			var responseMSG *dns.Msg
-			responseWriter := &mockResponseWriter{
+			responseWriter := &test.MockResponseWriter{
 				WriteMsgFunc: func(m *dns.Msg) error {
 					responseMSG = m
 					return nil
@@ -130,7 +132,7 @@ func TestUpstreamResolver_DeactivationReactivation(t *testing.T) {
 	resolver.failsTillDeact = 0
 	resolver.reactivatePeriod = time.Microsecond * 100
 
-	responseWriter := &mockResponseWriter{
+	responseWriter := &test.MockResponseWriter{
 		WriteMsgFunc: func(m *dns.Msg) error { return nil },
 	}
 
diff --git a/client/internal/dnsfwd/forwarder_test.go b/client/internal/dnsfwd/forwarder_test.go
index 88ffc2af..f0829bbb 100644
--- a/client/internal/dnsfwd/forwarder_test.go
+++ b/client/internal/dnsfwd/forwarder_test.go
@@ -1,56 +1,61 @@
 package dnsfwd
 
 import (
-	"sync"
 	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/netbirdio/netbird/management/domain"
+	"github.com/netbirdio/netbird/route"
 )
 
-func TestGetResIdForDomain(t *testing.T) {
+func Test_getMatchingEntries(t *testing.T) {
 	testCases := []struct {
 		name           string
-		storedMappings map[string]string // key: domain pattern, value: resId
+		storedMappings map[string]route.ResID // key: domain pattern, value: resId
 		queryDomain    string
-		expectedResId  string
+		expectedResId  route.ResID
 	}{
 		{
 			name:           "Empty map returns empty string",
-			storedMappings: map[string]string{},
+			storedMappings: map[string]route.ResID{},
 			queryDomain:    "example.com",
 			expectedResId:  "",
 		},
 		{
 			name:           "Exact match returns stored resId",
-			storedMappings: map[string]string{"example.com": "res1"},
+			storedMappings: map[string]route.ResID{"example.com": "res1"},
 			queryDomain:    "example.com",
 			expectedResId:  "res1",
 		},
 		{
 			name:           "Wildcard pattern matches base domain",
-			storedMappings: map[string]string{"*.example.com": "res2"},
+			storedMappings: map[string]route.ResID{"*.example.com": "res2"},
 			queryDomain:    "example.com",
 			expectedResId:  "res2",
 		},
 		{
 			name:           "Wildcard pattern matches subdomain",
-			storedMappings: map[string]string{"*.example.com": "res3"},
+			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]string{"*.example.com": "res4"},
+			storedMappings: map[string]route.ResID{"*.example.com": "res4"},
 			queryDomain:    "foo.notexample.com",
 			expectedResId:  "",
 		},
 		{
 			name:           "Non-wildcard pattern does not match subdomain",
-			storedMappings: map[string]string{"example.com": "res5"},
+			storedMappings: map[string]route.ResID{"example.com": "res5"},
 			queryDomain:    "foo.example.com",
 			expectedResId:  "",
 		},
 		{
 			name: "Exact match over overlapping wildcard",
-			storedMappings: map[string]string{
+			storedMappings: map[string]route.ResID{
 				"*.example.com":   "resWildcard",
 				"foo.example.com": "resExact",
 			},
@@ -59,7 +64,7 @@ func TestGetResIdForDomain(t *testing.T) {
 		},
 		{
 			name: "Overlapping wildcards: Select more specific wildcard",
-			storedMappings: map[string]string{
+			storedMappings: map[string]route.ResID{
 				"*.example.com":     "resA",
 				"*.sub.example.com": "resB",
 			},
@@ -68,7 +73,7 @@ func TestGetResIdForDomain(t *testing.T) {
 		},
 		{
 			name: "Wildcard multi-level subdomain match",
-			storedMappings: map[string]string{
+			storedMappings: map[string]route.ResID{
 				"*.example.com": "resMulti",
 			},
 			queryDomain:   "a.b.example.com",
@@ -78,18 +83,21 @@ func TestGetResIdForDomain(t *testing.T) {
 
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {
-			fwd := &DNSForwarder{
-				resId: sync.Map{},
-			}
+			fwd := &DNSForwarder{}
 
+			var entries []*ForwarderEntry
 			for domainPattern, resId := range tc.storedMappings {
-				fwd.resId.Store(domainPattern, resId)
+				d, err := domain.FromString(domainPattern)
+				require.NoError(t, err)
+				entries = append(entries, &ForwarderEntry{
+					Domain: d,
+					ResID:  resId,
+				})
 			}
+			fwd.UpdateDomains(entries)
 
-			got := fwd.getResIdForDomain(tc.queryDomain)
-			if got != tc.expectedResId {
-				t.Errorf("For query domain %q, expected resId %q, but got %q", tc.queryDomain, tc.expectedResId, got)
-			}
+			got, _ := fwd.getMatchingEntries(tc.queryDomain)
+			assert.Equal(t, got, tc.expectedResId)
 		})
 	}
 }
diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go
index 7afe0fcd..6bdd9ae3 100644
--- a/client/internal/engine_test.go
+++ b/client/internal/engine_test.go
@@ -28,8 +28,6 @@ import (
 
 	"github.com/netbirdio/management-integrations/integrations"
 
-	"github.com/netbirdio/netbird/management/server/types"
-
 	"github.com/netbirdio/netbird/client/iface"
 	"github.com/netbirdio/netbird/client/iface/bind"
 	"github.com/netbirdio/netbird/client/iface/configurer"
@@ -38,6 +36,7 @@ import (
 	"github.com/netbirdio/netbird/client/iface/wgproxy"
 	"github.com/netbirdio/netbird/client/internal/dns"
 	"github.com/netbirdio/netbird/client/internal/peer"
+	"github.com/netbirdio/netbird/client/internal/peer/dispatcher"
 	"github.com/netbirdio/netbird/client/internal/peer/guard"
 	icemaker "github.com/netbirdio/netbird/client/internal/peer/ice"
 	"github.com/netbirdio/netbird/client/internal/routemanager"
@@ -53,6 +52,7 @@ import (
 	"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/types"
 	relayClient "github.com/netbirdio/netbird/relay/client"
 	"github.com/netbirdio/netbird/route"
 	signal "github.com/netbirdio/netbird/signal/client"
@@ -93,12 +93,16 @@ type MockWGIface struct {
 	GetFilterFunc              func() device.PacketFilter
 	GetDeviceFunc              func() *device.FilteredDevice
 	GetWGDeviceFunc            func() *wgdevice.Device
-	GetStatsFunc               func(peerKey string) (configurer.WGStats, error)
+	GetStatsFunc               func() (map[string]configurer.WGStats, error)
 	GetInterfaceGUIDStringFunc func() (string, error)
 	GetProxyFunc               func() wgproxy.Proxy
 	GetNetFunc                 func() *netstack.Net
 }
 
+func (m *MockWGIface) FullStats() (*configurer.Stats, error) {
+	return nil, fmt.Errorf("not implemented")
+}
+
 func (m *MockWGIface) GetInterfaceGUIDString() (string, error) {
 	return m.GetInterfaceGUIDStringFunc()
 }
@@ -171,8 +175,8 @@ func (m *MockWGIface) GetWGDevice() *wgdevice.Device {
 	return m.GetWGDeviceFunc()
 }
 
-func (m *MockWGIface) GetStats(peerKey string) (configurer.WGStats, error) {
-	return m.GetStatsFunc(peerKey)
+func (m *MockWGIface) GetStats() (map[string]configurer.WGStats, error) {
+	return m.GetStatsFunc()
 }
 
 func (m *MockWGIface) GetProxy() wgproxy.Proxy {
@@ -371,13 +375,13 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
 		},
 		AddressFunc: func() wgaddr.Address {
 			return wgaddr.Address{
-				IP: net.ParseIP("10.20.0.1"),
-				Network: &net.IPNet{
-					IP:   net.ParseIP("10.20.0.0"),
-					Mask: net.IPv4Mask(255, 255, 255, 0),
-				},
+				IP:      netip.MustParseAddr("10.20.0.1"),
+				Network: netip.MustParsePrefix("10.20.0.0/24"),
 			}
 		},
+		UpdatePeerFunc: func(peerKey string, allowedIps []netip.Prefix, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error {
+			return nil
+		},
 	}
 	engine.wgInterface = wgIface
 	engine.routeManager = routemanager.NewManager(routemanager.ManagerConfig{
@@ -400,6 +404,8 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
 	engine.udpMux = bind.NewUniversalUDPMuxDefault(bind.UniversalUDPMuxParams{UDPConn: conn})
 	engine.ctx = ctx
 	engine.srWatcher = guard.NewSRWatcher(nil, nil, nil, icemaker.Config{})
+	engine.connMgr = NewConnMgr(engine.config, engine.statusRecorder, engine.peerStore, wgIface, dispatcher.NewConnectionDispatcher())
+	engine.connMgr.Start(ctx)
 
 	type testCase struct {
 		name       string
@@ -770,6 +776,8 @@ func TestEngine_UpdateNetworkMapWithRoutes(t *testing.T) {
 
 			engine.routeManager = mockRouteManager
 			engine.dnsServer = &dns.MockServer{}
+			engine.connMgr = NewConnMgr(engine.config, engine.statusRecorder, engine.peerStore, engine.wgInterface, dispatcher.NewConnectionDispatcher())
+			engine.connMgr.Start(ctx)
 
 			defer func() {
 				exitErr := engine.Stop()
@@ -966,6 +974,8 @@ func TestEngine_UpdateNetworkMapWithDNSUpdate(t *testing.T) {
 			}
 
 			engine.dnsServer = mockDNSServer
+			engine.connMgr = NewConnMgr(engine.config, engine.statusRecorder, engine.peerStore, engine.wgInterface, dispatcher.NewConnectionDispatcher())
+			engine.connMgr.Start(ctx)
 
 			defer func() {
 				exitErr := engine.Stop()
@@ -1476,7 +1486,7 @@ func getConnectedPeers(e *Engine) int {
 	i := 0
 	for _, id := range e.peerStore.PeersPubKey() {
 		conn, _ := e.peerStore.PeerConn(id)
-		if conn.Status() == peer.StatusConnected {
+		if conn.IsConnected() {
 			i++
 		}
 	}
diff --git a/client/internal/lazyconn/activity/listener_test.go b/client/internal/lazyconn/activity/listener_test.go
new file mode 100644
index 00000000..98d7838d
--- /dev/null
+++ b/client/internal/lazyconn/activity/listener_test.go
@@ -0,0 +1,41 @@
+package activity
+
+import (
+	"testing"
+	"time"
+
+	log "github.com/sirupsen/logrus"
+
+	"github.com/netbirdio/netbird/client/internal/lazyconn"
+)
+
+func TestNewListener(t *testing.T) {
+	peer := &MocPeer{
+		PeerID: "examplePublicKey1",
+	}
+
+	cfg := lazyconn.PeerConfig{
+		PublicKey:  peer.PeerID,
+		PeerConnID: peer.ConnID(),
+		Log:        log.WithField("peer", "examplePublicKey1"),
+	}
+
+	l, err := NewListener(MocWGIface{}, cfg)
+	if err != nil {
+		t.Fatalf("failed to create listener: %v", err)
+	}
+
+	chanClosed := make(chan struct{})
+	go func() {
+		defer close(chanClosed)
+		l.ReadPackets()
+	}()
+
+	time.Sleep(1 * time.Second)
+	l.Close()
+
+	select {
+	case <-chanClosed:
+	case <-time.After(time.Second):
+	}
+}
diff --git a/client/internal/lazyconn/activity/manager_test.go b/client/internal/lazyconn/activity/manager_test.go
new file mode 100644
index 00000000..c7c6c878
--- /dev/null
+++ b/client/internal/lazyconn/activity/manager_test.go
@@ -0,0 +1,162 @@
+package activity
+
+import (
+	"net"
+	"net/netip"
+	"testing"
+	"time"
+
+	log "github.com/sirupsen/logrus"
+	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
+
+	"github.com/netbirdio/netbird/client/internal/lazyconn"
+	peerid "github.com/netbirdio/netbird/client/internal/peer/id"
+)
+
+type MocPeer struct {
+	PeerID string
+}
+
+func (m *MocPeer) ConnID() peerid.ConnID {
+	return peerid.ConnID(m)
+}
+
+type MocWGIface struct {
+}
+
+func (m MocWGIface) RemovePeer(string) error {
+	return nil
+}
+
+func (m MocWGIface) UpdatePeer(string, []netip.Prefix, time.Duration, *net.UDPAddr, *wgtypes.Key) error {
+	return nil
+
+}
+
+func TestManager_MonitorPeerActivity(t *testing.T) {
+	mocWgInterface := &MocWGIface{}
+
+	peer1 := &MocPeer{
+		PeerID: "examplePublicKey1",
+	}
+	mgr := NewManager(mocWgInterface)
+	defer mgr.Close()
+	peerCfg1 := lazyconn.PeerConfig{
+		PublicKey:  peer1.PeerID,
+		PeerConnID: peer1.ConnID(),
+		Log:        log.WithField("peer", "examplePublicKey1"),
+	}
+
+	if err := mgr.MonitorPeerActivity(peerCfg1); err != nil {
+		t.Fatalf("failed to monitor peer activity: %v", err)
+	}
+
+	if err := trigger(mgr.peers[peerCfg1.PeerConnID].conn.LocalAddr().String()); err != nil {
+		t.Fatalf("failed to trigger activity: %v", err)
+	}
+
+	select {
+	case peerConnID := <-mgr.OnActivityChan:
+		if peerConnID != peerCfg1.PeerConnID {
+			t.Fatalf("unexpected peerConnID: %v", peerConnID)
+		}
+	case <-time.After(1 * time.Second):
+	}
+}
+
+func TestManager_RemovePeerActivity(t *testing.T) {
+	mocWgInterface := &MocWGIface{}
+
+	peer1 := &MocPeer{
+		PeerID: "examplePublicKey1",
+	}
+	mgr := NewManager(mocWgInterface)
+	defer mgr.Close()
+
+	peerCfg1 := lazyconn.PeerConfig{
+		PublicKey:  peer1.PeerID,
+		PeerConnID: peer1.ConnID(),
+		Log:        log.WithField("peer", "examplePublicKey1"),
+	}
+
+	if err := mgr.MonitorPeerActivity(peerCfg1); err != nil {
+		t.Fatalf("failed to monitor peer activity: %v", err)
+	}
+
+	addr := mgr.peers[peerCfg1.PeerConnID].conn.LocalAddr().String()
+
+	mgr.RemovePeer(peerCfg1.Log, peerCfg1.PeerConnID)
+
+	if err := trigger(addr); err != nil {
+		t.Fatalf("failed to trigger activity: %v", err)
+	}
+
+	select {
+	case <-mgr.OnActivityChan:
+		t.Fatal("should not have active activity")
+	case <-time.After(1 * time.Second):
+	}
+}
+
+func TestManager_MultiPeerActivity(t *testing.T) {
+	mocWgInterface := &MocWGIface{}
+
+	peer1 := &MocPeer{
+		PeerID: "examplePublicKey1",
+	}
+	mgr := NewManager(mocWgInterface)
+	defer mgr.Close()
+
+	peerCfg1 := lazyconn.PeerConfig{
+		PublicKey:  peer1.PeerID,
+		PeerConnID: peer1.ConnID(),
+		Log:        log.WithField("peer", "examplePublicKey1"),
+	}
+
+	peer2 := &MocPeer{}
+	peerCfg2 := lazyconn.PeerConfig{
+		PublicKey:  peer2.PeerID,
+		PeerConnID: peer2.ConnID(),
+		Log:        log.WithField("peer", "examplePublicKey2"),
+	}
+
+	if err := mgr.MonitorPeerActivity(peerCfg1); err != nil {
+		t.Fatalf("failed to monitor peer activity: %v", err)
+	}
+
+	if err := mgr.MonitorPeerActivity(peerCfg2); err != nil {
+		t.Fatalf("failed to monitor peer activity: %v", err)
+	}
+
+	if err := trigger(mgr.peers[peerCfg1.PeerConnID].conn.LocalAddr().String()); err != nil {
+		t.Fatalf("failed to trigger activity: %v", err)
+	}
+
+	if err := trigger(mgr.peers[peerCfg2.PeerConnID].conn.LocalAddr().String()); err != nil {
+		t.Fatalf("failed to trigger activity: %v", err)
+	}
+
+	for i := 0; i < 2; i++ {
+		select {
+		case <-mgr.OnActivityChan:
+		case <-time.After(1 * time.Second):
+			t.Fatal("timed out waiting for activity")
+		}
+	}
+}
+
+func trigger(addr string) error {
+	// Create a connection to the destination UDP address and port
+	conn, err := net.Dial("udp", addr)
+	if err != nil {
+		return err
+	}
+	defer conn.Close()
+
+	// Write the bytes to the UDP connection
+	_, err = conn.Write([]byte{0x01, 0x02, 0x03, 0x04, 0x05})
+	if err != nil {
+		return err
+	}
+	return nil
+}
diff --git a/client/internal/lazyconn/inactivity/inactivity_test.go b/client/internal/lazyconn/inactivity/inactivity_test.go
new file mode 100644
index 00000000..94451298
--- /dev/null
+++ b/client/internal/lazyconn/inactivity/inactivity_test.go
@@ -0,0 +1,156 @@
+package inactivity
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	peerid "github.com/netbirdio/netbird/client/internal/peer/id"
+)
+
+type MocPeer struct {
+}
+
+func (m *MocPeer) ConnID() peerid.ConnID {
+	return peerid.ConnID(m)
+}
+
+func TestInactivityMonitor(t *testing.T) {
+	tCtx, testTimeoutCancel := context.WithTimeout(context.Background(), time.Second*5)
+	defer testTimeoutCancel()
+
+	p := &MocPeer{}
+	im := NewInactivityMonitor(p.ConnID(), time.Second*2)
+
+	timeoutChan := make(chan peerid.ConnID)
+
+	exitChan := make(chan struct{})
+
+	go func() {
+		defer close(exitChan)
+		im.Start(tCtx, timeoutChan)
+	}()
+
+	select {
+	case <-timeoutChan:
+	case <-tCtx.Done():
+		t.Fatal("timeout")
+	}
+
+	select {
+	case <-exitChan:
+	case <-tCtx.Done():
+		t.Fatal("timeout")
+	}
+}
+
+func TestReuseInactivityMonitor(t *testing.T) {
+	p := &MocPeer{}
+	im := NewInactivityMonitor(p.ConnID(), time.Second*2)
+
+	timeoutChan := make(chan peerid.ConnID)
+
+	for i := 2; i > 0; i-- {
+		exitChan := make(chan struct{})
+
+		testTimeoutCtx, testTimeoutCancel := context.WithTimeout(context.Background(), time.Second*5)
+
+		go func() {
+			defer close(exitChan)
+			im.Start(testTimeoutCtx, timeoutChan)
+		}()
+
+		select {
+		case <-timeoutChan:
+		case <-testTimeoutCtx.Done():
+			t.Fatal("timeout")
+		}
+
+		select {
+		case <-exitChan:
+		case <-testTimeoutCtx.Done():
+			t.Fatal("timeout")
+		}
+		testTimeoutCancel()
+	}
+}
+
+func TestStopInactivityMonitor(t *testing.T) {
+	tCtx, testTimeoutCancel := context.WithTimeout(context.Background(), time.Second*5)
+	defer testTimeoutCancel()
+
+	p := &MocPeer{}
+	im := NewInactivityMonitor(p.ConnID(), DefaultInactivityThreshold)
+
+	timeoutChan := make(chan peerid.ConnID)
+
+	exitChan := make(chan struct{})
+
+	go func() {
+		defer close(exitChan)
+		im.Start(tCtx, timeoutChan)
+	}()
+
+	go func() {
+		time.Sleep(3 * time.Second)
+		im.Stop()
+	}()
+
+	select {
+	case <-timeoutChan:
+		t.Fatal("unexpected timeout")
+	case <-exitChan:
+	case <-tCtx.Done():
+		t.Fatal("timeout")
+	}
+}
+
+func TestPauseInactivityMonitor(t *testing.T) {
+	tCtx, testTimeoutCancel := context.WithTimeout(context.Background(), time.Second*10)
+	defer testTimeoutCancel()
+
+	p := &MocPeer{}
+	trashHold := time.Second * 3
+	im := NewInactivityMonitor(p.ConnID(), trashHold)
+
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	timeoutChan := make(chan peerid.ConnID)
+
+	exitChan := make(chan struct{})
+
+	go func() {
+		defer close(exitChan)
+		im.Start(ctx, timeoutChan)
+	}()
+
+	time.Sleep(1 * time.Second) // grant time to start the monitor
+	im.PauseTimer()
+
+	// check to do not receive timeout
+	thresholdCtx, thresholdCancel := context.WithTimeout(context.Background(), trashHold+time.Second)
+	defer thresholdCancel()
+	select {
+	case <-exitChan:
+		t.Fatal("unexpected exit")
+	case <-timeoutChan:
+		t.Fatal("unexpected timeout")
+	case <-thresholdCtx.Done():
+		// test ok
+	case <-tCtx.Done():
+		t.Fatal("test timed out")
+	}
+
+	// test reset timer
+	im.ResetTimer()
+
+	select {
+	case <-tCtx.Done():
+		t.Fatal("test timed out")
+	case <-exitChan:
+		t.Fatal("unexpected exit")
+	case <-timeoutChan:
+		// expected timeout
+	}
+}
diff --git a/client/internal/lazyconn/support_test.go b/client/internal/lazyconn/support_test.go
new file mode 100644
index 00000000..9ae95a4a
--- /dev/null
+++ b/client/internal/lazyconn/support_test.go
@@ -0,0 +1,31 @@
+package lazyconn
+
+import "testing"
+
+func TestIsSupported(t *testing.T) {
+	tests := []struct {
+		version string
+		want    bool
+	}{
+		{"development", true},
+		{"0.45.0", true},
+		{"v0.45.0", true},
+		{"0.45.1", true},
+		{"0.45.1-SNAPSHOT-559e6731", true},
+		{"v0.45.1-dev", true},
+		{"a7d5c522", false},
+		{"0.9.6", false},
+		{"0.9.6-SNAPSHOT", false},
+		{"0.9.6-SNAPSHOT-2033650", false},
+		{"meta_wt_version", false},
+		{"v0.31.1-dev", false},
+		{"", false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.version, func(t *testing.T) {
+			if got := IsSupported(tt.version); got != tt.want {
+				t.Errorf("IsSupported() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/client/internal/netflow/logger/logger_test.go b/client/internal/netflow/logger/logger_test.go
index 06e10c36..1144544d 100644
--- a/client/internal/netflow/logger/logger_test.go
+++ b/client/internal/netflow/logger/logger_test.go
@@ -1,7 +1,7 @@
 package logger_test
 
 import (
-	"net"
+	"net/netip"
 	"testing"
 	"time"
 
@@ -12,7 +12,7 @@ import (
 )
 
 func TestStore(t *testing.T) {
-	logger := logger.New(nil, net.IPNet{})
+	logger := logger.New(nil, netip.Prefix{})
 	logger.Enable()
 
 	event := types.EventFields{
diff --git a/client/internal/netflow/manager_test.go b/client/internal/netflow/manager_test.go
index bf7e05f8..0b5eb3be 100644
--- a/client/internal/netflow/manager_test.go
+++ b/client/internal/netflow/manager_test.go
@@ -1,7 +1,7 @@
 package netflow
 
 import (
-	"net"
+	"net/netip"
 	"testing"
 	"time"
 
@@ -33,10 +33,7 @@ func (m *mockIFaceMapper) IsUserspaceBind() bool {
 func TestManager_Update(t *testing.T) {
 	mockIFace := &mockIFaceMapper{
 		address: wgaddr.Address{
-			Network: &net.IPNet{
-				IP:   net.ParseIP("192.168.1.1"),
-				Mask: net.CIDRMask(24, 32),
-			},
+			Network: netip.MustParsePrefix("192.168.1.1/32"),
 		},
 		isUserspaceBind: true,
 	}
@@ -102,10 +99,7 @@ func TestManager_Update(t *testing.T) {
 func TestManager_Update_TokenPreservation(t *testing.T) {
 	mockIFace := &mockIFaceMapper{
 		address: wgaddr.Address{
-			Network: &net.IPNet{
-				IP:   net.ParseIP("192.168.1.1"),
-				Mask: net.CIDRMask(24, 32),
-			},
+			Network: netip.MustParsePrefix("192.168.1.1/32"),
 		},
 		isUserspaceBind: true,
 	}
diff --git a/client/internal/networkmonitor/check_change_windows_test.go b/client/internal/networkmonitor/check_change_windows_test.go
new file mode 100644
index 00000000..29ff34dc
--- /dev/null
+++ b/client/internal/networkmonitor/check_change_windows_test.go
@@ -0,0 +1,404 @@
+package networkmonitor
+
+import (
+	"net"
+	"net/netip"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
+)
+
+func TestRouteChanged(t *testing.T) {
+	tests := []struct {
+		name      string
+		route     systemops.RouteUpdate
+		nexthopv4 systemops.Nexthop
+		nexthopv6 systemops.Nexthop
+		expected  bool
+	}{
+		{
+			name: "soft interface should be ignored",
+			route: systemops.RouteUpdate{
+				Type:        systemops.RouteModified,
+				Destination: netip.PrefixFrom(netip.IPv4Unspecified(), 0),
+				NextHop: systemops.Nexthop{
+					IP: netip.MustParseAddr("192.168.1.1"),
+					Intf: &net.Interface{
+						Name: "ISATAP-Interface", // isSoftInterface checks name
+					},
+				},
+			},
+			nexthopv4: systemops.Nexthop{
+				IP: netip.MustParseAddr("192.168.1.2"),
+			},
+			nexthopv6: systemops.Nexthop{
+				IP: netip.MustParseAddr("2001:db8::1"),
+			},
+			expected: false,
+		},
+		{
+			name: "modified route with different v4 nexthop IP should return true",
+			route: systemops.RouteUpdate{
+				Type:        systemops.RouteModified,
+				Destination: netip.PrefixFrom(netip.IPv4Unspecified(), 0),
+				NextHop: systemops.Nexthop{
+					IP: netip.MustParseAddr("192.168.1.1"),
+					Intf: &net.Interface{
+						Index: 1, Name: "eth0",
+					},
+				},
+			},
+			nexthopv4: systemops.Nexthop{
+				IP: netip.MustParseAddr("192.168.1.2"),
+				Intf: &net.Interface{
+					Index: 1, Name: "eth0",
+				},
+			},
+			nexthopv6: systemops.Nexthop{
+				IP: netip.MustParseAddr("2001:db8::1"),
+			},
+			expected: true,
+		},
+		{
+			name: "modified route with same v4 nexthop (IP and Intf Index) should return false",
+			route: systemops.RouteUpdate{
+				Type:        systemops.RouteModified,
+				Destination: netip.PrefixFrom(netip.IPv4Unspecified(), 0),
+				NextHop: systemops.Nexthop{
+					IP: netip.MustParseAddr("192.168.1.1"),
+					Intf: &net.Interface{
+						Index: 1, Name: "eth0",
+					},
+				},
+			},
+			nexthopv4: systemops.Nexthop{
+				IP: netip.MustParseAddr("192.168.1.1"),
+				Intf: &net.Interface{
+					Index: 1, Name: "eth0",
+				},
+			},
+			nexthopv6: systemops.Nexthop{
+				IP: netip.MustParseAddr("2001:db8::1"),
+			},
+			expected: false,
+		},
+		{
+			name: "added route with different v6 nexthop IP should return true",
+			route: systemops.RouteUpdate{
+				Type:        systemops.RouteAdded,
+				Destination: netip.PrefixFrom(netip.IPv6Unspecified(), 0),
+				NextHop: systemops.Nexthop{
+					IP: netip.MustParseAddr("2001:db8::2"),
+					Intf: &net.Interface{
+						Index: 1, Name: "eth0",
+					},
+				},
+			},
+			nexthopv4: systemops.Nexthop{
+				IP: netip.MustParseAddr("192.168.1.1"),
+			},
+			nexthopv6: systemops.Nexthop{
+				IP: netip.MustParseAddr("2001:db8::1"),
+				Intf: &net.Interface{
+					Index: 1, Name: "eth0",
+				},
+			},
+			expected: true,
+		},
+		{
+			name: "added route with same v6 nexthop (IP and Intf Index) should return false",
+			route: systemops.RouteUpdate{
+				Type:        systemops.RouteAdded,
+				Destination: netip.PrefixFrom(netip.IPv6Unspecified(), 0),
+				NextHop: systemops.Nexthop{
+					IP: netip.MustParseAddr("2001:db8::1"),
+					Intf: &net.Interface{
+						Index: 1, Name: "eth0",
+					},
+				},
+			},
+			nexthopv4: systemops.Nexthop{
+				IP: netip.MustParseAddr("192.168.1.1"),
+			},
+			nexthopv6: systemops.Nexthop{
+				IP: netip.MustParseAddr("2001:db8::1"),
+				Intf: &net.Interface{
+					Index: 1, Name: "eth0",
+				},
+			},
+			expected: false,
+		},
+		{
+			name: "deleted route matching tracked v4 nexthop (IP and Intf Index) should return true",
+			route: systemops.RouteUpdate{
+				Type:        systemops.RouteDeleted,
+				Destination: netip.PrefixFrom(netip.IPv4Unspecified(), 0),
+				NextHop: systemops.Nexthop{
+					IP: netip.MustParseAddr("192.168.1.1"),
+					Intf: &net.Interface{
+						Index: 1, Name: "eth0",
+					},
+				},
+			},
+			nexthopv4: systemops.Nexthop{
+				IP: netip.MustParseAddr("192.168.1.1"),
+				Intf: &net.Interface{
+					Index: 1, Name: "eth0",
+				},
+			},
+			nexthopv6: systemops.Nexthop{
+				IP: netip.MustParseAddr("2001:db8::1"),
+			},
+			expected: true,
+		},
+		{
+			name: "deleted route not matching tracked v4 nexthop (different IP) should return false",
+			route: systemops.RouteUpdate{
+				Type:        systemops.RouteDeleted,
+				Destination: netip.PrefixFrom(netip.IPv4Unspecified(), 0),
+				NextHop: systemops.Nexthop{
+					IP: netip.MustParseAddr("192.168.1.3"), // Different IP
+					Intf: &net.Interface{
+						Index: 1, Name: "eth0",
+					},
+				},
+			},
+			nexthopv4: systemops.Nexthop{
+				IP: netip.MustParseAddr("192.168.1.1"),
+				Intf: &net.Interface{
+					Index: 1, Name: "eth0",
+				},
+			},
+			nexthopv6: systemops.Nexthop{
+				IP: netip.MustParseAddr("2001:db8::1"),
+			},
+			expected: false,
+		},
+		{
+			name: "modified v4 route with same IP, different Intf Index should return true",
+			route: systemops.RouteUpdate{
+				Type:        systemops.RouteModified,
+				Destination: netip.PrefixFrom(netip.IPv4Unspecified(), 0),
+				NextHop: systemops.Nexthop{
+					IP:   netip.MustParseAddr("192.168.1.1"),
+					Intf: &net.Interface{Index: 2, Name: "eth1"}, // Different Intf Index
+				},
+			},
+			nexthopv4: systemops.Nexthop{
+				IP:   netip.MustParseAddr("192.168.1.1"),
+				Intf: &net.Interface{Index: 1, Name: "eth0"},
+			},
+			expected: true,
+		},
+		{
+			name: "modified v4 route with same IP, one Intf nil, other non-nil should return true",
+			route: systemops.RouteUpdate{
+				Type:        systemops.RouteModified,
+				Destination: netip.PrefixFrom(netip.IPv4Unspecified(), 0),
+				NextHop: systemops.Nexthop{
+					IP:   netip.MustParseAddr("192.168.1.1"),
+					Intf: nil, // Intf is nil
+				},
+			},
+			nexthopv4: systemops.Nexthop{
+				IP:   netip.MustParseAddr("192.168.1.1"),
+				Intf: &net.Interface{Index: 1, Name: "eth0"}, // Tracked Intf is not nil
+			},
+			expected: true,
+		},
+		{
+			name: "added v4 route with same IP, different Intf Index should return true",
+			route: systemops.RouteUpdate{
+				Type:        systemops.RouteAdded,
+				Destination: netip.PrefixFrom(netip.IPv4Unspecified(), 0),
+				NextHop: systemops.Nexthop{
+					IP:   netip.MustParseAddr("192.168.1.1"),
+					Intf: &net.Interface{Index: 2, Name: "eth1"}, // Different Intf Index
+				},
+			},
+			nexthopv4: systemops.Nexthop{
+				IP:   netip.MustParseAddr("192.168.1.1"),
+				Intf: &net.Interface{Index: 1, Name: "eth0"},
+			},
+			expected: true,
+		},
+		{
+			name: "deleted v4 route with same IP, different Intf Index should return false",
+			route: systemops.RouteUpdate{
+				Type:        systemops.RouteDeleted,
+				Destination: netip.PrefixFrom(netip.IPv4Unspecified(), 0),
+				NextHop: systemops.Nexthop{ // This is the route being deleted
+					IP:   netip.MustParseAddr("192.168.1.1"),
+					Intf: &net.Interface{Index: 1, Name: "eth0"},
+				},
+			},
+			nexthopv4: systemops.Nexthop{ // This is our tracked nexthop
+				IP:   netip.MustParseAddr("192.168.1.1"),
+				Intf: &net.Interface{Index: 2, Name: "eth1"}, // Different Intf Index
+			},
+			expected: false, // Because nexthopv4.Equal(route.NextHop) will be false
+		},
+		{
+			name: "modified v6 route with different IP, same Intf Index should return true",
+			route: systemops.RouteUpdate{
+				Type:        systemops.RouteModified,
+				Destination: netip.PrefixFrom(netip.IPv6Unspecified(), 0),
+				NextHop: systemops.Nexthop{
+					IP:   netip.MustParseAddr("2001:db8::3"), // Different IP
+					Intf: &net.Interface{Index: 1, Name: "eth0"},
+				},
+			},
+			nexthopv6: systemops.Nexthop{
+				IP:   netip.MustParseAddr("2001:db8::1"),
+				Intf: &net.Interface{Index: 1, Name: "eth0"},
+			},
+			expected: true,
+		},
+		{
+			name: "modified v6 route with same IP, different Intf Index should return true",
+			route: systemops.RouteUpdate{
+				Type:        systemops.RouteModified,
+				Destination: netip.PrefixFrom(netip.IPv6Unspecified(), 0),
+				NextHop: systemops.Nexthop{
+					IP:   netip.MustParseAddr("2001:db8::1"),
+					Intf: &net.Interface{Index: 2, Name: "eth1"}, // Different Intf Index
+				},
+			},
+			nexthopv6: systemops.Nexthop{
+				IP:   netip.MustParseAddr("2001:db8::1"),
+				Intf: &net.Interface{Index: 1, Name: "eth0"},
+			},
+			expected: true,
+		},
+		{
+			name: "modified v6 route with same IP, same Intf Index should return false",
+			route: systemops.RouteUpdate{
+				Type:        systemops.RouteModified,
+				Destination: netip.PrefixFrom(netip.IPv6Unspecified(), 0),
+				NextHop: systemops.Nexthop{
+					IP:   netip.MustParseAddr("2001:db8::1"),
+					Intf: &net.Interface{Index: 1, Name: "eth0"},
+				},
+			},
+			nexthopv6: systemops.Nexthop{
+				IP:   netip.MustParseAddr("2001:db8::1"),
+				Intf: &net.Interface{Index: 1, Name: "eth0"},
+			},
+			expected: false,
+		},
+		{
+			name: "deleted v6 route matching tracked nexthop (IP and Intf Index) should return true",
+			route: systemops.RouteUpdate{
+				Type:        systemops.RouteDeleted,
+				Destination: netip.PrefixFrom(netip.IPv6Unspecified(), 0),
+				NextHop: systemops.Nexthop{
+					IP:   netip.MustParseAddr("2001:db8::1"),
+					Intf: &net.Interface{Index: 1, Name: "eth0"},
+				},
+			},
+			nexthopv6: systemops.Nexthop{
+				IP:   netip.MustParseAddr("2001:db8::1"),
+				Intf: &net.Interface{Index: 1, Name: "eth0"},
+			},
+			expected: true,
+		},
+		{
+			name: "deleted v6 route not matching tracked nexthop (different IP) should return false",
+			route: systemops.RouteUpdate{
+				Type:        systemops.RouteDeleted,
+				Destination: netip.PrefixFrom(netip.IPv6Unspecified(), 0),
+				NextHop: systemops.Nexthop{
+					IP:   netip.MustParseAddr("2001:db8::3"), // Different IP
+					Intf: &net.Interface{Index: 1, Name: "eth0"},
+				},
+			},
+			nexthopv6: systemops.Nexthop{
+				IP:   netip.MustParseAddr("2001:db8::1"),
+				Intf: &net.Interface{Index: 1, Name: "eth0"},
+			},
+			expected: false,
+		},
+		{
+			name: "deleted v6 route not matching tracked nexthop (same IP, different Intf Index) should return false",
+			route: systemops.RouteUpdate{
+				Type:        systemops.RouteDeleted,
+				Destination: netip.PrefixFrom(netip.IPv6Unspecified(), 0),
+				NextHop: systemops.Nexthop{ // This is the route being deleted
+					IP:   netip.MustParseAddr("2001:db8::1"),
+					Intf: &net.Interface{Index: 1, Name: "eth0"},
+				},
+			},
+			nexthopv6: systemops.Nexthop{ // This is our tracked nexthop
+				IP:   netip.MustParseAddr("2001:db8::1"),
+				Intf: &net.Interface{Index: 2, Name: "eth1"}, // Different Intf Index
+			},
+			expected: false,
+		},
+		{
+			name: "unknown route type should return false",
+			route: systemops.RouteUpdate{
+				Type:        systemops.RouteUpdateType(99), // Unknown type
+				Destination: netip.PrefixFrom(netip.IPv4Unspecified(), 0),
+				NextHop: systemops.Nexthop{
+					IP:   netip.MustParseAddr("192.168.1.1"),
+					Intf: &net.Interface{Index: 1, Name: "eth0"},
+				},
+			},
+			nexthopv4: systemops.Nexthop{
+				IP:   netip.MustParseAddr("192.168.1.2"), // Different from route.NextHop
+				Intf: &net.Interface{Index: 1, Name: "eth0"},
+			},
+			expected: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := routeChanged(tt.route, tt.nexthopv4, tt.nexthopv6)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
+
+func TestIsSoftInterface(t *testing.T) {
+	tests := []struct {
+		name     string
+		ifname   string
+		expected bool
+	}{
+		{
+			name:     "ISATAP interface should be detected",
+			ifname:   "ISATAP tunnel adapter",
+			expected: true,
+		},
+		{
+			name:     "lowercase soft interface should be detected",
+			ifname:   "isatap.{14A5CF17-CA72-43EC-B4EA-B4B093641B7D}",
+			expected: true,
+		},
+		{
+			name:     "Teredo interface should be detected",
+			ifname:   "Teredo Tunneling Pseudo-Interface",
+			expected: true,
+		},
+		{
+			name:     "regular interface should not be detected as soft",
+			ifname:   "eth0",
+			expected: false,
+		},
+		{
+			name:     "another regular interface should not be detected as soft",
+			ifname:   "wlan0",
+			expected: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := isSoftInterface(tt.ifname)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
diff --git a/client/internal/peer/conn_status_test.go b/client/internal/peer/conn_status_test.go
index 6088df55..e8c5efe5 100644
--- a/client/internal/peer/conn_status_test.go
+++ b/client/internal/peer/conn_status_test.go
@@ -14,7 +14,7 @@ func TestConnStatus_String(t *testing.T) {
 		want   string
 	}{
 		{"StatusConnected", StatusConnected, "Connected"},
-		{"StatusDisconnected", StatusDisconnected, "Disconnected"},
+		{"StatusIdle", StatusIdle, "Idle"},
 		{"StatusConnecting", StatusConnecting, "Connecting"},
 	}
 
@@ -24,5 +24,4 @@ func TestConnStatus_String(t *testing.T) {
 			assert.Equal(t, got, table.want, "they should be equal")
 		})
 	}
-
 }
diff --git a/client/internal/peer/conn_test.go b/client/internal/peer/conn_test.go
index 6d55cfff..c5055e64 100644
--- a/client/internal/peer/conn_test.go
+++ b/client/internal/peer/conn_test.go
@@ -1,7 +1,6 @@
 package peer
 
 import (
-	"context"
 	"fmt"
 	"os"
 	"sync"
@@ -11,6 +10,7 @@ import (
 	"github.com/stretchr/testify/assert"
 
 	"github.com/netbirdio/netbird/client/iface"
+	"github.com/netbirdio/netbird/client/internal/peer/dispatcher"
 	"github.com/netbirdio/netbird/client/internal/peer/guard"
 	"github.com/netbirdio/netbird/client/internal/peer/ice"
 	"github.com/netbirdio/netbird/client/internal/stdnet"
@@ -18,6 +18,8 @@ import (
 	semaphoregroup "github.com/netbirdio/netbird/util/semaphore-group"
 )
 
+var testDispatcher = dispatcher.NewConnectionDispatcher()
+
 var connConf = ConnConfig{
 	Key:         "LLHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
 	LocalKey:    "RRHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
@@ -48,7 +50,13 @@ func TestNewConn_interfaceFilter(t *testing.T) {
 
 func TestConn_GetKey(t *testing.T) {
 	swWatcher := guard.NewSRWatcher(nil, nil, nil, connConf.ICEConfig)
-	conn, err := NewConn(context.Background(), connConf, nil, nil, nil, nil, swWatcher, semaphoregroup.NewSemaphoreGroup(1))
+
+	sd := ServiceDependencies{
+		SrWatcher:          swWatcher,
+		Semaphore:          semaphoregroup.NewSemaphoreGroup(1),
+		PeerConnDispatcher: testDispatcher,
+	}
+	conn, err := NewConn(connConf, sd)
 	if err != nil {
 		return
 	}
@@ -60,7 +68,13 @@ func TestConn_GetKey(t *testing.T) {
 
 func TestConn_OnRemoteOffer(t *testing.T) {
 	swWatcher := guard.NewSRWatcher(nil, nil, nil, connConf.ICEConfig)
-	conn, err := NewConn(context.Background(), connConf, NewRecorder("https://mgm"), nil, nil, nil, swWatcher, semaphoregroup.NewSemaphoreGroup(1))
+	sd := ServiceDependencies{
+		StatusRecorder:     NewRecorder("https://mgm"),
+		SrWatcher:          swWatcher,
+		Semaphore:          semaphoregroup.NewSemaphoreGroup(1),
+		PeerConnDispatcher: testDispatcher,
+	}
+	conn, err := NewConn(connConf, sd)
 	if err != nil {
 		return
 	}
@@ -94,7 +108,13 @@ func TestConn_OnRemoteOffer(t *testing.T) {
 
 func TestConn_OnRemoteAnswer(t *testing.T) {
 	swWatcher := guard.NewSRWatcher(nil, nil, nil, connConf.ICEConfig)
-	conn, err := NewConn(context.Background(), connConf, NewRecorder("https://mgm"), nil, nil, nil, swWatcher, semaphoregroup.NewSemaphoreGroup(1))
+	sd := ServiceDependencies{
+		StatusRecorder:     NewRecorder("https://mgm"),
+		SrWatcher:          swWatcher,
+		Semaphore:          semaphoregroup.NewSemaphoreGroup(1),
+		PeerConnDispatcher: testDispatcher,
+	}
+	conn, err := NewConn(connConf, sd)
 	if err != nil {
 		return
 	}
@@ -125,43 +145,6 @@ func TestConn_OnRemoteAnswer(t *testing.T) {
 
 	wg.Wait()
 }
-func TestConn_Status(t *testing.T) {
-	swWatcher := guard.NewSRWatcher(nil, nil, nil, connConf.ICEConfig)
-	conn, err := NewConn(context.Background(), connConf, NewRecorder("https://mgm"), nil, nil, nil, swWatcher, semaphoregroup.NewSemaphoreGroup(1))
-	if err != nil {
-		return
-	}
-
-	tables := []struct {
-		name        string
-		statusIce   ConnStatus
-		statusRelay ConnStatus
-		want        ConnStatus
-	}{
-		{"StatusConnected", StatusConnected, StatusConnected, StatusConnected},
-		{"StatusDisconnected", StatusDisconnected, StatusDisconnected, StatusDisconnected},
-		{"StatusConnecting", StatusConnecting, StatusConnecting, StatusConnecting},
-		{"StatusConnectingIce", StatusConnecting, StatusDisconnected, StatusConnecting},
-		{"StatusConnectingIceAlternative", StatusConnecting, StatusConnected, StatusConnected},
-		{"StatusConnectingRelay", StatusDisconnected, StatusConnecting, StatusConnecting},
-		{"StatusConnectingRelayAlternative", StatusConnected, StatusConnecting, StatusConnected},
-	}
-
-	for _, table := range tables {
-		t.Run(table.name, func(t *testing.T) {
-			si := NewAtomicConnStatus()
-			si.Set(table.statusIce)
-			conn.statusICE = si
-
-			sr := NewAtomicConnStatus()
-			sr.Set(table.statusRelay)
-			conn.statusRelay = sr
-
-			got := conn.Status()
-			assert.Equal(t, got, table.want, "they should be equal")
-		})
-	}
-}
 
 func TestConn_presharedKey(t *testing.T) {
 	conn1 := Conn{
diff --git a/client/internal/peer/status_test.go b/client/internal/peer/status_test.go
index 931ec900..27263875 100644
--- a/client/internal/peer/status_test.go
+++ b/client/internal/peer/status_test.go
@@ -1,31 +1,35 @@
 package peer
 
 import (
+	"context"
 	"errors"
 	"sync"
 	"testing"
+	"time"
 
 	"github.com/stretchr/testify/assert"
 )
 
 func TestAddPeer(t *testing.T) {
 	key := "abc"
+	ip := "100.108.254.1"
 	status := NewRecorder("https://mgm")
-	err := status.AddPeer(key, "abc.netbird")
+	err := status.AddPeer(key, "abc.netbird", ip)
 	assert.NoError(t, err, "shouldn't return error")
 
 	_, exists := status.peers[key]
 	assert.True(t, exists, "value was found")
 
-	err = status.AddPeer(key, "abc.netbird")
+	err = status.AddPeer(key, "abc.netbird", ip)
 
 	assert.Error(t, err, "should return error on duplicate")
 }
 
 func TestGetPeer(t *testing.T) {
 	key := "abc"
+	ip := "100.108.254.1"
 	status := NewRecorder("https://mgm")
-	err := status.AddPeer(key, "abc.netbird")
+	err := status.AddPeer(key, "abc.netbird", ip)
 	assert.NoError(t, err, "shouldn't return error")
 
 	peerStatus, err := status.GetPeer(key)
@@ -40,16 +44,16 @@ func TestGetPeer(t *testing.T) {
 func TestUpdatePeerState(t *testing.T) {
 	key := "abc"
 	ip := "10.10.10.10"
+	fqdn := "peer-a.netbird.local"
 	status := NewRecorder("https://mgm")
+	_ = status.AddPeer(key, fqdn, ip)
+
 	peerState := State{
-		PubKey: key,
-		Mux:    new(sync.RWMutex),
+		PubKey:           key,
+		ConnStatusUpdate: time.Now(),
+		ConnStatus:       StatusConnecting,
 	}
 
-	status.peers[key] = peerState
-
-	peerState.IP = ip
-
 	err := status.UpdatePeerState(peerState)
 	assert.NoError(t, err, "shouldn't return error")
 
@@ -81,25 +85,27 @@ func TestGetPeerStateChangeNotifierLogic(t *testing.T) {
 	key := "abc"
 	ip := "10.10.10.10"
 	status := NewRecorder("https://mgm")
-	peerState := State{
-		PubKey: key,
-		Mux:    new(sync.RWMutex),
-	}
+	_ = status.AddPeer(key, "abc.netbird", ip)
 
-	status.peers[key] = peerState
+	sub := status.SubscribeToPeerStateChanges(context.Background(), key)
+	assert.NotNil(t, sub, "channel shouldn't be nil")
 
-	ch := status.GetPeerStateChangeNotifier(key)
-	assert.NotNil(t, ch, "channel shouldn't be nil")
-
-	peerState.IP = ip
+	peerState := State{
+		PubKey:           key,
+		ConnStatus:       StatusConnecting,
+		Relayed:          false,
+		ConnStatusUpdate: time.Now(),
+	}
 
 	err := status.UpdatePeerRelayedStateToDisconnected(peerState)
 	assert.NoError(t, err, "shouldn't return error")
 
+	timeoutCtx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
+	defer cancel()
 	select {
-	case <-ch:
-	default:
-		t.Errorf("channel wasn't closed after update")
+	case <-sub.eventsChan:
+	case <-timeoutCtx.Done():
+		t.Errorf("timed out waiting for event")
 	}
 }
 
diff --git a/client/internal/peer/wg_watcher_test.go b/client/internal/peer/wg_watcher_test.go
index 8bfb1af4..d7c277ef 100644
--- a/client/internal/peer/wg_watcher_test.go
+++ b/client/internal/peer/wg_watcher_test.go
@@ -11,26 +11,11 @@ import (
 )
 
 type MocWgIface struct {
-	initial       bool
-	lastHandshake time.Time
-	stop          bool
+	stop bool
 }
 
-func (m *MocWgIface) GetStats(key string) (configurer.WGStats, error) {
-	if !m.initial {
-		m.initial = true
-		return configurer.WGStats{}, nil
-	}
-
-	if !m.stop {
-		m.lastHandshake = time.Now()
-	}
-
-	stats := configurer.WGStats{
-		LastHandshake: m.lastHandshake,
-	}
-
-	return stats, nil
+func (m *MocWgIface) GetStats() (map[string]configurer.WGStats, error) {
+	return map[string]configurer.WGStats{}, nil
 }
 
 func (m *MocWgIface) disconnect() {
diff --git a/client/internal/routemanager/client_test.go b/client/internal/routemanager/client/client_test.go
similarity index 99%
rename from client/internal/routemanager/client_test.go
rename to client/internal/routemanager/client/client_test.go
index 56fcf161..48a9495b 100644
--- a/client/internal/routemanager/client_test.go
+++ b/client/internal/routemanager/client/client_test.go
@@ -1,4 +1,4 @@
-package routemanager
+package client
 
 import (
 	"fmt"
@@ -395,7 +395,7 @@ func TestGetBestrouteFromStatuses(t *testing.T) {
 			}
 
 			// create new clientNetwork
-			client := &clientNetwork{
+			client := &Watcher{
 				handler:       static.NewRoute(&route.Route{Network: netip.MustParsePrefix("192.168.0.0/24")}, nil, nil),
 				routes:        tc.existingRoutes,
 				currentChosen: currentRoute,
diff --git a/client/internal/routemanager/manager_test.go b/client/internal/routemanager/manager_test.go
index 318ef5ae..a46ae080 100644
--- a/client/internal/routemanager/manager_test.go
+++ b/client/internal/routemanager/manager_test.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"fmt"
 	"net/netip"
-	"runtime"
 	"testing"
 
 	"github.com/pion/transport/v3/stdnet"
@@ -45,7 +44,7 @@ func TestManagerUpdateRoutes(t *testing.T) {
 					ID:          "a",
 					NetID:       "routeA",
 					Peer:        remotePeerKey1,
-					Network:     netip.MustParsePrefix("100.64.251.250/30"),
+					Network:     netip.MustParsePrefix("100.64.251.248/30"),
 					NetworkType: route.IPv4Network,
 					Metric:      9999,
 					Masquerade:  false,
@@ -72,7 +71,7 @@ func TestManagerUpdateRoutes(t *testing.T) {
 					ID:          "a",
 					NetID:       "routeA",
 					Peer:        localPeerKey,
-					Network:     netip.MustParsePrefix("100.64.252.250/30"),
+					Network:     netip.MustParsePrefix("100.64.252.248/30"),
 					NetworkType: route.IPv4Network,
 					Metric:      9999,
 					Masquerade:  false,
@@ -100,7 +99,7 @@ func TestManagerUpdateRoutes(t *testing.T) {
 					ID:          "a",
 					NetID:       "routeA",
 					Peer:        localPeerKey,
-					Network:     netip.MustParsePrefix("100.64.30.250/30"),
+					Network:     netip.MustParsePrefix("100.64.30.248/30"),
 					NetworkType: route.IPv4Network,
 					Metric:      9999,
 					Masquerade:  false,
@@ -128,7 +127,7 @@ func TestManagerUpdateRoutes(t *testing.T) {
 					ID:          "a",
 					NetID:       "routeA",
 					Peer:        localPeerKey,
-					Network:     netip.MustParsePrefix("100.64.30.250/30"),
+					Network:     netip.MustParsePrefix("100.64.30.248/30"),
 					NetworkType: route.IPv4Network,
 					Metric:      9999,
 					Masquerade:  false,
@@ -212,7 +211,7 @@ func TestManagerUpdateRoutes(t *testing.T) {
 					ID:          "a",
 					NetID:       "routeA",
 					Peer:        remotePeerKey1,
-					Network:     netip.MustParsePrefix("100.64.251.250/30"),
+					Network:     netip.MustParsePrefix("100.64.251.248/30"),
 					NetworkType: route.IPv4Network,
 					Metric:      9999,
 					Masquerade:  false,
@@ -234,7 +233,7 @@ func TestManagerUpdateRoutes(t *testing.T) {
 					ID:          "a",
 					NetID:       "routeA",
 					Peer:        remotePeerKey1,
-					Network:     netip.MustParsePrefix("100.64.251.250/30"),
+					Network:     netip.MustParsePrefix("100.64.251.248/30"),
 					NetworkType: route.IPv4Network,
 					Metric:      9999,
 					Masquerade:  false,
@@ -251,7 +250,7 @@ func TestManagerUpdateRoutes(t *testing.T) {
 					ID:          "a",
 					NetID:       "routeA",
 					Peer:        remotePeerKey1,
-					Network:     netip.MustParsePrefix("100.64.251.250/30"),
+					Network:     netip.MustParsePrefix("100.64.251.248/30"),
 					NetworkType: route.IPv4Network,
 					Metric:      9999,
 					Masquerade:  false,
@@ -273,7 +272,7 @@ func TestManagerUpdateRoutes(t *testing.T) {
 					ID:          "a",
 					NetID:       "routeA",
 					Peer:        remotePeerKey1,
-					Network:     netip.MustParsePrefix("100.64.251.250/30"),
+					Network:     netip.MustParsePrefix("100.64.251.248/30"),
 					NetworkType: route.IPv4Network,
 					Metric:      9999,
 					Masquerade:  false,
@@ -283,7 +282,7 @@ func TestManagerUpdateRoutes(t *testing.T) {
 					ID:          "b",
 					NetID:       "routeA",
 					Peer:        remotePeerKey2,
-					Network:     netip.MustParsePrefix("100.64.251.250/30"),
+					Network:     netip.MustParsePrefix("100.64.251.248/30"),
 					NetworkType: route.IPv4Network,
 					Metric:      9999,
 					Masquerade:  false,
@@ -300,7 +299,7 @@ func TestManagerUpdateRoutes(t *testing.T) {
 					ID:          "a",
 					NetID:       "routeA",
 					Peer:        remotePeerKey1,
-					Network:     netip.MustParsePrefix("100.64.251.250/30"),
+					Network:     netip.MustParsePrefix("100.64.251.248/30"),
 					NetworkType: route.IPv4Network,
 					Metric:      9999,
 					Masquerade:  false,
@@ -328,7 +327,7 @@ func TestManagerUpdateRoutes(t *testing.T) {
 					ID:          "a",
 					NetID:       "routeA",
 					Peer:        localPeerKey,
-					Network:     netip.MustParsePrefix("100.64.251.250/30"),
+					Network:     netip.MustParsePrefix("100.64.251.248/30"),
 					NetworkType: route.IPv4Network,
 					Metric:      9999,
 					Masquerade:  false,
@@ -357,7 +356,7 @@ func TestManagerUpdateRoutes(t *testing.T) {
 					ID:          "l1",
 					NetID:       "routeA",
 					Peer:        localPeerKey,
-					Network:     netip.MustParsePrefix("100.64.251.250/30"),
+					Network:     netip.MustParsePrefix("100.64.251.248/30"),
 					NetworkType: route.IPv4Network,
 					Metric:      9999,
 					Masquerade:  false,
@@ -377,7 +376,7 @@ func TestManagerUpdateRoutes(t *testing.T) {
 					ID:          "r1",
 					NetID:       "routeA",
 					Peer:        remotePeerKey1,
-					Network:     netip.MustParsePrefix("100.64.251.250/30"),
+					Network:     netip.MustParsePrefix("100.64.251.248/30"),
 					NetworkType: route.IPv4Network,
 					Metric:      9999,
 					Masquerade:  false,
@@ -441,11 +440,11 @@ func TestManagerUpdateRoutes(t *testing.T) {
 			}
 
 			if len(testCase.inputInitRoutes) > 0 {
-				_ = routeManager.UpdateRoutes(testCase.inputSerial, testCase.inputRoutes, false)
+				err = routeManager.UpdateRoutes(testCase.inputSerial, testCase.inputRoutes, false)
 				require.NoError(t, err, "should update routes with init routes")
 			}
 
-			_ = routeManager.UpdateRoutes(testCase.inputSerial+uint64(len(testCase.inputInitRoutes)), testCase.inputRoutes, false)
+			err = routeManager.UpdateRoutes(testCase.inputSerial+uint64(len(testCase.inputInitRoutes)), testCase.inputRoutes, false)
 			require.NoError(t, err, "should update routes")
 
 			expectedWatchers := testCase.clientNetworkWatchersExpected
@@ -454,8 +453,8 @@ func TestManagerUpdateRoutes(t *testing.T) {
 			}
 			require.Len(t, routeManager.clientNetworks, expectedWatchers, "client networks size should match")
 
-			if runtime.GOOS == "linux" && routeManager.serverRouter != nil {
-				require.Len(t, routeManager.serverRouter.routes, testCase.serverRoutesExpected, "server networks size should match")
+			if routeManager.serverRouter != nil {
+				require.Equal(t, testCase.serverRoutesExpected, routeManager.serverRouter.RoutesCount(), "server networks size should match")
 			}
 		})
 	}
diff --git a/client/internal/routemanager/systemops/systemops_bsd_test.go b/client/internal/routemanager/systemops/systemops_bsd_test.go
index 84b84483..0d892c16 100644
--- a/client/internal/routemanager/systemops/systemops_bsd_test.go
+++ b/client/internal/routemanager/systemops/systemops_bsd_test.go
@@ -8,6 +8,8 @@ import (
 	"net/netip"
 	"os/exec"
 	"regexp"
+	"runtime"
+	"strings"
 	"sync"
 	"testing"
 
@@ -24,7 +26,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),
@@ -34,7 +35,12 @@ func init() {
 
 func TestConcurrentRoutes(t *testing.T) {
 	baseIP := netip.MustParseAddr("192.0.2.0")
-	intf := &net.Interface{Name: "lo0"}
+
+	var intf *net.Interface
+	var nexthop Nexthop
+
+	_, intf = setupDummyInterface(t)
+	nexthop = Nexthop{netip.Addr{}, intf}
 
 	r := NewSysOps(nil, nil)
 
@@ -44,7 +50,7 @@ func TestConcurrentRoutes(t *testing.T) {
 		go func(ip netip.Addr) {
 			defer wg.Done()
 			prefix := netip.PrefixFrom(ip, 32)
-			if err := r.addToRouteTable(prefix, Nexthop{netip.Addr{}, intf}); err != nil {
+			if err := r.addToRouteTable(prefix, nexthop); err != nil {
 				t.Errorf("Failed to add route for %s: %v", prefix, err)
 			}
 		}(baseIP)
@@ -60,7 +66,7 @@ func TestConcurrentRoutes(t *testing.T) {
 		go func(ip netip.Addr) {
 			defer wg.Done()
 			prefix := netip.PrefixFrom(ip, 32)
-			if err := r.removeFromRouteTable(prefix, Nexthop{netip.Addr{}, intf}); err != nil {
+			if err := r.removeFromRouteTable(prefix, nexthop); err != nil {
 				t.Errorf("Failed to remove route for %s: %v", prefix, err)
 			}
 		}(baseIP)
@@ -120,18 +126,39 @@ func TestBits(t *testing.T) {
 func createAndSetupDummyInterface(t *testing.T, intf string, ipAddressCIDR string) string {
 	t.Helper()
 
-	err := exec.Command("ifconfig", intf, "alias", ipAddressCIDR).Run()
-	require.NoError(t, err, "Failed to create loopback alias")
+	if runtime.GOOS == "darwin" {
+		err := exec.Command("ifconfig", intf, "alias", ipAddressCIDR).Run()
+		require.NoError(t, err, "Failed to create loopback alias")
+
+		t.Cleanup(func() {
+			err := exec.Command("ifconfig", intf, ipAddressCIDR, "-alias").Run()
+			assert.NoError(t, err, "Failed to remove loopback alias")
+		})
+
+		return intf
+	}
+
+	prefix, err := netip.ParsePrefix(ipAddressCIDR)
+	require.NoError(t, err, "Failed to parse prefix")
+
+	netIntf, err := net.InterfaceByName(intf)
+	require.NoError(t, err, "Failed to get interface by name")
+
+	nexthop := Nexthop{netip.Addr{}, netIntf}
+
+	r := NewSysOps(nil, nil)
+	err = r.addToRouteTable(prefix, nexthop)
+	require.NoError(t, err, "Failed to add route to table")
 
 	t.Cleanup(func() {
-		err := exec.Command("ifconfig", intf, ipAddressCIDR, "-alias").Run()
-		assert.NoError(t, err, "Failed to remove loopback alias")
+		err := r.removeFromRouteTable(prefix, nexthop)
+		assert.NoError(t, err, "Failed to remove route from table")
 	})
 
-	return "lo0"
+	return intf
 }
 
-func addDummyRoute(t *testing.T, dstCIDR string, gw net.IP, _ string) {
+func addDummyRoute(t *testing.T, dstCIDR string, gw netip.Addr, _ string) {
 	t.Helper()
 
 	var originalNexthop net.IP
@@ -177,12 +204,40 @@ func fetchOriginalGateway() (net.IP, error) {
 	return net.ParseIP(matches[1]), nil
 }
 
+// setupDummyInterface creates a dummy tun interface for FreeBSD route testing
+func setupDummyInterface(t *testing.T) (netip.Addr, *net.Interface) {
+	t.Helper()
+
+	if runtime.GOOS == "darwin" {
+		return netip.AddrFrom4([4]byte{192, 168, 1, 2}), &net.Interface{Name: "lo0"}
+	}
+
+	output, err := exec.Command("ifconfig", "tun", "create").CombinedOutput()
+	require.NoError(t, err, "Failed to create tun interface: %s", string(output))
+
+	tunName := strings.TrimSpace(string(output))
+
+	output, err = exec.Command("ifconfig", tunName, "192.168.1.1", "netmask", "255.255.0.0", "192.168.1.2", "up").CombinedOutput()
+	require.NoError(t, err, "Failed to configure tun interface: %s", string(output))
+
+	intf, err := net.InterfaceByName(tunName)
+	require.NoError(t, err, "Failed to get interface by name")
+
+	t.Cleanup(func() {
+		if err := exec.Command("ifconfig", tunName, "destroy").Run(); err != nil {
+			t.Logf("Failed to destroy tun interface %s: %v", tunName, err)
+		}
+	})
+
+	return netip.AddrFrom4([4]byte{192, 168, 1, 2}), intf
+}
+
 func setupDummyInterfacesAndRoutes(t *testing.T) {
 	t.Helper()
 
 	defaultDummy := createAndSetupDummyInterface(t, expectedExternalInt, "192.168.0.1/24")
-	addDummyRoute(t, "0.0.0.0/0", net.IPv4(192, 168, 0, 1), defaultDummy)
+	addDummyRoute(t, "0.0.0.0/0", netip.AddrFrom4([4]byte{192, 168, 0, 1}), defaultDummy)
 
 	otherDummy := createAndSetupDummyInterface(t, expectedInternalInt, "192.168.1.1/24")
-	addDummyRoute(t, "10.0.0.0/8", net.IPv4(192, 168, 1, 1), otherDummy)
+	addDummyRoute(t, "10.0.0.0/8", netip.AddrFrom4([4]byte{192, 168, 1, 1}), otherDummy)
 }
diff --git a/client/internal/routemanager/systemops/systemops_generic_test.go b/client/internal/routemanager/systemops/systemops_generic_test.go
index 5b7b13f9..2a57e604 100644
--- a/client/internal/routemanager/systemops/systemops_generic_test.go
+++ b/client/internal/routemanager/systemops/systemops_generic_test.go
@@ -3,23 +3,25 @@
 package systemops
 
 import (
-	"bytes"
 	"context"
+	"errors"
 	"fmt"
 	"net"
 	"net/netip"
-	"os"
+	"os/exec"
 	"runtime"
+	"strconv"
 	"strings"
+	"syscall"
 	"testing"
 
 	"github.com/pion/transport/v3/stdnet"
-	log "github.com/sirupsen/logrus"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
 
 	"github.com/netbirdio/netbird/client/iface"
+	"github.com/netbirdio/netbird/client/internal/routemanager/vars"
 )
 
 type dialer interface {
@@ -27,327 +29,401 @@ type dialer interface {
 	DialContext(ctx context.Context, network, address string) (net.Conn, error)
 }
 
-func TestAddRemoveRoutes(t *testing.T) {
+func TestAddVPNRoute(t *testing.T) {
 	testCases := []struct {
-		name                   string
-		prefix                 netip.Prefix
-		shouldRouteToWireguard bool
-		shouldBeRemoved        bool
+		name        string
+		prefix      netip.Prefix
+		expectError bool
 	}{
 		{
-			name:                   "Should Add And Remove Route 100.66.120.0/24",
-			prefix:                 netip.MustParsePrefix("100.66.120.0/24"),
-			shouldRouteToWireguard: true,
-			shouldBeRemoved:        true,
+			name:   "IPv4 - Private network route",
+			prefix: netip.MustParsePrefix("10.10.100.0/24"),
 		},
 		{
-			name:                   "Should Not Add Or Remove Route 127.0.0.1/32",
-			prefix:                 netip.MustParsePrefix("127.0.0.1/32"),
-			shouldRouteToWireguard: false,
-			shouldBeRemoved:        false,
+			name:   "IPv4 Single host",
+			prefix: netip.MustParsePrefix("10.111.111.111/32"),
+		},
+		{
+			name:   "IPv4 RFC3927 test range",
+			prefix: netip.MustParsePrefix("198.51.100.0/24"),
+		},
+		{
+			name:   "IPv4 Default route",
+			prefix: netip.MustParsePrefix("0.0.0.0/0"),
+		},
+
+		{
+			name:   "IPv6 Subnet",
+			prefix: netip.MustParsePrefix("fdb1:848a:7e16::/48"),
+		},
+		{
+			name:   "IPv6 Single host",
+			prefix: netip.MustParsePrefix("fdb1:848a:7e16:a::b/128"),
+		},
+		{
+			name:   "IPv6 Default route",
+			prefix: netip.MustParsePrefix("::/0"),
+		},
+
+		// IPv4 addresses that should be rejected (matches validateRoute logic)
+		{
+			name:        "IPv4 Loopback",
+			prefix:      netip.MustParsePrefix("127.0.0.1/32"),
+			expectError: true,
+		},
+		{
+			name:        "IPv4 Link-local unicast",
+			prefix:      netip.MustParsePrefix("169.254.1.1/32"),
+			expectError: true,
+		},
+		{
+			name:        "IPv4 Link-local multicast",
+			prefix:      netip.MustParsePrefix("224.0.0.251/32"),
+			expectError: true,
+		},
+		{
+			name:        "IPv4 Multicast",
+			prefix:      netip.MustParsePrefix("239.255.255.250/32"),
+			expectError: true,
+		},
+		{
+			name:        "IPv4 Unspecified with prefix",
+			prefix:      netip.MustParsePrefix("0.0.0.0/32"),
+			expectError: true,
+		},
+
+		// IPv6 addresses that should be rejected (matches validateRoute logic)
+		{
+			name:        "IPv6 Loopback",
+			prefix:      netip.MustParsePrefix("::1/128"),
+			expectError: true,
+		},
+		{
+			name:        "IPv6 Link-local unicast",
+			prefix:      netip.MustParsePrefix("fe80::1/128"),
+			expectError: true,
+		},
+		{
+			name:        "IPv6 Link-local multicast",
+			prefix:      netip.MustParsePrefix("ff02::1/128"),
+			expectError: true,
+		},
+		{
+			name:        "IPv6 Interface-local multicast",
+			prefix:      netip.MustParsePrefix("ff01::1/128"),
+			expectError: true,
+		},
+		{
+			name:        "IPv6 Multicast",
+			prefix:      netip.MustParsePrefix("ff00::1/128"),
+			expectError: true,
+		},
+		{
+			name:        "IPv6 Unspecified with prefix",
+			prefix:      netip.MustParsePrefix("::/128"),
+			expectError: true,
+		},
+
+		{
+			name:        "IPv4 WireGuard interface network overlap",
+			prefix:      netip.MustParsePrefix("100.65.75.0/24"),
+			expectError: true,
+		},
+		{
+			name:        "IPv4 WireGuard interface network subnet",
+			prefix:      netip.MustParsePrefix("100.65.75.0/32"),
+			expectError: true,
 		},
 	}
 
 	for n, testCase := range testCases {
-		// todo resolve test execution on freebsd
-		if runtime.GOOS == "freebsd" {
-			t.Skip("skipping ", testCase.name, " on freebsd")
-		}
 		t.Run(testCase.name, func(t *testing.T) {
 			t.Setenv("NB_DISABLE_ROUTE_CACHE", "true")
 
-			peerPrivateKey, _ := wgtypes.GeneratePrivateKey()
-			newNet, err := stdnet.NewNet()
-			if err != nil {
-				t.Fatal(err)
-			}
-			opts := iface.WGIFaceOpts{
-				IFaceName:    fmt.Sprintf("utun53%d", n),
-				Address:      "100.65.75.2/24",
-				WGPrivKey:    peerPrivateKey.String(),
-				MTU:          iface.DefaultMTU,
-				TransportNet: newNet,
-			}
-			wgInterface, err := iface.NewWGIFace(opts)
-			require.NoError(t, err, "should create testing WGIface interface")
-			defer wgInterface.Close()
-
-			err = wgInterface.Create()
-			require.NoError(t, err, "should create testing wireguard interface")
+			wgInterface := createWGInterface(t, fmt.Sprintf("utun53%d", n), "100.65.75.2/24", 33100+n)
 
 			r := NewSysOps(wgInterface, nil)
-
-			_, _, err = r.SetupRouting(nil, nil)
+			_, _, err := r.SetupRouting(nil, nil)
 			require.NoError(t, err)
 			t.Cleanup(func() {
 				assert.NoError(t, r.CleanupRouting(nil))
 			})
 
-			index, err := net.InterfaceByName(wgInterface.Name())
-			require.NoError(t, err, "InterfaceByName should not return err")
-			intf := &net.Interface{Index: index.Index, Name: wgInterface.Name()}
+			intf, err := net.InterfaceByName(wgInterface.Name())
+			require.NoError(t, err)
 
+			// add the route
 			err = r.AddVPNRoute(testCase.prefix, intf)
-			require.NoError(t, err, "genericAddVPNRoute should not return err")
-
-			if testCase.shouldRouteToWireguard {
-				assertWGOutInterface(t, testCase.prefix, wgInterface, false)
-			} else {
-				assertWGOutInterface(t, testCase.prefix, wgInterface, true)
-			}
-			exists, err := existsInRouteTable(testCase.prefix)
-			require.NoError(t, err, "existsInRouteTable should not return err")
-			if exists && testCase.shouldRouteToWireguard {
-				err = r.RemoveVPNRoute(testCase.prefix, intf)
-				require.NoError(t, err, "genericRemoveVPNRoute should not return err")
-
-				prefixNexthop, err := GetNextHop(testCase.prefix.Addr())
-				require.NoError(t, err, "GetNextHop should not return err")
-
-				internetNexthop, err := GetNextHop(netip.MustParseAddr("0.0.0.0"))
-				require.NoError(t, err)
-
-				if testCase.shouldBeRemoved {
-					require.Equal(t, internetNexthop.IP, prefixNexthop.IP, "route should be pointing to default internet gateway")
-				} else {
-					require.NotEqual(t, internetNexthop.IP, prefixNexthop.IP, "route should be pointing to a different gateway than the internet gateway")
-				}
+			if testCase.expectError {
+				assert.ErrorIs(t, err, vars.ErrRouteNotAllowed)
+				return
 			}
+
+			// validate it's pointing to the WireGuard interface
+			require.NoError(t, err)
+
+			nextHop := getNextHop(t, testCase.prefix.Addr())
+			assert.Equal(t, wgInterface.Name(), nextHop.Intf.Name, "next hop interface should be WireGuard interface")
+
+			// remove route again
+			err = r.RemoveVPNRoute(testCase.prefix, intf)
+			require.NoError(t, err)
+
+			// validate it's gone
+			nextHop, err = GetNextHop(testCase.prefix.Addr())
+			require.True(t,
+				errors.Is(err, vars.ErrRouteNotFound) || err == nil && nextHop.Intf != nil && nextHop.Intf.Name != wgInterface.Name(),
+				"err: %v, next hop: %v", err, nextHop)
 		})
 	}
 }
 
-func TestGetNextHop(t *testing.T) {
-	if runtime.GOOS == "freebsd" {
-		t.Skip("skipping on freebsd")
-	}
-	nexthop, err := GetNextHop(netip.MustParseAddr("0.0.0.0"))
-	if err != nil {
-		t.Fatal("shouldn't return error when fetching the gateway: ", err)
-	}
-	if !nexthop.IP.IsValid() {
-		t.Fatal("should return a gateway")
-	}
-	addresses, err := net.InterfaceAddrs()
-	if err != nil {
-		t.Fatal("shouldn't return error when fetching interface addresses: ", err)
-	}
+func getNextHop(t *testing.T, addr netip.Addr) Nexthop {
+	t.Helper()
 
-	var testingIP string
-	var testingPrefix netip.Prefix
-	for _, address := range addresses {
-		if address.Network() != "ip+net" {
-			continue
+	if runtime.GOOS == "windows" || runtime.GOOS == "linux" {
+		nextHop, err := GetNextHop(addr)
+
+		if runtime.GOOS == "windows" && errors.Is(err, vars.ErrRouteNotFound) && addr.Is6() {
+			// TODO: Fix this test. It doesn't return the route when running in a windows github runner, but it is
+			// present in the route table.
+			t.Skip("Skipping windows test")
 		}
-		prefix := netip.MustParsePrefix(address.String())
-		if !prefix.Addr().IsLoopback() && prefix.Addr().Is4() {
-			testingIP = prefix.Addr().String()
-			testingPrefix = prefix.Masked()
-			break
+
+		require.NoError(t, err)
+		require.NotNil(t, nextHop.Intf, "next hop interface should not be nil for %s", addr)
+
+		return nextHop
+	}
+	// GetNextHop for bsd is buggy and returns the wrong interface for the default route.
+
+	if addr.IsUnspecified() {
+		// On macOS, querying 0.0.0.0 returns the wrong interface
+		if addr.Is4() {
+			addr = netip.MustParseAddr("1.2.3.4")
+		} else {
+			addr = netip.MustParseAddr("2001:db8::1")
 		}
 	}
 
-	localIP, err := GetNextHop(testingPrefix.Addr())
-	if err != nil {
-		t.Fatal("shouldn't return error: ", err)
-	}
-	if !localIP.IP.IsValid() {
-		t.Fatal("should return a gateway for local network")
+	cmd := exec.Command("route", "-n", "get", addr.String())
+	if addr.Is6() {
+		cmd = exec.Command("route", "-n", "get", "-inet6", addr.String())
 	}
-	if localIP.IP.String() == nexthop.IP.String() {
-		t.Fatal("local IP should not match with gateway IP")
+
+	output, err := cmd.CombinedOutput()
+	t.Logf("route output: %s", output)
+	require.NoError(t, err, "%s failed")
+
+	lines := strings.Split(string(output), "\n")
+	var intf string
+	var gateway string
+
+	for _, line := range lines {
+		line = strings.TrimSpace(line)
+		if strings.HasPrefix(line, "interface:") {
+			intf = strings.TrimSpace(strings.TrimPrefix(line, "interface:"))
+		} else if strings.HasPrefix(line, "gateway:") {
+			gateway = strings.TrimSpace(strings.TrimPrefix(line, "gateway:"))
+		}
 	}
-	if localIP.IP.String() != testingIP {
-		t.Fatalf("local IP should match with testing IP: want %s got %s", testingIP, localIP.IP.String())
+
+	require.NotEmpty(t, intf, "interface should be found in route output")
+
+	iface, err := net.InterfaceByName(intf)
+	require.NoError(t, err, "interface %s should exist", intf)
+
+	nexthop := Nexthop{Intf: iface}
+
+	if gateway != "" && gateway != "link#"+strconv.Itoa(iface.Index) {
+		addr, err := netip.ParseAddr(gateway)
+		if err == nil {
+			nexthop.IP = addr
+		}
 	}
+
+	return nexthop
 }
 
-func TestAddExistAndRemoveRoute(t *testing.T) {
-	defaultNexthop, err := GetNextHop(netip.MustParseAddr("0.0.0.0"))
-	t.Log("defaultNexthop: ", defaultNexthop)
-	if err != nil {
-		t.Fatal("shouldn't return error when fetching the gateway: ", err)
-	}
+func TestAddRouteToNonVPNIntf(t *testing.T) {
 	testCases := []struct {
-		name              string
-		prefix            netip.Prefix
-		preExistingPrefix netip.Prefix
-		shouldAddRoute    bool
+		name        string
+		prefix      netip.Prefix
+		expectError bool
+		errorType   error
 	}{
 		{
-			name:           "Should Add And Remove random Route",
-			prefix:         netip.MustParsePrefix("99.99.99.99/32"),
-			shouldAddRoute: true,
+			name:   "IPv4 RFC3927 test range",
+			prefix: netip.MustParsePrefix("198.51.100.0/24"),
+		},
+		{
+			name:   "IPv4 Single host",
+			prefix: netip.MustParsePrefix("8.8.8.8/32"),
+		},
+		{
+			name:   "IPv6 External network route",
+			prefix: netip.MustParsePrefix("2001:db8:1000::/48"),
+		},
+		{
+			name:   "IPv6 Single host",
+			prefix: netip.MustParsePrefix("2001:db8::1/128"),
+		},
+		{
+			name:   "IPv6 Subnet",
+			prefix: netip.MustParsePrefix("2a05:d014:1f8d::/48"),
+		},
+		{
+			name:   "IPv6 Single host",
+			prefix: netip.MustParsePrefix("2a05:d014:1f8d:7302:ebca:ec15:b24d:d07e/128"),
+		},
+
+		// Addresses that should be rejected
+		{
+			name:        "IPv4 Loopback",
+			prefix:      netip.MustParsePrefix("127.0.0.1/32"),
+			expectError: true,
+			errorType:   vars.ErrRouteNotAllowed,
+		},
+		{
+			name:        "IPv4 Link-local unicast",
+			prefix:      netip.MustParsePrefix("169.254.1.1/32"),
+			expectError: true,
+			errorType:   vars.ErrRouteNotAllowed,
+		},
+		{
+			name:        "IPv4 Multicast",
+			prefix:      netip.MustParsePrefix("239.255.255.250/32"),
+			expectError: true,
+			errorType:   vars.ErrRouteNotAllowed,
 		},
 		{
-			name:           "Should Not Add Route if overlaps with default gateway",
-			prefix:         netip.MustParsePrefix(defaultNexthop.IP.String() + "/31"),
-			shouldAddRoute: false,
+			name:        "IPv4 Unspecified",
+			prefix:      netip.MustParsePrefix("0.0.0.0/0"),
+			expectError: true,
+			errorType:   vars.ErrRouteNotAllowed,
 		},
 		{
-			name:              "Should Add Route if bigger network exists",
-			prefix:            netip.MustParsePrefix("100.100.100.0/24"),
-			preExistingPrefix: netip.MustParsePrefix("100.100.0.0/16"),
-			shouldAddRoute:    true,
+			name:        "IPv6 Loopback",
+			prefix:      netip.MustParsePrefix("::1/128"),
+			expectError: true,
+			errorType:   vars.ErrRouteNotAllowed,
 		},
 		{
-			name:              "Should Add Route if smaller network exists",
-			prefix:            netip.MustParsePrefix("100.100.0.0/16"),
-			preExistingPrefix: netip.MustParsePrefix("100.100.100.0/24"),
-			shouldAddRoute:    true,
+			name:        "IPv6 Link-local unicast",
+			prefix:      netip.MustParsePrefix("fe80::1/128"),
+			expectError: true,
+			errorType:   vars.ErrRouteNotAllowed,
 		},
 		{
-			name:              "Should Not Add Route if same network exists",
-			prefix:            netip.MustParsePrefix("100.100.0.0/16"),
-			preExistingPrefix: netip.MustParsePrefix("100.100.0.0/16"),
-			shouldAddRoute:    false,
+			name:        "IPv6 Multicast",
+			prefix:      netip.MustParsePrefix("ff00::1/128"),
+			expectError: true,
+			errorType:   vars.ErrRouteNotAllowed,
+		},
+		{
+			name:        "IPv6 Unspecified",
+			prefix:      netip.MustParsePrefix("::/0"),
+			expectError: true,
+			errorType:   vars.ErrRouteNotAllowed,
+		},
+		{
+			name:        "IPv4 WireGuard interface network overlap",
+			prefix:      netip.MustParsePrefix("100.65.75.0/24"),
+			expectError: true,
+			errorType:   vars.ErrRouteNotAllowed,
 		},
 	}
 
 	for n, testCase := range testCases {
-
-		var buf bytes.Buffer
-		log.SetOutput(&buf)
-		defer func() {
-			log.SetOutput(os.Stderr)
-		}()
 		t.Run(testCase.name, func(t *testing.T) {
-			t.Setenv("NB_USE_LEGACY_ROUTING", "true")
 			t.Setenv("NB_DISABLE_ROUTE_CACHE", "true")
 
-			peerPrivateKey, _ := wgtypes.GeneratePrivateKey()
-			newNet, err := stdnet.NewNet()
-			if err != nil {
-				t.Fatal(err)
-			}
-			opts := iface.WGIFaceOpts{
-				IFaceName:    fmt.Sprintf("utun53%d", n),
-				Address:      "100.65.75.2/24",
-				WGPort:       33100,
-				WGPrivKey:    peerPrivateKey.String(),
-				MTU:          iface.DefaultMTU,
-				TransportNet: newNet,
-			}
-			wgInterface, err := iface.NewWGIFace(opts)
-			require.NoError(t, err, "should create testing WGIface interface")
-			defer wgInterface.Close()
-
-			err = wgInterface.Create()
-			require.NoError(t, err, "should create testing wireguard interface")
-
-			index, err := net.InterfaceByName(wgInterface.Name())
-			require.NoError(t, err, "InterfaceByName should not return err")
-			intf := &net.Interface{Index: index.Index, Name: wgInterface.Name()}
+			wgInterface := createWGInterface(t, fmt.Sprintf("utun54%d", n), "100.65.75.2/24", 33200+n)
 
 			r := NewSysOps(wgInterface, nil)
+			_, _, err := r.SetupRouting(nil, nil)
+			require.NoError(t, err)
+			t.Cleanup(func() {
+				assert.NoError(t, r.CleanupRouting(nil))
+			})
+
+			initialNextHopV4, err := GetNextHop(netip.IPv4Unspecified())
+			require.NoError(t, err, "Should be able to get IPv4 default route")
+			t.Logf("Initial IPv4 next hop: %s", initialNextHopV4)
 
-			// Prepare the environment
-			if testCase.preExistingPrefix.IsValid() {
-				err := r.AddVPNRoute(testCase.preExistingPrefix, intf)
-				require.NoError(t, err, "should not return err when adding pre-existing route")
+			initialNextHopV6, err := GetNextHop(netip.IPv6Unspecified())
+			if testCase.prefix.Addr().Is6() &&
+				(errors.Is(err, vars.ErrRouteNotFound) || initialNextHopV6.Intf != nil && strings.HasPrefix(initialNextHopV6.Intf.Name, "utun")) {
+				t.Skip("Skipping test as no ipv6 default route is available")
+			}
+			if err != nil && !errors.Is(err, vars.ErrRouteNotFound) {
+				t.Fatalf("Failed to get IPv6 default route: %v", err)
 			}
 
-			// Add the route
-			err = r.AddVPNRoute(testCase.prefix, intf)
-			require.NoError(t, err, "should not return err when adding route")
+			var initialNextHop Nexthop
+			if testCase.prefix.Addr().Is6() {
+				initialNextHop = initialNextHopV6
+			} else {
+				initialNextHop = initialNextHopV4
+			}
 
-			if testCase.shouldAddRoute {
-				// test if route exists after adding
-				ok, err := existsInRouteTable(testCase.prefix)
-				require.NoError(t, err, "should not return err")
-				require.True(t, ok, "route should exist")
+			nexthop, err := r.addRouteToNonVPNIntf(testCase.prefix, wgInterface, initialNextHop)
 
-				// remove route again if added
-				err = r.RemoveVPNRoute(testCase.prefix, intf)
-				require.NoError(t, err, "should not return err")
+			if testCase.expectError {
+				require.ErrorIs(t, err, vars.ErrRouteNotAllowed)
+				return
 			}
+			require.NoError(t, err)
+			t.Logf("Next hop for %s: %s", testCase.prefix, nexthop)
 
-			// route should either not have been added or should have been removed
-			// In case of already existing route, it should not have been added (but still exist)
-			ok, err := existsInRouteTable(testCase.prefix)
-			t.Log("Buffer string: ", buf.String())
-			require.NoError(t, err, "should not return err")
+			// Verify the route was added and points to non-VPN interface
+			currentNextHop, err := GetNextHop(testCase.prefix.Addr())
+			require.NoError(t, err)
+			assert.NotEqual(t, wgInterface.Name(), currentNextHop.Intf.Name, "Route should not point to VPN interface")
 
-			if !strings.Contains(buf.String(), "because it already exists") {
-				require.False(t, ok, "route should not exist")
-			}
+			err = r.removeFromRouteTable(testCase.prefix, nexthop)
+			assert.NoError(t, err)
 		})
 	}
 }
 
-func TestIsSubRange(t *testing.T) {
-	addresses, err := net.InterfaceAddrs()
+func TestGetNextHop(t *testing.T) {
+	defaultNh, err := GetNextHop(netip.MustParseAddr("0.0.0.0"))
 	if err != nil {
-		t.Fatal("shouldn't return error when fetching interface addresses: ", err)
-	}
-
-	var subRangeAddressPrefixes []netip.Prefix
-	var nonSubRangeAddressPrefixes []netip.Prefix
-	for _, address := range addresses {
-		p := netip.MustParsePrefix(address.String())
-		if !p.Addr().IsLoopback() && p.Addr().Is4() && p.Bits() < 32 {
-			p2 := netip.PrefixFrom(p.Masked().Addr(), p.Bits()+1)
-			subRangeAddressPrefixes = append(subRangeAddressPrefixes, p2)
-			nonSubRangeAddressPrefixes = append(nonSubRangeAddressPrefixes, p.Masked())
-		}
-	}
-
-	for _, prefix := range subRangeAddressPrefixes {
-		isSubRangePrefix, err := isSubRange(prefix)
-		if err != nil {
-			t.Fatal("shouldn't return error when checking if address is sub-range: ", err)
-		}
-		if !isSubRangePrefix {
-			t.Fatalf("address %s should be sub-range of an existing route in the table", prefix)
-		}
+		t.Fatal("shouldn't return error when fetching the gateway: ", err)
 	}
-
-	for _, prefix := range nonSubRangeAddressPrefixes {
-		isSubRangePrefix, err := isSubRange(prefix)
-		if err != nil {
-			t.Fatal("shouldn't return error when checking if address is sub-range: ", err)
-		}
-		if isSubRangePrefix {
-			t.Fatalf("address %s should not be sub-range of an existing route in the table", prefix)
-		}
+	if !defaultNh.IP.IsValid() {
+		t.Fatal("should return a gateway")
 	}
-}
-
-func TestExistsInRouteTable(t *testing.T) {
 	addresses, err := net.InterfaceAddrs()
 	if err != nil {
 		t.Fatal("shouldn't return error when fetching interface addresses: ", err)
 	}
 
-	var addressPrefixes []netip.Prefix
+	var testingPrefix netip.Prefix
 	for _, address := range addresses {
-		p := netip.MustParsePrefix(address.String())
-
-		switch {
-		case p.Addr().Is6():
-			continue
-		// Windows sometimes has hidden interface link local addrs that don't turn up on any interface
-		case runtime.GOOS == "windows" && p.Addr().IsLinkLocalUnicast():
-			continue
-		// Linux loopback 127/8 is in the local table, not in the main table and always takes precedence
-		case runtime.GOOS == "linux" && p.Addr().IsLoopback():
-			continue
-		// FreeBSD loopback 127/8 is not added to the routing table
-		case runtime.GOOS == "freebsd" && p.Addr().IsLoopback():
+		if address.Network() != "ip+net" {
 			continue
-		default:
-			addressPrefixes = append(addressPrefixes, p.Masked())
+		}
+		prefix := netip.MustParsePrefix(address.String())
+		if !prefix.Addr().IsLoopback() && prefix.Addr().Is4() {
+			testingPrefix = prefix.Masked()
+			break
 		}
 	}
 
-	for _, prefix := range addressPrefixes {
-		exists, err := existsInRouteTable(prefix)
-		if err != nil {
-			t.Fatal("shouldn't return error when checking if address exists in route table: ", err)
-		}
-		if !exists {
-			t.Fatalf("address %s should exist in route table", prefix)
-		}
+	nh, err := GetNextHop(testingPrefix.Addr())
+	if err != nil {
+		t.Fatal("shouldn't return error: ", err)
+	}
+	if nh.Intf == nil {
+		t.Fatal("should return a gateway for local network")
+	}
+	if nh.IP.String() == defaultNh.IP.String() {
+		t.Fatal("next hop IP should not match with default gateway IP")
+	}
+	if nh.Intf.Name != defaultNh.Intf.Name {
+		t.Fatalf("next hop interface name should match with default gateway interface name, got: %s, want: %s", nh.Intf.Name, defaultNh.Intf.Name)
 	}
 }
 
@@ -384,11 +460,16 @@ func createWGInterface(t *testing.T, interfaceName, ipAddressCIDR string, listen
 func setupRouteAndCleanup(t *testing.T, r *SysOps, prefix netip.Prefix, intf *net.Interface) {
 	t.Helper()
 
-	err := r.AddVPNRoute(prefix, intf)
-	require.NoError(t, err, "addVPNRoute should not return err")
+	if err := r.AddVPNRoute(prefix, intf); err != nil {
+		if !errors.Is(err, syscall.EEXIST) && !errors.Is(err, vars.ErrRouteNotAllowed) {
+			t.Fatalf("addVPNRoute should not return err: %v", err)
+		}
+		t.Logf("addVPNRoute %v returned: %v", prefix, err)
+	}
 	t.Cleanup(func() {
-		err = r.RemoveVPNRoute(prefix, intf)
-		assert.NoError(t, err, "removeVPNRoute should not return err")
+		if err := r.RemoveVPNRoute(prefix, intf); err != nil && !errors.Is(err, vars.ErrRouteNotAllowed) {
+			t.Fatalf("removeVPNRoute should not return err: %v", err)
+		}
 	})
 }
 
@@ -422,28 +503,10 @@ func setupTestEnv(t *testing.T) {
 	// 10.10.0.0/24 more specific route exists in vpn table
 	setupRouteAndCleanup(t, r, netip.MustParsePrefix("10.10.0.0/24"), intf)
 
-	// 127.0.10.0/24 more specific route exists in vpn table
-	setupRouteAndCleanup(t, r, netip.MustParsePrefix("127.0.10.0/24"), intf)
-
 	// unique route in vpn table
 	setupRouteAndCleanup(t, r, netip.MustParsePrefix("172.16.0.0/12"), intf)
 }
 
-func assertWGOutInterface(t *testing.T, prefix netip.Prefix, wgIface *iface.WGIface, invert bool) {
-	t.Helper()
-	if runtime.GOOS == "linux" && prefix.Addr().IsLoopback() {
-		return
-	}
-
-	prefixNexthop, err := GetNextHop(prefix.Addr())
-	require.NoError(t, err, "GetNextHop should not return err")
-	if invert {
-		assert.NotEqual(t, wgIface.Address().IP.String(), prefixNexthop.IP.String(), "route should not point to wireguard interface IP")
-	} else {
-		assert.Equal(t, wgIface.Address().IP.String(), prefixNexthop.IP.String(), "route should point to wireguard interface IP")
-	}
-}
-
 func TestIsVpnRoute(t *testing.T) {
 	tests := []struct {
 		name           string
diff --git a/client/internal/routemanager/systemops/systemops_linux_test.go b/client/internal/routemanager/systemops/systemops_linux_test.go
index 8f12740d..880296d9 100644
--- a/client/internal/routemanager/systemops/systemops_linux_test.go
+++ b/client/internal/routemanager/systemops/systemops_linux_test.go
@@ -19,7 +19,6 @@ import (
 )
 
 var expectedVPNint = "wgtest0"
-var expectedLoopbackInt = "lo"
 var expectedExternalInt = "dummyext0"
 var expectedInternalInt = "dummyint0"
 
@@ -27,18 +26,10 @@ 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 +125,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 +146,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)
 
-	link, err := netlink.LinkByName(intf)
-	require.NoError(t, err)
-	linkIndex := link.Attrs().Index
+			// add new route
+			assert.NoError(t, netlink.RouteAdd(route))
 
-	route := &netlink.Route{
-		Dst:       dstIPNet,
-		Gw:        gw,
-		LinkIndex: linkIndex,
+			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
+		}
 	}
+
 	err = netlink.RouteDel(route)
 	if err != nil && !errors.Is(err, syscall.ESRCH) {
 		t.Logf("Failed to delete route: %v", err)
@@ -180,7 +173,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 +182,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_test.go b/client/internal/routemanager/systemops/systemops_test.go
new file mode 100644
index 00000000..1d1f7883
--- /dev/null
+++ b/client/internal/routemanager/systemops/systemops_test.go
@@ -0,0 +1,268 @@
+package systemops
+
+import (
+	"net/netip"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/netbirdio/netbird/client/iface/wgaddr"
+	"github.com/netbirdio/netbird/client/internal/routemanager/notifier"
+	"github.com/netbirdio/netbird/client/internal/routemanager/vars"
+)
+
+type mockWGIface struct {
+	address wgaddr.Address
+	name    string
+}
+
+func (m *mockWGIface) Address() wgaddr.Address {
+	return m.address
+}
+
+func (m *mockWGIface) Name() string {
+	return m.name
+}
+
+func TestSysOps_validateRoute(t *testing.T) {
+	wgNetwork := netip.MustParsePrefix("10.0.0.0/24")
+	mockWG := &mockWGIface{
+		address: wgaddr.Address{
+			IP:      wgNetwork.Addr(),
+			Network: wgNetwork,
+		},
+		name: "wg0",
+	}
+
+	sysOps := &SysOps{
+		wgInterface: mockWG,
+		notifier:    &notifier.Notifier{},
+	}
+
+	tests := []struct {
+		name        string
+		prefix      string
+		expectError bool
+	}{
+		// Valid routes
+		{
+			name:        "valid IPv4 route",
+			prefix:      "192.168.1.0/24",
+			expectError: false,
+		},
+		{
+			name:        "valid IPv6 route",
+			prefix:      "2001:db8::/32",
+			expectError: false,
+		},
+		{
+			name:        "valid single IPv4 host",
+			prefix:      "8.8.8.8/32",
+			expectError: false,
+		},
+		{
+			name:        "valid single IPv6 host",
+			prefix:      "2001:4860:4860::8888/128",
+			expectError: false,
+		},
+
+		// Invalid routes - loopback
+		{
+			name:        "IPv4 loopback",
+			prefix:      "127.0.0.1/32",
+			expectError: true,
+		},
+		{
+			name:        "IPv6 loopback",
+			prefix:      "::1/128",
+			expectError: true,
+		},
+
+		// Invalid routes - link-local unicast
+		{
+			name:        "IPv4 link-local unicast",
+			prefix:      "169.254.1.1/32",
+			expectError: true,
+		},
+		{
+			name:        "IPv6 link-local unicast",
+			prefix:      "fe80::1/128",
+			expectError: true,
+		},
+
+		// Invalid routes - multicast
+		{
+			name:        "IPv4 multicast",
+			prefix:      "224.0.0.1/32",
+			expectError: true,
+		},
+		{
+			name:        "IPv6 multicast",
+			prefix:      "ff02::1/128",
+			expectError: true,
+		},
+
+		// Invalid routes - link-local multicast
+		{
+			name:        "IPv4 link-local multicast",
+			prefix:      "224.0.0.0/24",
+			expectError: true,
+		},
+		{
+			name:        "IPv6 link-local multicast",
+			prefix:      "ff02::/16",
+			expectError: true,
+		},
+
+		// Invalid routes - interface-local multicast (IPv6 only)
+		{
+			name:        "IPv6 interface-local multicast",
+			prefix:      "ff01::1/128",
+			expectError: true,
+		},
+
+		// Invalid routes - overlaps with WG interface network
+		{
+			name:        "overlaps with WG network - exact match",
+			prefix:      "10.0.0.0/24",
+			expectError: true,
+		},
+		{
+			name:        "overlaps with WG network - subset",
+			prefix:      "10.0.0.1/32",
+			expectError: true,
+		},
+		{
+			name:        "overlaps with WG network - host in range",
+			prefix:      "10.0.0.100/32",
+			expectError: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			prefix, err := netip.ParsePrefix(tt.prefix)
+			require.NoError(t, err, "Failed to parse test prefix %s", tt.prefix)
+
+			err = sysOps.validateRoute(prefix)
+
+			if tt.expectError {
+				require.Error(t, err, "validateRoute() expected error for %s", tt.prefix)
+				assert.Equal(t, vars.ErrRouteNotAllowed, err, "validateRoute() expected ErrRouteNotAllowed for %s", tt.prefix)
+			} else {
+				assert.NoError(t, err, "validateRoute() expected no error for %s", tt.prefix)
+			}
+		})
+	}
+}
+
+func TestSysOps_validateRoute_SubnetOverlap(t *testing.T) {
+	wgNetwork := netip.MustParsePrefix("192.168.100.0/24")
+	mockWG := &mockWGIface{
+		address: wgaddr.Address{
+			IP:      wgNetwork.Addr(),
+			Network: wgNetwork,
+		},
+		name: "wg0",
+	}
+
+	sysOps := &SysOps{
+		wgInterface: mockWG,
+		notifier:    &notifier.Notifier{},
+	}
+
+	tests := []struct {
+		name        string
+		prefix      string
+		expectError bool
+		description string
+	}{
+		{
+			name:        "identical subnet",
+			prefix:      "192.168.100.0/24",
+			expectError: true,
+			description: "exact same network as WG interface",
+		},
+		{
+			name:        "broader subnet containing WG network",
+			prefix:      "192.168.0.0/16",
+			expectError: false,
+			description: "broader network that contains WG network should be allowed",
+		},
+		{
+			name:        "host within WG network",
+			prefix:      "192.168.100.50/32",
+			expectError: true,
+			description: "specific host within WG network",
+		},
+		{
+			name:        "subnet within WG network",
+			prefix:      "192.168.100.128/25",
+			expectError: true,
+			description: "smaller subnet within WG network",
+		},
+		{
+			name:        "adjacent subnet - same /23",
+			prefix:      "192.168.101.0/24",
+			expectError: false,
+			description: "adjacent subnet, no overlap",
+		},
+		{
+			name:        "adjacent subnet - different /16",
+			prefix:      "192.167.100.0/24",
+			expectError: false,
+			description: "different network, no overlap",
+		},
+		{
+			name:        "WG network broadcast address",
+			prefix:      "192.168.100.255/32",
+			expectError: true,
+			description: "broadcast address of WG network",
+		},
+		{
+			name:        "WG network first usable",
+			prefix:      "192.168.100.1/32",
+			expectError: true,
+			description: "first usable address in WG network",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			prefix, err := netip.ParsePrefix(tt.prefix)
+			require.NoError(t, err, "Failed to parse test prefix %s", tt.prefix)
+
+			err = sysOps.validateRoute(prefix)
+
+			if tt.expectError {
+				require.Error(t, err, "validateRoute() expected error for %s (%s)", tt.prefix, tt.description)
+				assert.Equal(t, vars.ErrRouteNotAllowed, err, "validateRoute() expected ErrRouteNotAllowed for %s (%s)", tt.prefix, tt.description)
+			} else {
+				assert.NoError(t, err, "validateRoute() expected no error for %s (%s)", tt.prefix, tt.description)
+			}
+		})
+	}
+}
+
+func TestSysOps_validateRoute_InvalidPrefix(t *testing.T) {
+	wgNetwork := netip.MustParsePrefix("10.0.0.0/24")
+	mockWG := &mockWGIface{
+		address: wgaddr.Address{
+			IP:      wgNetwork.Addr(),
+			Network: wgNetwork,
+		},
+		name: "wt0",
+	}
+
+	sysOps := &SysOps{
+		wgInterface: mockWG,
+		notifier:    &notifier.Notifier{},
+	}
+
+	var invalidPrefix netip.Prefix
+	err := sysOps.validateRoute(invalidPrefix)
+
+	require.Error(t, err, "validateRoute() expected error for invalid prefix")
+	assert.Equal(t, vars.ErrRouteNotAllowed, err, "validateRoute() expected ErrRouteNotAllowed for invalid prefix")
+}
diff --git a/client/internal/routemanager/systemops/systemops_unix_test.go b/client/internal/routemanager/systemops/systemops_unix_test.go
index d88c1ab6..ad37f611 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/routemanager/systemops/systemops_windows_test.go b/client/internal/routemanager/systemops/systemops_windows_test.go
index 19b00601..523bd0b0 100644
--- a/client/internal/routemanager/systemops/systemops_windows_test.go
+++ b/client/internal/routemanager/systemops/systemops_windows_test.go
@@ -5,18 +5,23 @@ import (
 	"encoding/json"
 	"fmt"
 	"net"
+	"net/netip"
 	"os/exec"
 	"strings"
 	"testing"
 	"time"
 
+	log "github.com/sirupsen/logrus"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
 	nbnet "github.com/netbirdio/netbird/util/net"
 )
 
-var expectedExtInt = "Ethernet1"
+var (
+	expectedExternalInt = "Ethernet1"
+	expectedVPNint      = "wgtest0"
+)
 
 type RouteInfo struct {
 	NextHop        string `json:"nexthop"`
@@ -43,8 +48,6 @@ type testCase struct {
 	dialer             dialer
 }
 
-var expectedVPNint = "wgtest0"
-
 var testCases = []testCase{
 	{
 		name:               "To external host without custom dialer via vpn",
@@ -52,14 +55,14 @@ var testCases = []testCase{
 		expectedSourceIP:   "100.64.0.1",
 		expectedDestPrefix: "128.0.0.0/1",
 		expectedNextHop:    "0.0.0.0",
-		expectedInterface:  "wgtest0",
+		expectedInterface:  expectedVPNint,
 		dialer:             &net.Dialer{},
 	},
 	{
 		name:               "To external host with custom dialer via physical interface",
 		destination:        "192.0.2.1:53",
 		expectedDestPrefix: "192.0.2.1/32",
-		expectedInterface:  expectedExtInt,
+		expectedInterface:  expectedExternalInt,
 		dialer:             nbnet.NewDialer(),
 	},
 
@@ -67,24 +70,15 @@ var testCases = []testCase{
 		name:               "To duplicate internal route with custom dialer via physical interface",
 		destination:        "10.0.0.2:53",
 		expectedDestPrefix: "10.0.0.2/32",
-		expectedInterface:  expectedExtInt,
+		expectedInterface:  expectedExternalInt,
 		dialer:             nbnet.NewDialer(),
 	},
-	{
-		name:               "To duplicate internal route without custom dialer via physical interface", // local route takes precedence
-		destination:        "10.0.0.2:53",
-		expectedSourceIP:   "127.0.0.1",
-		expectedDestPrefix: "10.0.0.0/8",
-		expectedNextHop:    "0.0.0.0",
-		expectedInterface:  "Loopback Pseudo-Interface 1",
-		dialer:             &net.Dialer{},
-	},
 
 	{
 		name:               "To unique vpn route with custom dialer via physical interface",
 		destination:        "172.16.0.2:53",
 		expectedDestPrefix: "172.16.0.2/32",
-		expectedInterface:  expectedExtInt,
+		expectedInterface:  expectedExternalInt,
 		dialer:             nbnet.NewDialer(),
 	},
 	{
@@ -93,7 +87,7 @@ var testCases = []testCase{
 		expectedSourceIP:   "100.64.0.1",
 		expectedDestPrefix: "172.16.0.0/12",
 		expectedNextHop:    "0.0.0.0",
-		expectedInterface:  "wgtest0",
+		expectedInterface:  expectedVPNint,
 		dialer:             &net.Dialer{},
 	},
 
@@ -103,22 +97,14 @@ var testCases = []testCase{
 		expectedSourceIP:   "100.64.0.1",
 		expectedDestPrefix: "10.10.0.0/24",
 		expectedNextHop:    "0.0.0.0",
-		expectedInterface:  "wgtest0",
-		dialer:             &net.Dialer{},
-	},
-
-	{
-		name:               "To more specific route (local) without custom dialer via physical interface",
-		destination:        "127.0.10.2:53",
-		expectedSourceIP:   "127.0.0.1",
-		expectedDestPrefix: "127.0.0.0/8",
-		expectedNextHop:    "0.0.0.0",
-		expectedInterface:  "Loopback Pseudo-Interface 1",
+		expectedInterface:  expectedVPNint,
 		dialer:             &net.Dialer{},
 	},
 }
 
 func TestRouting(t *testing.T) {
+	log.SetLevel(log.DebugLevel)
+
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {
 			setupTestEnv(t)
@@ -129,7 +115,7 @@ func TestRouting(t *testing.T) {
 			require.NoError(t, err, "Failed to fetch interface IP")
 
 			output := testRoute(t, tc.destination, tc.dialer)
-			if tc.expectedInterface == expectedExtInt {
+			if tc.expectedInterface == expectedExternalInt {
 				verifyOutput(t, output, ip, tc.expectedDestPrefix, route.NextHop, route.InterfaceAlias)
 			} else {
 				verifyOutput(t, output, tc.expectedSourceIP, tc.expectedDestPrefix, tc.expectedNextHop, tc.expectedInterface)
@@ -242,19 +228,23 @@ func setupDummyInterfacesAndRoutes(t *testing.T) {
 func addDummyRoute(t *testing.T, dstCIDR string) {
 	t.Helper()
 
-	script := fmt.Sprintf(`New-NetRoute -DestinationPrefix "%s" -InterfaceIndex 1 -PolicyStore ActiveStore`, dstCIDR)
-
-	output, err := exec.Command("powershell", "-Command", script).CombinedOutput()
+	prefix, err := netip.ParsePrefix(dstCIDR)
 	if err != nil {
-		t.Logf("Failed to add dummy route: %v\nOutput: %s", err, output)
-		t.FailNow()
+		t.Fatalf("Failed to parse destination CIDR %s: %v", dstCIDR, err)
+	}
+
+	nexthop := Nexthop{
+		Intf: &net.Interface{Index: 1},
+	}
+
+	if err = addRoute(prefix, nexthop); err != nil {
+		t.Fatalf("Failed to add dummy route: %v", err)
 	}
 
 	t.Cleanup(func() {
-		script = fmt.Sprintf(`Remove-NetRoute -DestinationPrefix  "%s" -InterfaceIndex 1 -Confirm:$false`, dstCIDR)
-		output, err := exec.Command("powershell", "-Command", script).CombinedOutput()
+		err := deleteRoute(prefix, nexthop)
 		if err != nil {
-			t.Logf("Failed to remove dummy route: %v\nOutput: %s", err, output)
+			t.Logf("Failed to remove dummy route: %v", err)
 		}
 	})
 }
diff --git a/client/internal/routeselector/routeselector_test.go b/client/internal/routeselector/routeselector_test.go
index a1461dff..cfa72324 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,7 +314,7 @@ func TestRouteSelector_NewRoutesBehavior(t *testing.T) {
 				rs.SelectAllRoutes()
 				return rs.DeselectRoutes([]route.NetID{"route1"}, initialRoutes)
 			},
-			// After deselecting specific routes, new routes should remain unselected
+			// After deselecting specific routes, new routes should be selected
 			wantNewSelected: []route.NetID{"route2", "route3", "route4", "route5"},
 		},
 		{
@@ -323,8 +322,8 @@ func TestRouteSelector_NewRoutesBehavior(t *testing.T) {
 			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"},
 		},
 	}
 
@@ -428,3 +427,213 @@ func TestRouteSelector_MixedSelectionDeselection(t *testing.T) {
 		})
 	}
 }
+
+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/server/debug_test.go b/client/server/debug_test.go
new file mode 100644
index 00000000..53d9ac8e
--- /dev/null
+++ b/client/server/debug_test.go
@@ -0,0 +1,49 @@
+package server
+
+import (
+	"context"
+	"errors"
+	"net/http"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/netbirdio/netbird/upload-server/server"
+	"github.com/netbirdio/netbird/upload-server/types"
+)
+
+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)
+		}
+	})
+
+	file := filepath.Join(t.TempDir(), "tmpfile")
+	fileContent := []byte("test file content")
+	err := os.WriteFile(file, fileContent, 0640)
+	require.NoError(t, err)
+	key, err := uploadDebugBundle(context.Background(), testURL+types.GetURLPath, testURL, file)
+	require.NoError(t, err)
+	id := getURLHash(testURL)
+	require.Contains(t, key, id+"/")
+	expectedFilePath := filepath.Join(testDir, key)
+	createdFileContent, err := os.ReadFile(expectedFilePath)
+	require.NoError(t, err)
+	require.Equal(t, fileContent, createdFileContent)
+}
diff --git a/client/status/status_test.go b/client/status/status_test.go
index e48b441f..33eda4b9 100644
--- a/client/status/status_test.go
+++ b/client/status/status_test.go
@@ -383,7 +383,8 @@ func TestParsingToJSON(t *testing.T) {
               "error": "timeout"
             }
           ],
-          "events": []
+          "events": [],
+          "lazyConnectionEnabled": false
         }`
 	// @formatter:on
 
@@ -484,6 +485,7 @@ dnsServers:
       enabled: false
       error: timeout
 events: []
+lazyConnectionEnabled: false
 `
 
 	assert.Equal(t, expectedYAML, yaml)
@@ -548,6 +550,7 @@ FQDN: some-localhost.awesome-domain.com
 NetBird IP: 192.168.178.100/16
 Interface type: Kernel
 Quantum resistance: false
+Lazy connection: false
 Networks: 10.10.0.0/24
 Forwarding rules: 0
 Peers count: 2/2 Connected
@@ -570,6 +573,7 @@ FQDN: some-localhost.awesome-domain.com
 NetBird IP: 192.168.178.100/16
 Interface type: Kernel
 Quantum resistance: false
+Lazy connection: false
 Networks: 10.10.0.0/24
 Forwarding rules: 0
 Peers count: 2/2 Connected
diff --git a/infrastructure_files/tests/setup.env b/infrastructure_files/tests/setup.env
index 2945e1c4..dc1d6e01 100644
--- a/infrastructure_files/tests/setup.env
+++ b/infrastructure_files/tests/setup.env
@@ -28,3 +28,4 @@ NETBIRD_MGMT_IDP_SIGNKEY_REFRESH=$CI_NETBIRD_MGMT_IDP_SIGNKEY_REFRESH
 NETBIRD_TURN_EXTERNAL_IP=1.2.3.4
 NETBIRD_RELAY_PORT=33445
 NETBIRD_AUTH_PKCE_DISABLE_PROMPT_LOGIN=true
+NETBIRD_AUTH_PKCE_LOGIN_FLAG=0
diff --git a/management/client/rest/accounts_test.go b/management/client/rest/accounts_test.go
index f6d48d87..d2ace4ec 100644
--- a/management/client/rest/accounts_test.go
+++ b/management/client/rest/accounts_test.go
@@ -66,6 +66,15 @@ func TestAccounts_List_Err(t *testing.T) {
 	})
 }
 
+func TestAccounts_List_ConnErr(t *testing.T) {
+	withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+		ret, err := c.Accounts.List(context.Background())
+		assert.Error(t, err)
+		assert.Contains(t, err.Error(), "404")
+		assert.Empty(t, ret)
+	})
+}
+
 func TestAccounts_Update_200(t *testing.T) {
 	withMockClient(func(c *rest.Client, mux *http.ServeMux) {
 		mux.HandleFunc("/api/accounts/Test", func(w http.ResponseWriter, r *http.Request) {
diff --git a/management/client/rest/impersonation_test.go b/management/client/rest/impersonation_test.go
new file mode 100644
index 00000000..69c0f972
--- /dev/null
+++ b/management/client/rest/impersonation_test.go
@@ -0,0 +1,77 @@
+//go:build integration
+// +build integration
+
+package rest_test
+
+import (
+	"context"
+	"encoding/json"
+	"net/http"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/netbirdio/netbird/management/client/rest"
+	"github.com/netbirdio/netbird/management/server/http/api"
+)
+
+var (
+	testImpersonatedAccount = api.Account{
+		Id: "ImpersonatedTest",
+		Settings: api.AccountSettings{
+			Extra: &api.AccountExtraSettings{
+				PeerApprovalEnabled: false,
+			},
+			GroupsPropagationEnabled:        ptr(true),
+			JwtGroupsEnabled:                ptr(false),
+			PeerInactivityExpiration:        7,
+			PeerInactivityExpirationEnabled: true,
+			PeerLoginExpiration:             24,
+			PeerLoginExpirationEnabled:      true,
+			RegularUsersViewBlocked:         false,
+			RoutingPeerDnsResolutionEnabled: ptr(false),
+		},
+	}
+)
+
+func TestImpersonation_Peers_List_200(t *testing.T) {
+	withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+		impersonatedClient := c.Impersonate(testImpersonatedAccount.Id)
+		mux.HandleFunc("/api/peers", func(w http.ResponseWriter, r *http.Request) {
+			require.Equal(t, r.URL.Query().Get("account"), testImpersonatedAccount.Id)
+			retBytes, _ := json.Marshal([]api.Peer{testPeer})
+			_, err := w.Write(retBytes)
+			require.NoError(t, err)
+		})
+		ret, err := impersonatedClient.Peers.List(context.Background())
+		require.NoError(t, err)
+		assert.Len(t, ret, 1)
+		assert.Equal(t, testPeer, ret[0])
+	})
+}
+
+func TestImpersonation_Change_Account(t *testing.T) {
+	withMockClient(func(c *rest.Client, mux *http.ServeMux) {
+		impersonatedClient := c.Impersonate(testImpersonatedAccount.Id)
+		mux.HandleFunc("/api/peers", func(w http.ResponseWriter, r *http.Request) {
+			require.Equal(t, r.URL.Query().Get("account"), testImpersonatedAccount.Id)
+			retBytes, _ := json.Marshal([]api.Peer{testPeer})
+			_, err := w.Write(retBytes)
+			require.NoError(t, err)
+		})
+		_, err := impersonatedClient.Peers.List(context.Background())
+		require.NoError(t, err)
+
+		impersonatedClient = impersonatedClient.Impersonate("another-test-account")
+		mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) {
+			require.Equal(t, r.URL.Query().Get("account"), "another-test-account")
+			retBytes, _ := json.Marshal(testPeer)
+			_, err := w.Write(retBytes)
+			require.NoError(t, err)
+		})
+
+		_, err = impersonatedClient.Peers.Get(context.Background(), "Test")
+		require.NoError(t, err)
+	})
+}
diff --git a/management/client/rest/users_test.go b/management/client/rest/users_test.go
index f68c5f08..715eb166 100644
--- a/management/client/rest/users_test.go
+++ b/management/client/rest/users_test.go
@@ -30,11 +30,8 @@ var (
 		Issued:        ptr("api"),
 		LastLogin:     &time.Time{},
 		Name:          "M. Essam",
-		Permissions: &api.UserPermissions{
-			DashboardView: ptr(api.UserPermissionsDashboardViewFull),
-		},
-		Role:   "user",
-		Status: api.UserStatusActive,
+		Role:          "user",
+		Status:        api.UserStatusActive,
 	}
 )
 
diff --git a/management/domain/validate_test.go b/management/domain/validate_test.go
index c9c042d9..30efcd9a 100644
--- a/management/domain/validate_test.go
+++ b/management/domain/validate_test.go
@@ -97,110 +97,89 @@ func TestValidateDomains(t *testing.T) {
 	}
 }
 
-// TestValidateDomainsStrSlice tests the ValidateDomainsStrSlice function.
-func TestValidateDomainsStrSlice(t *testing.T) {
-	// Generate a slice of valid domains up to maxDomains
+func TestValidateDomainsList(t *testing.T) {
 	validDomains := make([]string, maxDomains)
-	for i := 0; i < maxDomains; i++ {
+	for i := range maxDomains {
 		validDomains[i] = fmt.Sprintf("example%d.com", i)
 	}
 
 	tests := []struct {
-		name     string
-		domains  []string
-		expected []string
-		wantErr  bool
+		name    string
+		domains []string
+		wantErr bool
 	}{
 		{
-			name:     "Empty list",
-			domains:  nil,
-			expected: nil,
-			wantErr:  false,
+			name:    "Empty list",
+			domains: nil,
+			wantErr: false,
 		},
 		{
-			name:     "Single valid ASCII domain",
-			domains:  []string{"sub.ex-ample.com"},
-			expected: []string{"sub.ex-ample.com"},
-			wantErr:  false,
+			name:    "Single valid ASCII domain",
+			domains: []string{"sub.ex-ample.com"},
+			wantErr: false,
 		},
 		{
-			name:     "Underscores in labels",
-			domains:  []string{"_jabber._tcp.gmail.com"},
-			expected: []string{"_jabber._tcp.gmail.com"},
-			wantErr:  false,
+			name:    "Underscores in labels",
+			domains: []string{"_jabber._tcp.gmail.com"},
+			wantErr: false,
 		},
 		{
 			// Unlike ValidateDomains (which converts to punycode),
 			// ValidateDomainsStrSlice will fail on non-ASCII domain chars.
-			name:     "Unicode domain fails (no punycode conversion)",
-			domains:  []string{"münchen.de"},
-			expected: nil,
-			wantErr:  true,
+			name:    "Unicode domain fails (no punycode conversion)",
+			domains: []string{"münchen.de"},
+			wantErr: true,
 		},
 		{
-			name:     "Invalid domain format - leading dash",
-			domains:  []string{"-example.com"},
-			expected: nil,
-			wantErr:  true,
+			name:    "Invalid domain format - leading dash",
+			domains: []string{"-example.com"},
+			wantErr: true,
 		},
 		{
-			name:     "Invalid domain format - trailing dash",
-			domains:  []string{"example-.com"},
-			expected: nil,
-			wantErr:  true,
+			name:    "Invalid domain format - trailing dash",
+			domains: []string{"example-.com"},
+			wantErr: true,
 		},
 		{
-			// The function stops on the first invalid domain and returns an error,
-			// so only the first domain is definitely valid, but the second is invalid.
-			name:     "Multiple domains with a valid one, then invalid",
-			domains:  []string{"google.com", "invalid_domain.com-"},
-			expected: []string{"google.com"},
-			wantErr:  true,
+			name:    "Multiple domains with a valid one, then invalid",
+			domains: []string{"google.com", "invalid_domain.com-"},
+			wantErr: true,
 		},
 		{
-			name:     "Valid wildcard domain",
-			domains:  []string{"*.example.com"},
-			expected: []string{"*.example.com"},
-			wantErr:  false,
+			name:    "Valid wildcard domain",
+			domains: []string{"*.example.com"},
+			wantErr: false,
 		},
 		{
-			name:     "Wildcard with leading dot - invalid",
-			domains:  []string{".*.example.com"},
-			expected: nil,
-			wantErr:  true,
+			name:    "Wildcard with leading dot - invalid",
+			domains: []string{".*.example.com"},
+			wantErr: true,
 		},
 		{
-			name:     "Invalid wildcard with multiple asterisks",
-			domains:  []string{"a.*.example.com"},
-			expected: nil,
-			wantErr:  true,
+			name:    "Invalid wildcard with multiple asterisks",
+			domains: []string{"a.*.example.com"},
+			wantErr: true,
 		},
 		{
-			name:     "Exactly maxDomains items (valid)",
-			domains:  validDomains,
-			expected: validDomains,
-			wantErr:  false,
+			name:    "Exactly maxDomains items (valid)",
+			domains: validDomains,
+			wantErr: false,
 		},
 		{
-			name:     "Exceeds maxDomains items",
-			domains:  append(validDomains, "extra.com"),
-			expected: nil,
-			wantErr:  true,
+			name:    "Exceeds maxDomains items",
+			domains: append(validDomains, "extra.com"),
+			wantErr: true,
 		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			got, err := ValidateDomainsStrSlice(tt.domains)
-			// Check if we got an error where expected
+			err := ValidateDomainsList(tt.domains)
 			if tt.wantErr {
 				assert.Error(t, err)
 			} else {
 				assert.NoError(t, err)
 			}
-
-			// Compare the returned domains to what we expect
-			assert.Equal(t, tt.expected, got)
 		})
 	}
 }
diff --git a/management/server/account_test.go b/management/server/account_test.go
index 7f34cf84..5ada28ca 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"
-
 	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/idp"
+	"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"
 )
 
@@ -853,6 +853,42 @@ func TestAccountManager_DeleteAccount(t *testing.T) {
 		t.Fatal(err)
 	}
 
+	account.Users["service-user-1"] = &types.User{
+		Id:            "service-user-1",
+		Role:          types.UserRoleAdmin,
+		IsServiceUser: true,
+		Issued:        types.UserIssuedAPI,
+		PATs: map[string]*types.PersonalAccessToken{
+			"pat-1": {
+				ID:          "pat-1",
+				UserID:      "service-user-1",
+				Name:        "service-user-1",
+				HashedToken: "hashedToken",
+				CreatedAt:   time.Now(),
+			},
+		},
+	}
+	account.Users[userId] = &types.User{
+		Id:            "service-user-2",
+		Role:          types.UserRoleUser,
+		IsServiceUser: true,
+		Issued:        types.UserIssuedAPI,
+		PATs: map[string]*types.PersonalAccessToken{
+			"pat-2": {
+				ID:          "pat-2",
+				UserID:      userId,
+				Name:        userId,
+				HashedToken: "hashedToken",
+				CreatedAt:   time.Now(),
+			},
+		},
+	}
+
+	err = manager.Store.SaveAccount(context.Background(), account)
+	if err != nil {
+		t.Fatal(err)
+	}
+
 	err = manager.DeleteAccount(context.Background(), account.Id, userId)
 	if err != nil {
 		t.Fatal(err)
@@ -862,6 +898,14 @@ func TestAccountManager_DeleteAccount(t *testing.T) {
 	if err == nil {
 		t.Fatal(fmt.Errorf("expected to get an error when trying to get deleted account, got %v", getAccount))
 	}
+
+	pats, err := manager.Store.GetUserPATs(context.Background(), store.LockingStrengthShare, "service-user-1")
+	require.NoError(t, err)
+	assert.Len(t, pats, 0)
+
+	pats, err = manager.Store.GetUserPATs(context.Background(), store.LockingStrengthShare, userId)
+	require.NoError(t, err)
+	assert.Len(t, pats, 0)
 }
 
 func BenchmarkTest_GetAccountWithclaims(b *testing.B) {
@@ -3154,7 +3198,7 @@ func BenchmarkLoginPeer_NewPeer(b *testing.B) {
 	}
 }
 
-func Test_CreateAccountByPrivateDomain(t *testing.T) {
+func Test_GetCreateAccountByPrivateDomain(t *testing.T) {
 	manager, err := createManager(t)
 	if err != nil {
 		t.Fatal(err)
@@ -3165,9 +3209,10 @@ func Test_CreateAccountByPrivateDomain(t *testing.T) {
 	initiatorId := "test-user"
 	domain := "example.com"
 
-	account, err := manager.CreateAccountByPrivateDomain(ctx, initiatorId, domain)
+	account, created, err := manager.GetOrCreateAccountByPrivateDomain(ctx, initiatorId, domain)
 	assert.NoError(t, err)
 
+	assert.True(t, created)
 	assert.False(t, account.IsDomainPrimaryAccount)
 	assert.Equal(t, domain, account.Domain)
 	assert.Equal(t, types.PrivateCategory, account.DomainCategory)
@@ -3176,9 +3221,25 @@ func Test_CreateAccountByPrivateDomain(t *testing.T) {
 	assert.Equal(t, 0, len(account.Users))
 	assert.Equal(t, 0, len(account.SetupKeys))
 
-	// retry should fail
-	_, err = manager.CreateAccountByPrivateDomain(ctx, initiatorId, domain)
-	assert.Error(t, err)
+	// should return a new account because the previous one is not primary
+	account2, created2, err := manager.GetOrCreateAccountByPrivateDomain(ctx, initiatorId, domain)
+	assert.NoError(t, err)
+
+	assert.True(t, created2)
+	assert.False(t, account2.IsDomainPrimaryAccount)
+	assert.Equal(t, domain, account2.Domain)
+	assert.Equal(t, types.PrivateCategory, account2.DomainCategory)
+	assert.Equal(t, initiatorId, account2.CreatedBy)
+	assert.Equal(t, 1, len(account2.Groups))
+	assert.Equal(t, 0, len(account2.Users))
+	assert.Equal(t, 0, len(account2.SetupKeys))
+
+	account, err = manager.UpdateToPrimaryAccount(ctx, account.Id)
+	assert.NoError(t, err)
+	assert.True(t, account.IsDomainPrimaryAccount)
+
+	_, err = manager.UpdateToPrimaryAccount(ctx, account2.Id)
+	assert.Error(t, err, "should not be able to update a second account to primary")
 }
 
 func Test_UpdateToPrimaryAccount(t *testing.T) {
@@ -3192,12 +3253,69 @@ func Test_UpdateToPrimaryAccount(t *testing.T) {
 	initiatorId := "test-user"
 	domain := "example.com"
 
-	account, err := manager.CreateAccountByPrivateDomain(ctx, initiatorId, domain)
+	account, created, err := manager.GetOrCreateAccountByPrivateDomain(ctx, initiatorId, domain)
 	assert.NoError(t, err)
+	assert.True(t, created)
 	assert.False(t, account.IsDomainPrimaryAccount)
+	assert.Equal(t, domain, account.Domain)
 
-	// retry should fail
 	account, err = manager.UpdateToPrimaryAccount(ctx, account.Id)
 	assert.NoError(t, err)
 	assert.True(t, account.IsDomainPrimaryAccount)
+
+	account2, created2, err := manager.GetOrCreateAccountByPrivateDomain(ctx, initiatorId, domain)
+	assert.NoError(t, err)
+	assert.False(t, created2)
+	assert.True(t, account.IsDomainPrimaryAccount)
+	assert.Equal(t, account.Id, account2.Id)
+}
+
+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/sqlite/migration_test.go b/management/server/activity/sqlite/migration_test.go
deleted file mode 100644
index a03774fa..00000000
--- a/management/server/activity/sqlite/migration_test.go
+++ /dev/null
@@ -1,84 +0,0 @@
-package sqlite
-
-import (
-	"context"
-	"database/sql"
-	"path/filepath"
-	"testing"
-	"time"
-
-	_ "github.com/mattn/go-sqlite3"
-	"github.com/netbirdio/netbird/management/server/activity"
-
-	"github.com/stretchr/testify/require"
-)
-
-func setupDatabase(t *testing.T) *sql.DB {
-	t.Helper()
-
-	dbFile := filepath.Join(t.TempDir(), eventSinkDB)
-	db, err := sql.Open("sqlite3", dbFile)
-	require.NoError(t, err, "Failed to open database")
-
-	t.Cleanup(func() {
-		_ = db.Close()
-	})
-
-	_, err = db.Exec(createTableQuery)
-	require.NoError(t, err, "Failed to create events table")
-
-	_, err = db.Exec(`CREATE TABLE deleted_users (id TEXT NOT NULL, email TEXT NOT NULL, name TEXT);`)
-	require.NoError(t, err, "Failed to create deleted_users table")
-
-	return db
-}
-
-func TestMigrate(t *testing.T) {
-	db := setupDatabase(t)
-
-	key, err := GenerateKey()
-	require.NoError(t, err, "Failed to generate key")
-
-	crypt, err := NewFieldEncrypt(key)
-	require.NoError(t, err, "Failed to initialize FieldEncrypt")
-
-	legacyEmail := crypt.LegacyEncrypt("testaccount@test.com")
-	legacyName := crypt.LegacyEncrypt("Test Account")
-
-	_, err = db.Exec(`INSERT INTO events(activity, timestamp, initiator_id, target_id, account_id, meta) VALUES(?, ?, ?, ?, ?, ?)`,
-		activity.UserDeleted, time.Now(), "initiatorID", "targetID", "accountID", "")
-	require.NoError(t, err, "Failed to insert event")
-
-	_, err = db.Exec(`INSERT INTO deleted_users(id, email, name) VALUES(?, ?, ?)`, "targetID", legacyEmail, legacyName)
-	require.NoError(t, err, "Failed to insert legacy encrypted data")
-
-	colExists, err := checkColumnExists(db, "deleted_users", "enc_algo")
-	require.NoError(t, err, "Failed to check if enc_algo column exists")
-	require.False(t, colExists, "enc_algo column should not exist before migration")
-
-	err = migrate(context.Background(), crypt, db)
-	require.NoError(t, err, "Migration failed")
-
-	colExists, err = checkColumnExists(db, "deleted_users", "enc_algo")
-	require.NoError(t, err, "Failed to check if enc_algo column exists after migration")
-	require.True(t, colExists, "enc_algo column should exist after migration")
-
-	var encAlgo string
-	err = db.QueryRow(`SELECT enc_algo FROM deleted_users LIMIT 1`, "").Scan(&encAlgo)
-	require.NoError(t, err, "Failed to select updated data")
-	require.Equal(t, gcmEncAlgo, encAlgo, "enc_algo should be set to 'GCM' after migration")
-
-	store, err := createStore(crypt, db)
-	require.NoError(t, err, "Failed to create store")
-
-	events, err := store.Get(context.Background(), "accountID", 0, 1, false)
-	require.NoError(t, err, "Failed to get events")
-
-	require.Len(t, events, 1, "Should have one event")
-	require.Equal(t, activity.UserDeleted, events[0].Activity, "activity should match")
-	require.Equal(t, "initiatorID", events[0].InitiatorID, "initiator id should match")
-	require.Equal(t, "targetID", events[0].TargetID, "target id should match")
-	require.Equal(t, "accountID", events[0].AccountID, "account id should match")
-	require.Equal(t, "testaccount@test.com", events[0].Meta["email"], "email should match")
-	require.Equal(t, "Test Account", events[0].Meta["username"], "username should match")
-}
diff --git a/management/server/activity/sqlite/crypt_test.go b/management/server/activity/store/crypt_test.go
similarity index 99%
rename from management/server/activity/sqlite/crypt_test.go
rename to management/server/activity/store/crypt_test.go
index aff3a08b..700bbcd6 100644
--- a/management/server/activity/sqlite/crypt_test.go
+++ b/management/server/activity/store/crypt_test.go
@@ -1,4 +1,4 @@
-package sqlite
+package store
 
 import (
 	"bytes"
diff --git a/management/server/activity/store/migration_test.go b/management/server/activity/store/migration_test.go
new file mode 100644
index 00000000..e3261d9f
--- /dev/null
+++ b/management/server/activity/store/migration_test.go
@@ -0,0 +1,143 @@
+package store
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"gorm.io/driver/postgres"
+	"gorm.io/gorm"
+
+	"github.com/netbirdio/netbird/management/server/activity"
+	"github.com/netbirdio/netbird/management/server/migration"
+	"github.com/netbirdio/netbird/management/server/testutil"
+)
+
+const (
+	insertDeletedUserQuery = `INSERT INTO deleted_users (id, email, name, enc_algo) VALUES (?, ?, ?, ?)`
+)
+
+func setupDatabase(t *testing.T) *gorm.DB {
+	t.Helper()
+
+	cleanup, dsn, err := testutil.CreatePostgresTestContainer()
+	require.NoError(t, err, "Failed to create Postgres test container")
+	t.Cleanup(cleanup)
+
+	db, err := gorm.Open(postgres.Open(dsn))
+	require.NoError(t, err)
+
+	sql, err := db.DB()
+	require.NoError(t, err)
+	t.Cleanup(func() {
+		_ = sql.Close()
+	})
+
+	return db
+}
+
+func TestMigrateLegacyEncryptedUsersToGCM(t *testing.T) {
+	db := setupDatabase(t)
+
+	key, err := GenerateKey()
+	require.NoError(t, err, "Failed to generate key")
+
+	crypt, err := NewFieldEncrypt(key)
+	require.NoError(t, err, "Failed to initialize FieldEncrypt")
+
+	t.Run("empty table, no migration required", func(t *testing.T) {
+		require.NoError(t, migrateLegacyEncryptedUsersToGCM(context.Background(), db, crypt))
+		assert.False(t, db.Migrator().HasTable("deleted_users"))
+	})
+
+	require.NoError(t, db.Exec(`CREATE TABLE deleted_users (id TEXT NOT NULL, email TEXT NOT NULL, name TEXT);`).Error)
+	assert.True(t, db.Migrator().HasTable("deleted_users"))
+	assert.False(t, db.Migrator().HasColumn("deleted_users", "enc_algo"))
+
+	require.NoError(t, migration.MigrateNewField[activity.DeletedUser](context.Background(), db, "enc_algo", ""))
+	assert.True(t, db.Migrator().HasColumn("deleted_users", "enc_algo"))
+
+	t.Run("legacy users migration", func(t *testing.T) {
+		legacyEmail := crypt.LegacyEncrypt("test.user@test.com")
+		legacyName := crypt.LegacyEncrypt("Test User")
+
+		require.NoError(t, db.Exec(insertDeletedUserQuery, "user1", legacyEmail, legacyName, "").Error)
+		require.NoError(t, db.Exec(insertDeletedUserQuery, "user2", legacyEmail, legacyName, "legacy").Error)
+
+		require.NoError(t, migrateLegacyEncryptedUsersToGCM(context.Background(), db, crypt))
+
+		var users []activity.DeletedUser
+		require.NoError(t, db.Find(&users).Error)
+		assert.Len(t, users, 2)
+
+		for _, user := range users {
+			assert.Equal(t, gcmEncAlgo, user.EncAlgo)
+
+			decryptedEmail, err := crypt.Decrypt(user.Email)
+			require.NoError(t, err)
+			assert.Equal(t, "test.user@test.com", decryptedEmail)
+
+			decryptedName, err := crypt.Decrypt(user.Name)
+			require.NoError(t, err)
+			require.Equal(t, "Test User", decryptedName)
+		}
+	})
+
+	t.Run("users already migrated, no migration", func(t *testing.T) {
+		encryptedEmail, err := crypt.Encrypt("test.user@test.com")
+		require.NoError(t, err)
+
+		encryptedName, err := crypt.Encrypt("Test User")
+		require.NoError(t, err)
+
+		require.NoError(t, db.Exec(insertDeletedUserQuery, "user3", encryptedEmail, encryptedName, gcmEncAlgo).Error)
+		require.NoError(t, migrateLegacyEncryptedUsersToGCM(context.Background(), db, crypt))
+
+		var users []activity.DeletedUser
+		require.NoError(t, db.Find(&users).Error)
+		assert.Len(t, users, 3)
+
+		for _, user := range users {
+			assert.Equal(t, gcmEncAlgo, user.EncAlgo)
+
+			decryptedEmail, err := crypt.Decrypt(user.Email)
+			require.NoError(t, err)
+			assert.Equal(t, "test.user@test.com", decryptedEmail)
+
+			decryptedName, err := crypt.Decrypt(user.Name)
+			require.NoError(t, err)
+			require.Equal(t, "Test User", decryptedName)
+		}
+	})
+}
+
+func TestMigrateDuplicateDeletedUsers(t *testing.T) {
+	db := setupDatabase(t)
+
+	require.NoError(t, migrateDuplicateDeletedUsers(context.Background(), db))
+	assert.False(t, db.Migrator().HasTable("deleted_users"))
+
+	require.NoError(t, db.Exec(`CREATE TABLE deleted_users (id TEXT NOT NULL, email TEXT NOT NULL, name TEXT, enc_algo TEXT NOT NULL);`).Error)
+	assert.True(t, db.Migrator().HasTable("deleted_users"))
+
+	isPrimaryKey, err := isColumnPrimaryKey[activity.DeletedUser](db, "id")
+	require.NoError(t, err)
+	assert.False(t, isPrimaryKey)
+
+	require.NoError(t, db.Exec(insertDeletedUserQuery, "user1", "email1", "name1", "GCM").Error)
+	require.NoError(t, db.Exec(insertDeletedUserQuery, "user1", "email2", "name2", "GCM").Error)
+	require.NoError(t, migrateDuplicateDeletedUsers(context.Background(), db))
+
+	isPrimaryKey, err = isColumnPrimaryKey[activity.DeletedUser](db, "id")
+	require.NoError(t, err)
+	assert.True(t, isPrimaryKey)
+
+	var users []activity.DeletedUser
+	require.NoError(t, db.Find(&users).Error)
+	assert.Len(t, users, 1)
+	assert.Equal(t, "user1", users[0].ID)
+	assert.Equal(t, "email2", users[0].Email)
+	assert.Equal(t, "name2", users[0].Name)
+	assert.Equal(t, "GCM", users[0].EncAlgo)
+}
diff --git a/management/server/activity/sqlite/sqlite_test.go b/management/server/activity/store/sql_store_test.go
similarity index 90%
rename from management/server/activity/sqlite/sqlite_test.go
rename to management/server/activity/store/sql_store_test.go
index b10f9b58..8c0d159d 100644
--- a/management/server/activity/sqlite/sqlite_test.go
+++ b/management/server/activity/store/sql_store_test.go
@@ -1,4 +1,4 @@
-package sqlite
+package store
 
 import (
 	"context"
@@ -11,10 +11,10 @@ import (
 	"github.com/netbirdio/netbird/management/server/activity"
 )
 
-func TestNewSQLiteStore(t *testing.T) {
+func TestNewSqlStore(t *testing.T) {
 	dataDir := t.TempDir()
 	key, _ := GenerateKey()
-	store, err := NewSQLiteStore(context.Background(), dataDir, key)
+	store, err := NewSqlStore(context.Background(), dataDir, key)
 	if err != nil {
 		t.Fatal(err)
 		return
diff --git a/management/server/cache/idp_test.go b/management/server/cache/idp_test.go
index beefcd9b..3fcfbb11 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/http/handlers/accounts/accounts_handler_test.go b/management/server/http/handlers/accounts/accounts_handler_test.go
index e971a651..fec5140f 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,8 @@ func TestAccounts_AccountsHandler(t *testing.T) {
 				JwtAllowGroups:                  &[]string{},
 				RegularUsersViewBlocked:         true,
 				RoutingPeerDnsResolutionEnabled: br(false),
+				LazyConnectionEnabled:           br(false),
+				DnsDomain:                       sr(""),
 			},
 			expectedArray: true,
 			expectedID:    accountID,
@@ -122,6 +130,8 @@ func TestAccounts_AccountsHandler(t *testing.T) {
 				JwtAllowGroups:                  &[]string{},
 				RegularUsersViewBlocked:         false,
 				RoutingPeerDnsResolutionEnabled: br(false),
+				LazyConnectionEnabled:           br(false),
+				DnsDomain:                       sr(""),
 			},
 			expectedArray: false,
 			expectedID:    accountID,
@@ -142,6 +152,8 @@ func TestAccounts_AccountsHandler(t *testing.T) {
 				JwtAllowGroups:                  &[]string{"test"},
 				RegularUsersViewBlocked:         true,
 				RoutingPeerDnsResolutionEnabled: br(false),
+				LazyConnectionEnabled:           br(false),
+				DnsDomain:                       sr(""),
 			},
 			expectedArray: false,
 			expectedID:    accountID,
@@ -162,6 +174,8 @@ func TestAccounts_AccountsHandler(t *testing.T) {
 				JwtAllowGroups:                  &[]string{},
 				RegularUsersViewBlocked:         true,
 				RoutingPeerDnsResolutionEnabled: br(false),
+				LazyConnectionEnabled:           br(false),
+				DnsDomain:                       sr(""),
 			},
 			expectedArray: false,
 			expectedID:    accountID,
diff --git a/management/server/http/handlers/peers/peers_handler_test.go b/management/server/http/handlers/peers/peers_handler_test.go
index a03c3c29..a1fc13dd 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/users/users_handler_test.go b/management/server/http/handlers/users/users_handler_test.go
index 60495481..58e33a6d 100644
--- a/management/server/http/handlers/users/users_handler_test.go
+++ b/management/server/http/handlers/users/users_handler_test.go
@@ -13,12 +13,16 @@ import (
 
 	"github.com/gorilla/mux"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 
 	nbcontext "github.com/netbirdio/netbird/management/server/context"
 	"github.com/netbirdio/netbird/management/server/http/api"
 	"github.com/netbirdio/netbird/management/server/mock_server"
+	"github.com/netbirdio/netbird/management/server/permissions/modules"
+	"github.com/netbirdio/netbird/management/server/permissions/roles"
 	"github.com/netbirdio/netbird/management/server/status"
 	"github.com/netbirdio/netbird/management/server/types"
+	"github.com/netbirdio/netbird/management/server/users"
 )
 
 const (
@@ -107,7 +111,7 @@ func initUsersTestData() *handler {
 					return nil, status.Errorf(status.NotFound, "user with ID %s does not exists", userID)
 				}
 
-				info, err := update.Copy().ToUserInfo(nil, &types.Settings{RegularUsersViewBlocked: false})
+				info, err := update.Copy().ToUserInfo(nil)
 				if err != nil {
 					return nil, err
 				}
@@ -124,8 +128,8 @@ func initUsersTestData() *handler {
 
 				return nil
 			},
-			GetCurrentUserInfoFunc: func(ctx context.Context, accountID, userID string) (*types.UserInfo, error) {
-				switch userID {
+			GetCurrentUserInfoFunc: func(ctx context.Context, userAuth nbcontext.UserAuth) (*users.UserInfoWithPermissions, error) {
+				switch userAuth.UserId {
 				case "not-found":
 					return nil, status.NewUserNotFoundError("not-found")
 				case "not-of-account":
@@ -135,52 +139,68 @@ func initUsersTestData() *handler {
 				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",
+					return &users.UserInfoWithPermissions{
+						UserInfo: &types.UserInfo{
+							ID:            "owner",
+							Name:          "",
+							Role:          "owner",
+							Status:        "active",
+							IsServiceUser: false,
+							IsBlocked:     false,
+							NonDeletable:  false,
+							Issued:        "api",
 						},
+						Permissions: mergeRolePermissions(roles.Owner),
 					}, 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",
+					return &users.UserInfoWithPermissions{
+						UserInfo: &types.UserInfo{
+							ID:            "regular-user",
+							Name:          "",
+							Role:          "user",
+							Status:        "active",
+							IsServiceUser: false,
+							IsBlocked:     false,
+							NonDeletable:  false,
+							Issued:        "api",
 						},
+						Permissions: mergeRolePermissions(roles.User),
 					}, 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",
+					return &users.UserInfoWithPermissions{
+						UserInfo: &types.UserInfo{
+							ID:            "admin-user",
+							Name:          "",
+							Role:          "admin",
+							Status:        "active",
+							IsServiceUser: false,
+							IsBlocked:     false,
+							NonDeletable:  false,
+							LastLogin:     time.Time{},
+							Issued:        "api",
 						},
+						Permissions: mergeRolePermissions(roles.Admin),
+					}, nil
+				case "restricted-user":
+					return &users.UserInfoWithPermissions{
+						UserInfo: &types.UserInfo{
+							ID:            "restricted-user",
+							Name:          "",
+							Role:          "user",
+							Status:        "active",
+							IsServiceUser: false,
+							IsBlocked:     false,
+							NonDeletable:  false,
+							LastLogin:     time.Time{},
+							Issued:        "api",
+						},
+						Permissions: mergeRolePermissions(roles.User),
+						Restricted:  true,
 					}, nil
 				}
 
-				return nil, fmt.Errorf("user id %s not handled", userID)
+				return nil, fmt.Errorf("user id %s not handled", userAuth.UserId)
 			},
 		},
 	}
@@ -546,6 +566,7 @@ func TestCurrentUser(t *testing.T) {
 		name           string
 		expectedStatus int
 		requestAuth    nbcontext.UserAuth
+		expectedResult *api.User
 	}{
 		{
 			name:           "without auth",
@@ -575,16 +596,78 @@ func TestCurrentUser(t *testing.T) {
 			name:           "owner",
 			requestAuth:    nbcontext.UserAuth{UserId: "owner"},
 			expectedStatus: http.StatusOK,
+			expectedResult: &api.User{
+				Id:            "owner",
+				Role:          "owner",
+				Status:        "active",
+				IsBlocked:     false,
+				IsCurrent:     ptr(true),
+				IsServiceUser: ptr(false),
+				AutoGroups:    []string{},
+				Issued:        ptr("api"),
+				LastLogin:     ptr(time.Time{}),
+				Permissions: &api.UserPermissions{
+					Modules: stringifyPermissionsKeys(mergeRolePermissions(roles.Owner)),
+				},
+			},
 		},
 		{
 			name:           "regular user",
 			requestAuth:    nbcontext.UserAuth{UserId: "regular-user"},
 			expectedStatus: http.StatusOK,
+			expectedResult: &api.User{
+				Id:            "regular-user",
+				Role:          "user",
+				Status:        "active",
+				IsBlocked:     false,
+				IsCurrent:     ptr(true),
+				IsServiceUser: ptr(false),
+				AutoGroups:    []string{},
+				Issued:        ptr("api"),
+				LastLogin:     ptr(time.Time{}),
+				Permissions: &api.UserPermissions{
+					Modules: stringifyPermissionsKeys(mergeRolePermissions(roles.User)),
+				},
+			},
 		},
 		{
 			name:           "admin user",
 			requestAuth:    nbcontext.UserAuth{UserId: "admin-user"},
 			expectedStatus: http.StatusOK,
+			expectedResult: &api.User{
+				Id:            "admin-user",
+				Role:          "admin",
+				Status:        "active",
+				IsBlocked:     false,
+				IsCurrent:     ptr(true),
+				IsServiceUser: ptr(false),
+				AutoGroups:    []string{},
+				Issued:        ptr("api"),
+				LastLogin:     ptr(time.Time{}),
+				Permissions: &api.UserPermissions{
+					Modules: stringifyPermissionsKeys(mergeRolePermissions(roles.Admin)),
+				},
+			},
+		},
+		{
+			name:           "restricted user",
+			requestAuth:    nbcontext.UserAuth{UserId: "restricted-user"},
+			expectedStatus: http.StatusOK,
+			expectedResult: &api.User{
+				Id:            "restricted-user",
+				Role:          "user",
+				Status:        "active",
+				IsBlocked:     false,
+				IsCurrent:     ptr(true),
+				IsServiceUser: ptr(false),
+				AutoGroups:    []string{},
+				Issued:        ptr("api"),
+				LastLogin:     ptr(time.Time{}),
+				Permissions: &api.UserPermissions{
+					IsRestricted: true,
+					Modules:      stringifyPermissionsKeys(mergeRolePermissions(roles.User)),
+				},
+			},
 		},
 	}
 
@@ -603,10 +686,42 @@ func TestCurrentUser(t *testing.T) {
 			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)
+			assert.Equal(t, tc.expectedStatus, rr.Code, "handler returned wrong status code")
+
+			if tc.expectedResult != nil {
+				var result api.User
+				require.NoError(t, json.NewDecoder(res.Body).Decode(&result))
+				assert.EqualValues(t, *tc.expectedResult, result)
 			}
 		})
 	}
 }
+
+func ptr[T any, PT *T](x T) PT {
+	return &x
+}
+
+func mergeRolePermissions(role roles.RolePermissions) roles.Permissions {
+	permissions := roles.Permissions{}
+
+	for k := range modules.All {
+		if rolePermissions, ok := role.Permissions[k]; ok {
+			permissions[k] = rolePermissions
+			continue
+		}
+		permissions[k] = role.AutoAllowNew
+	}
+
+	return permissions
+}
+
+func stringifyPermissionsKeys(permissions roles.Permissions) map[string]map[string]bool {
+	modules := make(map[string]map[string]bool)
+	for module, operations := range permissions {
+		modules[string(module)] = make(map[string]bool)
+		for op, val := range operations {
+			modules[string(module)][string(op)] = val
+		}
+	}
+	return modules
+}
diff --git a/management/server/http/middleware/auth_middleware_test.go b/management/server/http/middleware/auth_middleware_test.go
index 3dc7d51c..2285ed24 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/migration/migration_test.go b/management/server/migration/migration_test.go
index e907d685..94377930 100644
--- a/management/server/migration/migration_test.go
+++ b/management/server/migration/migration_test.go
@@ -227,3 +227,25 @@ func TestMigrateSetupKeyToHashedSetupKey_ForAlreadyMigratedKey_Case2(t *testing.
 
 	assert.Equal(t, "9+FQcmNd2GCxIK+SvHmtp6PPGV4MKEicDS+xuSQmvlE=", key.Key, "Key should be hashed")
 }
+
+func TestDropIndex(t *testing.T) {
+	db := setupDatabase(t)
+
+	err := db.AutoMigrate(&types.SetupKey{})
+	require.NoError(t, err, "Failed to auto-migrate tables")
+
+	err = db.Save(&types.SetupKey{
+		Id:  "1",
+		Key: "9+FQcmNd2GCxIK+SvHmtp6PPGV4MKEicDS+xuSQmvlE=",
+	}).Error
+	require.NoError(t, err, "Failed to insert setup key")
+
+	exist := db.Migrator().HasIndex(&types.SetupKey{}, "idx_setup_keys_account_id")
+	assert.True(t, exist, "Should have the index")
+
+	err = migration.DropIndex[types.SetupKey](context.Background(), db, "idx_setup_keys_account_id")
+	require.NoError(t, err, "Migration should not fail to remove index")
+
+	exist = db.Migrator().HasIndex(&types.SetupKey{}, "idx_setup_keys_account_id")
+	assert.False(t, exist, "Should not have the index")
+}
diff --git a/management/server/nameserver_test.go b/management/server/nameserver_test.go
index 1ba79079..dc4be594 100644
--- a/management/server/nameserver_test.go
+++ b/management/server/nameserver_test.go
@@ -899,13 +899,33 @@ func TestValidateDomain(t *testing.T) {
 			errFunc: require.NoError,
 		},
 		{
-			name:    "Invalid domain name with double hyphen",
-			domain:  "test--example.com",
+			name:    "Valid domain name with only one label",
+			domain:  "example",
+			errFunc: require.NoError,
+		},
+		{
+			name:    "Valid domain name with trailing dot",
+			domain:  "example.",
+			errFunc: require.NoError,
+		},
+		{
+			name:    "Invalid wildcard domain name",
+			domain:  "*.example",
 			errFunc: require.Error,
 		},
 		{
-			name:    "Invalid domain name with only one label",
-			domain:  "com",
+			name:    "Invalid domain name with leading dot",
+			domain:  ".com",
+			errFunc: require.Error,
+		},
+		{
+			name:    "Invalid domain name with dot only",
+			domain:  ".",
+			errFunc: require.Error,
+		},
+		{
+			name:    "Invalid domain name with double hyphen",
+			domain:  "test--example.com",
 			errFunc: require.Error,
 		},
 		{
diff --git a/management/server/peer_test.go b/management/server/peer_test.go
index 406c3e49..3d782f04 100644
--- a/management/server/peer_test.go
+++ b/management/server/peer_test.go
@@ -10,6 +10,7 @@ import (
 	"net/netip"
 	"os"
 	"runtime"
+	"strings"
 	"testing"
 	"time"
 
@@ -1156,8 +1157,8 @@ func TestToSyncResponse(t *testing.T) {
 		},
 	}
 	dnsCache := &DNSConfigCache{}
-
-	response := toSyncResponse(context.Background(), config, peer, turnRelayToken, turnRelayToken, networkMap, dnsName, checks, dnsCache, true, nil)
+	accountSettings := &types.Settings{RoutingPeerDNSResolutionEnabled: true}
+	response := toSyncResponse(context.Background(), config, peer, turnRelayToken, turnRelayToken, networkMap, dnsName, checks, dnsCache, accountSettings, nil)
 
 	assert.NotNil(t, response)
 	// assert peer config
@@ -1290,15 +1291,21 @@ func Test_RegisterPeerByUser(t *testing.T) {
 		Status:     &nbpeer.PeerStatus{Connected: false, LastSeen: time.Now()},
 		SSHEnabled: false,
 		LastLogin:  util.ToPtr(time.Now()),
+		ExtraDNSLabels: []string{
+			"extraLabel1",
+			"extraLabel2",
+		},
 	}
 
 	addedPeer, _, _, err := am.AddPeer(context.Background(), "", existingUserID, newPeer)
 	require.NoError(t, err)
+	assert.Equal(t, newPeer.ExtraDNSLabels, addedPeer.ExtraDNSLabels)
 
 	peer, err := s.GetPeerByPeerPubKey(context.Background(), store.LockingStrengthShare, addedPeer.Key)
 	require.NoError(t, err)
 	assert.Equal(t, peer.AccountID, existingAccountID)
 	assert.Equal(t, peer.UserID, existingUserID)
+	assert.Equal(t, newPeer.ExtraDNSLabels, peer.ExtraDNSLabels)
 
 	account, err := s.GetAccount(context.Background(), existingAccountID)
 	require.NoError(t, err)
@@ -1339,15 +1346,12 @@ func Test_RegisterPeerBySetupKey(t *testing.T) {
 	assert.NoError(t, err)
 
 	existingAccountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b"
-	existingSetupKeyID := "A2C8E62B-38F5-4553-B31E-DD66C696CEBB"
 
 	_, err = s.GetAccount(context.Background(), existingAccountID)
 	require.NoError(t, err)
 
-	newPeer := &nbpeer.Peer{
-		ID:        xid.New().String(),
+	newPeerTemplate := &nbpeer.Peer{
 		AccountID: existingAccountID,
-		Key:       "newPeerKey",
 		UserID:    "",
 		IP:        net.IP{123, 123, 123, 123},
 		Meta: nbpeer.PeerSystemMeta{
@@ -1358,31 +1362,96 @@ func Test_RegisterPeerBySetupKey(t *testing.T) {
 		DNSLabel:   "newPeer.test",
 		Status:     &nbpeer.PeerStatus{Connected: false, LastSeen: time.Now()},
 		SSHEnabled: false,
+		ExtraDNSLabels: []string{
+			"extraLabel1",
+			"extraLabel2",
+		},
 	}
 
-	addedPeer, _, _, err := am.AddPeer(context.Background(), existingSetupKeyID, "", newPeer)
+	testCases := []struct {
+		name                      string
+		existingSetupKeyID        string
+		expectedGroupIDsInAccount []string
+		expectAddPeerError        bool
+		expectedErrorMsgSubstring string
+	}{
+		{
+			name:                      "Successful registration with setup key allowing extra DNS labels",
+			existingSetupKeyID:        "A2C8E62B-38F5-4553-B31E-DD66C696CEBD",
+			expectAddPeerError:        false,
+			expectedGroupIDsInAccount: []string{"cfefqs706sqkneg59g2g", "cfefqs706sqkneg59g4g"},
+		},
+		{
+			name:                      "Failed registration with setup key not allowing extra DNS labels",
+			existingSetupKeyID:        "A2C8E62B-38F5-4553-B31E-DD66C696CEBB",
+			expectAddPeerError:        true,
+			expectedErrorMsgSubstring: "setup key doesn't allow extra DNS labels",
+		},
+		{
+			name:                      "Absent setup key",
+			existingSetupKeyID:        "AAAAAAAA-38F5-4553-B31E-DD66C696CEBB",
+			expectAddPeerError:        true,
+			expectedErrorMsgSubstring: "failed adding new peer: account not found",
+		},
+	}
 
-	require.NoError(t, err)
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			currentPeer := &nbpeer.Peer{
+				ID:             xid.New().String(),
+				AccountID:      newPeerTemplate.AccountID,
+				Key:            "newPeerKey_" + xid.New().String(),
+				UserID:         newPeerTemplate.UserID,
+				IP:             newPeerTemplate.IP,
+				Meta:           newPeerTemplate.Meta,
+				Name:           newPeerTemplate.Name,
+				DNSLabel:       newPeerTemplate.DNSLabel,
+				Status:         &nbpeer.PeerStatus{Connected: false, LastSeen: time.Now()},
+				SSHEnabled:     newPeerTemplate.SSHEnabled,
+				ExtraDNSLabels: newPeerTemplate.ExtraDNSLabels,
+			}
 
-	peer, err := s.GetPeerByPeerPubKey(context.Background(), store.LockingStrengthShare, newPeer.Key)
-	require.NoError(t, err)
-	assert.Equal(t, peer.AccountID, existingAccountID)
+			addedPeer, _, _, err := am.AddPeer(context.Background(), tc.existingSetupKeyID, "", currentPeer)
 
-	account, err := s.GetAccount(context.Background(), existingAccountID)
-	require.NoError(t, err)
-	assert.Contains(t, account.Peers, addedPeer.ID)
-	assert.Contains(t, account.Groups["cfefqs706sqkneg59g2g"].Peers, addedPeer.ID)
-	assert.Contains(t, account.Groups["cfefqs706sqkneg59g4g"].Peers, addedPeer.ID)
+			if tc.expectAddPeerError {
+				require.Error(t, err, "Expected an error when adding peer with setup key: %s", tc.existingSetupKeyID)
+				assert.Contains(t, err.Error(), tc.expectedErrorMsgSubstring, "Error message mismatch")
+				return
+			}
 
-	assert.Equal(t, uint64(1), account.Network.Serial)
+			require.NoError(t, err, "Expected no error when adding peer with setup key: %s", tc.existingSetupKeyID)
+			assert.NotNil(t, addedPeer, "addedPeer should not be nil on success")
+			assert.Equal(t, currentPeer.ExtraDNSLabels, addedPeer.ExtraDNSLabels, "ExtraDNSLabels mismatch")
 
-	lastUsed, err := time.Parse("2006-01-02T15:04:05Z", "0001-01-01T00:00:00Z")
-	assert.NoError(t, err)
+			peerFromStore, err := s.GetPeerByPeerPubKey(context.Background(), store.LockingStrengthShare, currentPeer.Key)
+			require.NoError(t, err, "Failed to get peer by pub key: %s", currentPeer.Key)
+			assert.Equal(t, existingAccountID, peerFromStore.AccountID, "AccountID mismatch for peer from store")
+			assert.Equal(t, currentPeer.ExtraDNSLabels, peerFromStore.ExtraDNSLabels, "ExtraDNSLabels mismatch for peer from store")
+			assert.Equal(t, addedPeer.ID, peerFromStore.ID, "Peer ID mismatch between addedPeer and peerFromStore")
 
-	hashedKey := sha256.Sum256([]byte(existingSetupKeyID))
-	encodedHashedKey := b64.StdEncoding.EncodeToString(hashedKey[:])
-	assert.NotEqual(t, lastUsed, account.SetupKeys[encodedHashedKey].LastUsed)
-	assert.Equal(t, 1, account.SetupKeys[encodedHashedKey].UsedTimes)
+			account, err := s.GetAccount(context.Background(), existingAccountID)
+			require.NoError(t, err, "Failed to get account: %s", existingAccountID)
+			assert.Contains(t, account.Peers, addedPeer.ID, "Peer ID not found in account.Peers")
+
+			for _, groupID := range tc.expectedGroupIDsInAccount {
+				require.NotNil(t, account.Groups[groupID], "Group %s not found in account", groupID)
+				assert.Contains(t, account.Groups[groupID].Peers, addedPeer.ID, "Peer ID %s not found in group %s", addedPeer.ID, groupID)
+			}
+
+			assert.Equal(t, uint64(1), account.Network.Serial, "Network.Serial mismatch; this assumes specific initial state or increment logic.")
+
+			hashedKey := sha256.Sum256([]byte(tc.existingSetupKeyID))
+			encodedHashedKey := b64.StdEncoding.EncodeToString(hashedKey[:])
+
+			setupKeyData, ok := account.SetupKeys[encodedHashedKey]
+			require.True(t, ok, "Setup key data not found in account.SetupKeys for key ID %s (encoded: %s)", tc.existingSetupKeyID, encodedHashedKey)
+
+			var zeroTime time.Time
+			assert.NotEqual(t, zeroTime, setupKeyData.LastUsed, "Setup key LastUsed time should have been updated and not be zero.")
+
+			assert.Equal(t, 1, setupKeyData.UsedTimes, "Setup key UsedTimes should be 1 after first use.")
+		})
+	}
 
 }
 
@@ -1456,6 +1525,160 @@ func Test_RegisterPeerRollbackOnFailure(t *testing.T) {
 	assert.Equal(t, 0, account.SetupKeys[encodedHashedKey].UsedTimes)
 }
 
+func Test_LoginPeer(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		t.Skip("The SQLite store is not properly supported by Windows yet")
+	}
+
+	s, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "testdata/extended-store.sql", t.TempDir())
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer cleanup()
+
+	eventStore := &activity.InMemoryEventStore{}
+
+	metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
+	assert.NoError(t, err)
+
+	ctrl := gomock.NewController(t)
+	t.Cleanup(ctrl.Finish)
+	settingsMockManager := settings.NewMockManager(ctrl)
+	permissionsManager := permissions.NewManager(s)
+
+	am, err := BuildManager(context.Background(), s, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false, MocIntegratedValidator{}, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManager)
+	assert.NoError(t, err)
+
+	existingAccountID := "bf1c8084-ba50-4ce7-9439-34653001fc3b"
+	_, err = s.GetAccount(context.Background(), existingAccountID)
+	require.NoError(t, err, "Failed to get existing account, check testdata/extended-store.sql. Account ID: %s", existingAccountID)
+
+	baseMeta := nbpeer.PeerSystemMeta{
+		Hostname: "loginPeerHost",
+		GoOS:     "linux",
+	}
+
+	newPeerTemplate := &nbpeer.Peer{
+		AccountID: existingAccountID,
+		UserID:    "",
+		IP:        net.IP{123, 123, 123, 123},
+		Meta: nbpeer.PeerSystemMeta{
+			Hostname: "newPeer",
+			GoOS:     "linux",
+		},
+		Name:       "newPeerName",
+		DNSLabel:   "newPeer.test",
+		Status:     &nbpeer.PeerStatus{Connected: false, LastSeen: time.Now()},
+		SSHEnabled: false,
+		ExtraDNSLabels: []string{
+			"extraLabel1",
+			"extraLabel2",
+		},
+	}
+
+	testCases := []struct {
+		name                         string
+		setupKey                     string
+		wireGuardPubKey              string
+		expectExtraDNSLabelsMismatch bool
+		extraDNSLabels               []string
+		expectLoginError             bool
+		expectedErrorMsgSubstring    string
+	}{
+		{
+			name:             "Successful login with setup key",
+			setupKey:         "A2C8E62B-38F5-4553-B31E-DD66C696CEBD",
+			expectLoginError: false,
+		},
+		{
+			name:                         "Successful login with setup key with DNS labels mismatch",
+			setupKey:                     "A2C8E62B-38F5-4553-B31E-DD66C696CEBD",
+			expectExtraDNSLabelsMismatch: true,
+			extraDNSLabels:               []string{"anotherLabel1", "anotherLabel2"},
+			expectLoginError:             false,
+		},
+		{
+			name:                         "Failed login with setup key not allowing extra DNS labels",
+			setupKey:                     "A2C8E62B-38F5-4553-B31E-DD66C696CEBB",
+			expectExtraDNSLabelsMismatch: true,
+			extraDNSLabels:               []string{"anotherLabel1", "anotherLabel2"},
+			expectLoginError:             true,
+			expectedErrorMsgSubstring:    "setup key doesn't allow extra DNS labels",
+		},
+	}
+
+	for _, tc := range testCases {
+		currentWireGuardPubKey := "testPubKey_" + xid.New().String()
+
+		t.Run(tc.name, func(t *testing.T) {
+			upperKey := strings.ToUpper(tc.setupKey)
+			hashedKey := sha256.Sum256([]byte(upperKey))
+			encodedHashedKey := b64.StdEncoding.EncodeToString(hashedKey[:])
+			sk, err := s.GetSetupKeyBySecret(context.Background(), store.LockingStrengthUpdate, encodedHashedKey)
+			require.NoError(t, err, "Failed to get setup key %s from storage", tc.setupKey)
+
+			currentPeer := &nbpeer.Peer{
+				ID:         xid.New().String(),
+				AccountID:  newPeerTemplate.AccountID,
+				Key:        currentWireGuardPubKey,
+				UserID:     newPeerTemplate.UserID,
+				IP:         newPeerTemplate.IP,
+				Meta:       newPeerTemplate.Meta,
+				Name:       newPeerTemplate.Name,
+				DNSLabel:   newPeerTemplate.DNSLabel,
+				Status:     &nbpeer.PeerStatus{Connected: false, LastSeen: time.Now()},
+				SSHEnabled: newPeerTemplate.SSHEnabled,
+			}
+			// add peer manually to bypass creation during login stage
+			if sk.AllowExtraDNSLabels {
+				currentPeer.ExtraDNSLabels = newPeerTemplate.ExtraDNSLabels
+			}
+			_, _, _, err = am.AddPeer(context.Background(), tc.setupKey, "", currentPeer)
+			require.NoError(t, err, "Expected no error when adding peer with setup key: %s", tc.setupKey)
+
+			loginInput := types.PeerLogin{
+				WireGuardPubKey: currentWireGuardPubKey,
+				SSHKey:          "test-ssh-key",
+				Meta:            baseMeta,
+				UserID:          "",
+				SetupKey:        tc.setupKey,
+				ConnectionIP:    net.ParseIP("192.0.2.100"),
+			}
+
+			if tc.expectExtraDNSLabelsMismatch {
+				loginInput.ExtraDNSLabels = tc.extraDNSLabels
+			}
+
+			loggedinPeer, networkMap, postureChecks, loginErr := am.LoginPeer(context.Background(), loginInput)
+			if tc.expectLoginError {
+				require.Error(t, loginErr, "Expected an error during LoginPeer with setup key: %s", tc.setupKey)
+				assert.Contains(t, loginErr.Error(), tc.expectedErrorMsgSubstring, "Error message mismatch")
+				assert.Nil(t, loggedinPeer, "LoggedinPeer should be nil on error")
+				assert.Nil(t, networkMap, "NetworkMap should be nil on error")
+				assert.Nil(t, postureChecks, "PostureChecks should be empty or nil on error")
+				return
+			}
+
+			require.NoError(t, loginErr, "Expected no error during LoginPeer with setup key: %s", tc.setupKey)
+			assert.NotNil(t, loggedinPeer, "loggedinPeer should not be nil on success")
+			if tc.expectExtraDNSLabelsMismatch {
+				assert.NotEqual(t, tc.extraDNSLabels, loggedinPeer.ExtraDNSLabels, "ExtraDNSLabels should not match on loggedinPeer")
+				assert.Equal(t, currentPeer.ExtraDNSLabels, loggedinPeer.ExtraDNSLabels, "ExtraDNSLabels mismatch on loggedinPeer")
+			} else {
+				assert.Equal(t, currentPeer.ExtraDNSLabels, loggedinPeer.ExtraDNSLabels, "ExtraDNSLabels mismatch on loggedinPeer")
+			}
+			assert.NotNil(t, networkMap, "networkMap should not be nil on success")
+
+			assert.Equal(t, existingAccountID, loggedinPeer.AccountID, "AccountID mismatch for logged peer")
+
+			peerFromStore, err := s.GetPeerByPeerPubKey(context.Background(), store.LockingStrengthShare, loginInput.WireGuardPubKey)
+			require.NoError(t, err, "Failed to get peer by pub key: %s", loginInput.WireGuardPubKey)
+			assert.Equal(t, existingAccountID, peerFromStore.AccountID, "AccountID mismatch for peer from store")
+			assert.Equal(t, loggedinPeer.ID, peerFromStore.ID, "Peer ID mismatch between loggedinPeer and peerFromStore")
+		})
+	}
+}
+
 func TestPeerAccountPeersUpdate(t *testing.T) {
 	manager, account, peer1, peer2, peer3 := setupNetworkMapTest(t)
 
diff --git a/management/server/posture_checks_test.go b/management/server/posture_checks_test.go
index 232955f7..8bd2fab6 100644
--- a/management/server/posture_checks_test.go
+++ b/management/server/posture_checks_test.go
@@ -455,7 +455,7 @@ func TestArePostureCheckChangesAffectPeers(t *testing.T) {
 		AccountID: account.Id,
 		Peers:     []string{},
 	}
-	err = manager.Store.SaveGroups(context.Background(), store.LockingStrengthUpdate, []*types.Group{groupA, groupB})
+	err = manager.Store.SaveGroups(context.Background(), store.LockingStrengthUpdate, account.Id, []*types.Group{groupA, groupB})
 	require.NoError(t, err, "failed to save groups")
 
 	postureCheckA := &posture.Checks{
diff --git a/management/server/route_test.go b/management/server/route_test.go
index dcda3e6d..833477b5 100644
--- a/management/server/route_test.go
+++ b/management/server/route_test.go
@@ -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))
@@ -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/store/sql_store_test.go b/management/server/store/sql_store_test.go
index 589e727e..2c1f5f8e 100644
--- a/management/server/store/sql_store_test.go
+++ b/management/server/store/sql_store_test.go
@@ -60,10 +60,10 @@ func Test_NewStore(t *testing.T) {
 
 	runTestForAllEngines(t, "", func(t *testing.T, store Store) {
 		if store == nil {
-			t.Errorf("expected to create a new Store")
+			t.Fatalf("expected to create a new Store")
 		}
 		if len(store.GetAllAccounts(context.Background())) != 0 {
-			t.Errorf("expected to create a new empty Accounts map when creating a new FileStore")
+			t.Fatalf("expected to create a new empty Accounts map when creating a new FileStore")
 		}
 	})
 }
@@ -1115,7 +1115,7 @@ func TestSqlite_CreateAndGetObjectInTransaction(t *testing.T) {
 
 	group := &types.Group{
 		ID:        "group-id",
-		AccountID: "account-id",
+		AccountID: "bf1c8084-ba50-4ce7-9439-34653001fc3b",
 		Name:      "group-name",
 		Issued:    "api",
 		Peers:     nil,
@@ -1324,11 +1324,11 @@ func TestSqlStore_SaveGroups(t *testing.T) {
 			Peers:     []string{"peer3", "peer4"},
 		},
 	}
-	err = store.SaveGroups(context.Background(), LockingStrengthUpdate, groups)
+	err = store.SaveGroups(context.Background(), LockingStrengthUpdate, accountID, groups)
 	require.NoError(t, err)
 
 	groups[1].Peers = []string{}
-	err = store.SaveGroups(context.Background(), LockingStrengthUpdate, groups)
+	err = store.SaveGroups(context.Background(), LockingStrengthUpdate, accountID, groups)
 	require.NoError(t, err)
 
 	group, err := store.GetGroupByID(context.Background(), LockingStrengthShare, accountID, groups[1].ID)
@@ -3240,10 +3240,51 @@ func TestSqlStore_SaveGroups_LargeBatch(t *testing.T) {
 		})
 	}
 
-	err = store.SaveGroups(context.Background(), LockingStrengthUpdate, groupsToSave)
+	err = store.SaveGroups(context.Background(), LockingStrengthUpdate, accountID, groupsToSave)
 	require.NoError(t, err)
 
 	accountGroups, err = store.GetAccountGroups(context.Background(), LockingStrengthShare, accountID)
 	require.NoError(t, err)
 	require.Equal(t, 8003, len(accountGroups))
 }
+
+func TestSqlStore_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_test.go b/management/server/store/store_test.go
index 1d0026e3..19fce219 100644
--- a/management/server/store/store_test.go
+++ b/management/server/store/store_test.go
@@ -16,7 +16,7 @@ type benchCase struct {
 
 var newSqlite = func(b *testing.B) Store {
 	b.Helper()
-	store, _ := NewSqliteStore(context.Background(), b.TempDir(), nil)
+	store, _ := NewSqliteStore(context.Background(), b.TempDir(), nil, false)
 	return store
 }
 
diff --git a/management/server/testdata/extended-store.sql b/management/server/testdata/extended-store.sql
index 2859e82c..324bf629 100644
--- a/management/server/testdata/extended-store.sql
+++ b/management/server/testdata/extended-store.sql
@@ -1,5 +1,5 @@
 CREATE TABLE `accounts` (`id` text,`created_by` text,`created_at` datetime,`domain` text,`domain_category` text,`is_domain_primary_account` numeric,`network_identifier` text,`network_net` text,`network_dns` text,`network_serial` integer,`dns_settings_disabled_management_groups` text,`settings_peer_login_expiration_enabled` numeric,`settings_peer_login_expiration` integer,`settings_regular_users_view_blocked` numeric,`settings_groups_propagation_enabled` numeric,`settings_jwt_groups_enabled` numeric,`settings_jwt_groups_claim_name` text,`settings_jwt_allow_groups` text,`settings_extra_peer_approval_enabled` numeric,`settings_extra_integrated_validator_groups` text,PRIMARY KEY (`id`));
-CREATE TABLE `setup_keys` (`id` text,`account_id` text,`key` text,`name` text,`type` text,`created_at` datetime,`expires_at` datetime,`updated_at` datetime,`revoked` numeric,`used_times` integer,`last_used` datetime DEFAULT NULL,`auto_groups` text,`usage_limit` integer,`ephemeral` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_setup_keys_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
+CREATE TABLE `setup_keys` (`id` text,`account_id` text,`key` text,`name` text,`type` text,`created_at` datetime,`expires_at` datetime,`updated_at` datetime,`revoked` numeric,`used_times` integer,`last_used` datetime DEFAULT NULL,`auto_groups` text,`usage_limit` integer,`ephemeral` numeric,`allow_extra_dns_labels` numeric,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_setup_keys_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
 CREATE TABLE `peers` (`id` text,`account_id` text,`key` text,`setup_key` text,`ip` text,`meta_hostname` text,`meta_go_os` text,`meta_kernel` text,`meta_core` text,`meta_platform` text,`meta_os` text,`meta_os_version` text,`meta_wt_version` text,`meta_ui_version` text,`meta_kernel_version` text,`meta_network_addresses` text,`meta_system_serial_number` text,`meta_system_product_name` text,`meta_system_manufacturer` text,`meta_environment` text,`meta_files` text,`name` text,`dns_label` text,`peer_status_last_seen` datetime,`peer_status_connected` numeric,`peer_status_login_expired` numeric,`peer_status_requires_approval` numeric,`user_id` text,`ssh_key` text,`ssh_enabled` numeric,`login_expiration_enabled` numeric,`last_login` datetime,`created_at` datetime,`ephemeral` numeric,`location_connection_ip` text,`location_country_code` text,`location_city_name` text,`location_geo_name_id` integer,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_peers_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
 CREATE TABLE `users` (`id` text,`account_id` text,`role` text,`is_service_user` numeric,`non_deletable` numeric,`service_user_name` text,`auto_groups` text,`blocked` numeric,`last_login` datetime DEFAULT NULL,`created_at` datetime,`issued` text DEFAULT "api",`integration_ref_id` integer,`integration_ref_integration_type` text,PRIMARY KEY (`id`),CONSTRAINT `fk_accounts_users_g` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`));
 CREATE TABLE `personal_access_tokens` (`id` text,`user_id` text,`name` text,`hashed_token` text,`expiration_date` datetime,`created_by` text,`created_at` datetime,`last_used` datetime,PRIMARY KEY (`id`),CONSTRAINT `fk_users_pa_ts_g` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`));
@@ -25,9 +25,10 @@ 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 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 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,0);
+INSERT INTO setup_keys VALUES('A2C8E62B-38F5-4553-B31E-DD66C696CEBD','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBD','Default key with extra DNS labels','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,1);
+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,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,'');
 INSERT INTO users VALUES('f4f6d672-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','user',0,0,'','[]',0,NULL,'2024-10-02 16:01:38.210678+02:00','api',0,'');
 INSERT INTO personal_access_tokens VALUES('9dj38s35-63fb-11ec-90d6-0242ac120003','f4f6d672-63fb-11ec-90d6-0242ac120003','','SoMeHaShEdToKeN','2023-02-27 00:00:00+00:00','user','2023-01-01 00:00:00+00:00','2023-02-01 00:00:00+00:00');
diff --git a/management/server/testutil/store.go b/management/server/testutil/store.go
index 8672efa7..db418c45 100644
--- a/management/server/testutil/store.go
+++ b/management/server/testutil/store.go
@@ -5,21 +5,35 @@ package testutil
 
 import (
 	"context"
-	"os"
 	"time"
 
 	log "github.com/sirupsen/logrus"
 	"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"
 )
 
+var (
+	pgContainer    *postgres.PostgresContainer
+	mysqlContainer *mysql.MySQLContainer
+)
+
 // CreateMysqlTestContainer creates a new MySQL container for testing.
-func CreateMysqlTestContainer() (func(), error) {
+func CreateMysqlTestContainer() (func(), string, error) {
 	ctx := context.Background()
 
-	myContainer, err := mysql.RunContainer(ctx,
+	if mysqlContainer != nil {
+		connStr, err := mysqlContainer.ConnectionString(ctx)
+		if err != nil {
+			return nil, "", err
+		}
+		return noOpCleanup, connStr, nil
+	}
+
+	var err error
+	mysqlContainer, err = mysql.RunContainer(ctx,
 		testcontainers.WithImage("mlsmaycon/warmed-mysql:8"),
 		mysql.WithDatabase("testing"),
 		mysql.WithUsername("root"),
@@ -30,31 +44,42 @@ func CreateMysqlTestContainer() (func(), error) {
 		),
 	)
 	if err != nil {
-		return nil, err
+		return nil, "", err
 	}
 
 	cleanup := func() {
-		os.Unsetenv("NETBIRD_STORE_ENGINE_MYSQL_DSN")
-		timeoutCtx, cancelFunc := context.WithTimeout(ctx, 1*time.Second)
-		defer cancelFunc()
-		if err = myContainer.Terminate(timeoutCtx); err != nil {
-			log.WithContext(ctx).Warnf("failed to stop mysql container %s: %s", myContainer.GetContainerID(), err)
+		if mysqlContainer != nil {
+			timeoutCtx, cancelFunc := context.WithTimeout(ctx, 1*time.Second)
+			defer cancelFunc()
+			if err = mysqlContainer.Terminate(timeoutCtx); err != nil {
+				log.WithContext(ctx).Warnf("failed to stop mysql container %s: %s", mysqlContainer.GetContainerID(), err)
+			}
+			mysqlContainer = nil // reset the container to allow recreation
 		}
 	}
 
-	talksConn, err := myContainer.ConnectionString(ctx)
+	talksConn, err := mysqlContainer.ConnectionString(ctx)
 	if err != nil {
-		return nil, err
+		return nil, "", err
 	}
 
-	return cleanup, os.Setenv("NETBIRD_STORE_ENGINE_MYSQL_DSN", talksConn)
+	return cleanup, talksConn, nil
 }
 
 // CreatePostgresTestContainer creates a new PostgreSQL container for testing.
-func CreatePostgresTestContainer() (func(), error) {
+func CreatePostgresTestContainer() (func(), string, error) {
 	ctx := context.Background()
 
-	pgContainer, err := postgres.RunContainer(ctx,
+	if pgContainer != nil {
+		connStr, err := pgContainer.ConnectionString(ctx)
+		if err != nil {
+			return nil, "", err
+		}
+		return noOpCleanup, connStr, nil
+	}
+
+	var err error
+	pgContainer, err = postgres.RunContainer(ctx,
 		testcontainers.WithImage("postgres:16-alpine"),
 		postgres.WithDatabase("netbird"),
 		postgres.WithUsername("root"),
@@ -65,22 +90,54 @@ func CreatePostgresTestContainer() (func(), error) {
 		),
 	)
 	if err != nil {
-		return nil, err
+		return nil, "", err
+	}
+
+	cleanup := func() {
+		if pgContainer != nil {
+			timeoutCtx, cancelFunc := context.WithTimeout(ctx, 1*time.Second)
+			defer cancelFunc()
+			if err = pgContainer.Terminate(timeoutCtx); err != nil {
+				log.WithContext(ctx).Warnf("failed to stop postgres container %s: %s", pgContainer.GetContainerID(), err)
+			}
+			pgContainer = nil // reset the container to allow recreation
+		}
+
+	}
+
+	talksConn, err := pgContainer.ConnectionString(ctx)
+	if err != nil {
+		return nil, "", err
+	}
+
+	return cleanup, talksConn, nil
+}
+
+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() {
-		os.Unsetenv("NETBIRD_STORE_ENGINE_POSTGRES_DSN")
 		timeoutCtx, cancelFunc := context.WithTimeout(ctx, 1*time.Second)
 		defer cancelFunc()
-		if err = pgContainer.Terminate(timeoutCtx); err != nil {
-			log.WithContext(ctx).Warnf("failed to stop postgres container %s: %s", pgContainer.GetContainerID(), err)
+		if err = redisContainer.Terminate(timeoutCtx); err != nil {
+			log.WithContext(ctx).Warnf("failed to stop redis container %s: %s", redisContainer.GetContainerID(), err)
 		}
 	}
 
-	talksConn, err := pgContainer.ConnectionString(ctx)
+	redisURL, err := redisContainer.ConnectionString(ctx)
 	if err != nil {
-		return nil, err
+		return nil, "", err
 	}
 
-	return cleanup, os.Setenv("NETBIRD_STORE_ENGINE_POSTGRES_DSN", talksConn)
+	return cleanup, redisURL, nil
 }
diff --git a/management/server/testutil/store_ios.go b/management/server/testutil/store_ios.go
index edde62f1..c3dd839d 100644
--- a/management/server/testutil/store_ios.go
+++ b/management/server/testutil/store_ios.go
@@ -3,14 +3,20 @@
 
 package testutil
 
-func CreatePostgresTestContainer() (func(), error) {
+func CreatePostgresTestContainer() (func(), string, error) {
 	return func() {
 		// Empty function for Postgres
-	}, nil
+	}, "", nil
 }
 
-func CreateMysqlTestContainer() (func(), error) {
+func CreateMysqlTestContainer() (func(), string, error) {
 	return func() {
 		// Empty function for MySQL
-	}, nil
+	}, "", nil
+}
+
+func CreateRedisTestContainer() (func(), string, error) {
+	return func() {
+		// Empty function for Redis
+	}, "", nil
 }
diff --git a/management/server/user_test.go b/management/server/user_test.go
index 83c5ac49..66bdc168 100644
--- a/management/server/user_test.go
+++ b/management/server/user_test.go
@@ -13,7 +13,10 @@ 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/permissions/modules"
+	"github.com/netbirdio/netbird/management/server/permissions/roles"
 	"github.com/netbirdio/netbird/management/server/status"
+	"github.com/netbirdio/netbird/management/server/users"
 	"github.com/netbirdio/netbird/management/server/util"
 
 	nbpeer "github.com/netbirdio/netbird/management/server/peer"
@@ -1020,90 +1023,6 @@ func TestDefaultAccountManager_ListUsers(t *testing.T) {
 	assert.Equal(t, 2, regular)
 }
 
-func TestDefaultAccountManager_ListUsers_DashboardPermissions(t *testing.T) {
-	testCases := []struct {
-		name                         string
-		role                         types.UserRole
-		limitedViewSettings          bool
-		expectedDashboardPermissions string
-	}{
-		{
-			name:                         "Regular user, no limited view settings",
-			role:                         types.UserRoleUser,
-			limitedViewSettings:          false,
-			expectedDashboardPermissions: "limited",
-		},
-		{
-			name:                         "Admin user, no limited view settings",
-			role:                         types.UserRoleAdmin,
-			limitedViewSettings:          false,
-			expectedDashboardPermissions: "full",
-		},
-		{
-			name:                         "Owner, no limited view settings",
-			role:                         types.UserRoleOwner,
-			limitedViewSettings:          false,
-			expectedDashboardPermissions: "full",
-		},
-		{
-			name:                         "Regular user, limited view settings",
-			role:                         types.UserRoleUser,
-			limitedViewSettings:          true,
-			expectedDashboardPermissions: "blocked",
-		},
-		{
-			name:                         "Admin user, limited view settings",
-			role:                         types.UserRoleAdmin,
-			limitedViewSettings:          true,
-			expectedDashboardPermissions: "full",
-		},
-		{
-			name:                         "Owner, limited view settings",
-			role:                         types.UserRoleOwner,
-			limitedViewSettings:          true,
-			expectedDashboardPermissions: "full",
-		},
-	}
-
-	for _, testCase := range testCases {
-		t.Run(testCase.name, func(t *testing.T) {
-			store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir())
-			if err != nil {
-				t.Fatalf("Error when creating store: %s", err)
-			}
-			t.Cleanup(cleanup)
-
-			account := newAccountWithId(context.Background(), mockAccountID, mockUserID, "")
-			account.Users["normal_user1"] = types.NewUser("normal_user1", testCase.role, false, false, "", []string{}, types.UserIssuedAPI)
-			account.Settings.RegularUsersViewBlocked = testCase.limitedViewSettings
-			delete(account.Users, mockUserID)
-
-			err = store.SaveAccount(context.Background(), account)
-			if err != nil {
-				t.Fatalf("Error when saving account: %s", err)
-			}
-
-			permissionsManager := permissions.NewManager(store)
-			am := DefaultAccountManager{
-				Store:              store,
-				eventStore:         &activity.InMemoryEventStore{},
-				permissionsManager: permissionsManager,
-			}
-
-			users, err := am.ListUsers(context.Background(), mockAccountID)
-			if err != nil {
-				t.Fatalf("Error when checking user role: %s", err)
-			}
-
-			assert.Equal(t, 1, len(users))
-
-			userInfo, _ := users[0].ToUserInfo(nil, account.Settings)
-			assert.Equal(t, testCase.expectedDashboardPermissions, userInfo.Permissions.DashboardView)
-		})
-	}
-
-}
-
 func TestDefaultAccountManager_ExternalCache(t *testing.T) {
 	store, cleanup, err := store.NewTestStoreFromSQL(context.Background(), "", t.TempDir())
 	if err != nil {
@@ -1654,121 +1573,154 @@ func TestDefaultAccountManager_GetCurrentUserInfo(t *testing.T) {
 
 	tt := []struct {
 		name           string
-		accountId      string
-		userId         string
+		userAuth       nbcontext.UserAuth
 		expectedErr    error
-		expectedResult *types.UserInfo
+		expectedResult *users.UserInfoWithPermissions
 	}{
 		{
 			name:        "not found",
-			accountId:   account1.Id,
-			userId:      "not-found",
+			userAuth:    nbcontext.UserAuth{AccountId: account1.Id, UserId: "not-found"},
 			expectedErr: status.NewUserNotFoundError("not-found"),
 		},
 		{
 			name:        "not part of account",
-			accountId:   account1.Id,
-			userId:      "account2Owner",
+			userAuth:    nbcontext.UserAuth{AccountId: account1.Id, UserId: "account2Owner"},
 			expectedErr: status.NewUserNotPartOfAccountError(),
 		},
 		{
 			name:        "blocked",
-			accountId:   account1.Id,
-			userId:      "blocked-user",
+			userAuth:    nbcontext.UserAuth{AccountId: account1.Id, UserId: "blocked-user"},
 			expectedErr: status.NewUserBlockedError(),
 		},
 		{
 			name:        "service user",
-			accountId:   account1.Id,
-			userId:      "service-user",
+			userAuth:    nbcontext.UserAuth{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:     "owner user",
+			userAuth: nbcontext.UserAuth{AccountId: account1.Id, UserId: "account1Owner"},
+			expectedResult: &users.UserInfoWithPermissions{
+				UserInfo: &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: mergeRolePermissions(roles.Owner),
 			},
 		},
 		{
-			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:     "regular user",
+			userAuth: nbcontext.UserAuth{AccountId: account1.Id, UserId: "regular-user"},
+			expectedResult: &users.UserInfoWithPermissions{
+				UserInfo: &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: mergeRolePermissions(roles.User),
 			},
 		},
 		{
-			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:     "admin user",
+			userAuth: nbcontext.UserAuth{AccountId: account1.Id, UserId: "admin-user"},
+			expectedResult: &users.UserInfoWithPermissions{
+				UserInfo: &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: mergeRolePermissions(roles.Admin),
 			},
 		},
 		{
-			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",
+			name:     "settings blocked regular user",
+			userAuth: nbcontext.UserAuth{AccountId: account2.Id, UserId: "settings-blocked-user"},
+			expectedResult: &users.UserInfoWithPermissions{
+				UserInfo: &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: mergeRolePermissions(roles.User),
+				Restricted:  true,
+			},
+		},
+
+		{
+			name:     "settings blocked regular user child account",
+			userAuth: nbcontext.UserAuth{AccountId: account2.Id, UserId: "settings-blocked-user", IsChild: true},
+			expectedResult: &users.UserInfoWithPermissions{
+				UserInfo: &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: mergeRolePermissions(roles.User),
+				Restricted:  false,
+			},
+		},
+		{
+			name:     "settings blocked owner user",
+			userAuth: nbcontext.UserAuth{AccountId: account2.Id, UserId: "account2Owner"},
+			expectedResult: &users.UserInfoWithPermissions{
+				UserInfo: &types.UserInfo{
+					ID:                   "account2Owner",
+					Name:                 "",
+					Role:                 "owner",
+					AutoGroups:           []string{},
+					Status:               "active",
+					IsServiceUser:        false,
+					IsBlocked:            false,
+					NonDeletable:         false,
+					LastLogin:            time.Time{},
+					Issued:               "api",
+					IntegrationReference: integration_reference.IntegrationReference{},
+				},
+				Permissions: mergeRolePermissions(roles.Owner),
 			},
 		},
 	}
 
 	for _, tc := range tt {
 		t.Run(tc.name, func(t *testing.T) {
-			result, err := am.GetCurrentUserInfo(context.Background(), tc.accountId, tc.userId)
+			result, err := am.GetCurrentUserInfo(context.Background(), tc.userAuth)
 
 			if tc.expectedErr != nil {
 				assert.Equal(t, err, tc.expectedErr)
@@ -1780,3 +1732,17 @@ func TestDefaultAccountManager_GetCurrentUserInfo(t *testing.T) {
 		})
 	}
 }
+
+func mergeRolePermissions(role roles.RolePermissions) roles.Permissions {
+	permissions := roles.Permissions{}
+
+	for k := range modules.All {
+		if rolePermissions, ok := role.Permissions[k]; ok {
+			permissions[k] = rolePermissions
+			continue
+		}
+		permissions[k] = role.AutoAllowNew
+	}
+
+	return permissions
+}
diff --git a/upload-server/server/local_test.go b/upload-server/server/local_test.go
new file mode 100644
index 00000000..bd8a8780
--- /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_test.go b/upload-server/server/s3_test.go
new file mode 100644
index 00000000..26b0ecd0
--- /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/util/net/net_test.go b/util/net/net_test.go
new file mode 100644
index 00000000..e0633cb6
--- /dev/null
+++ b/util/net/net_test.go
@@ -0,0 +1,94 @@
+package net
+
+import (
+	"net/netip"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestGetLastIPFromNetwork(t *testing.T) {
+	tests := []struct {
+		name      string
+		network   string
+		fromEnd   int
+		expected  string
+		expectErr bool
+	}{
+		{
+			name:     "IPv4 /24 network - last IP (fromEnd=0)",
+			network:  "192.168.1.0/24",
+			fromEnd:  0,
+			expected: "192.168.1.255",
+		},
+		{
+			name:     "IPv4 /24 network - fromEnd=1",
+			network:  "192.168.1.0/24",
+			fromEnd:  1,
+			expected: "192.168.1.254",
+		},
+		{
+			name:     "IPv4 /24 network - fromEnd=5",
+			network:  "192.168.1.0/24",
+			fromEnd:  5,
+			expected: "192.168.1.250",
+		},
+		{
+			name:     "IPv4 /16 network - last IP",
+			network:  "10.0.0.0/16",
+			fromEnd:  0,
+			expected: "10.0.255.255",
+		},
+		{
+			name:     "IPv4 /16 network - fromEnd=256",
+			network:  "10.0.0.0/16",
+			fromEnd:  256,
+			expected: "10.0.254.255",
+		},
+		{
+			name:     "IPv4 /32 network - single host",
+			network:  "192.168.1.100/32",
+			fromEnd:  0,
+			expected: "192.168.1.100",
+		},
+		{
+			name:     "IPv6 /64 network - last IP",
+			network:  "2001:db8::/64",
+			fromEnd:  0,
+			expected: "2001:db8::ffff:ffff:ffff:ffff",
+		},
+		{
+			name:     "IPv6 /64 network - fromEnd=1",
+			network:  "2001:db8::/64",
+			fromEnd:  1,
+			expected: "2001:db8::ffff:ffff:ffff:fffe",
+		},
+		{
+			name:     "IPv6 /128 network - single host",
+			network:  "2001:db8::1/128",
+			fromEnd:  0,
+			expected: "2001:db8::1",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			network, err := netip.ParsePrefix(tt.network)
+			require.NoError(t, err, "Failed to parse network prefix")
+
+			result, err := GetLastIPFromNetwork(network, tt.fromEnd)
+
+			if tt.expectErr {
+				assert.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+
+			expectedIP, err := netip.ParseAddr(tt.expected)
+			require.NoError(t, err, "Failed to parse expected IP")
+
+			assert.Equal(t, expectedIP, result, "IP mismatch for network %s with fromEnd=%d", tt.network, tt.fromEnd)
+		})
+	}
+}