Repository: netbirdio/netbird

Issue:
netbird client inside docker: can not access to status or route list: context deadline exceeded
**Describe the problem**

Can not access to netbird daemon from outside docker container.
Daemon successfully started and registered in mesh with 
docker compose up -d

But i am can not access to daemon stats and routes:

$ docker compose exec netbird_client netbird --daemon-addr=unix:///var/run/netbird.sock status
Error: failed to connect to daemon error: context deadline exceeded
If the daemon is not running please run: 
netbird service install 
netbird service start

$ docker compose exec netbird_client ls -laF /var/run/
total 8
drwxr-xr-x    2 root     root          4096 Jun 18 15:37 ./
drwxr-xr-x    1 root     root          4096 Jul 29 14:48 ../
ubuntu@netbird-gw-dev-dc-01 .../docker-compose/netbird $ 


**To Reproduce**

use "docker compose up -d" and then "docker compose exec netbird_client netbird status"
with this docker-compose.yaml:

```
services:
  netbird_client:
    image: "netbirdio/netbird:latest"
    restart: always
    logging:
      driver: local
    labels:
      logging: "promtail"
      logging_jobname: "containerlogs"
    container_name: netbird_client
    cap_add:
      - NET_ADMIN
      - SYS_ADMIN
      - SYS_RESOURCE
    environment:
      - NB_SETUP_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
      - NB_HOSTNAME=netbird-gw-dev-dc.domain.com
      - NB_SERVICE=netbird
      - NB_DAEMON_ADDR=unix:///var/run/netbird.sock
      - NB_MANAGEMENT_URL=https://mgmt.domain.com:33073
      - NB_LOG_LEVEL=debug
    volumes:
      - netbird-client:/etc/netbird
    network_mode: host
    privileged: true

volumes:
  netbird-client:
    driver: local
```


**Expected behavior**

netbird status should be fetched and printed

**Are you using NetBird Cloud?**

self-host NetBird's control plane

**NetBird version**

latest, 0.28.4, 0.27.7


Discussion:
@itoffshore: I noticed the same issue under a privileged podman container / cloud hosted netbird

Maybe a good solution here would be to add a small function to detect if `netbird` is running in a container (query `/proc/1/cgroup`) - & if so skip the daemon check when querying `status`@houckham: Netbird docker version 0.36.3 and still not able to poll netbird stats. Is this intended or a fault? Being worked on? Any work-arounds?@mojiefong: 0.38.0 also has this problem@azdolinski: workaround: 

```yaml
services:
  netbird:
    container_name: netbird
    hostname: netbird
    image: netbirdio/netbird:latest
    entrypoint: "netbird service run"             <<< !!!
    restart: always
    cap_add:
      - NET_ADMIN
      - SYS_ADMIN
      - SYS_RESOURCE
    environment:
      - NB_SETUP_KEY=${NB_SETUP_KEY}
      - NB_HOSTNAME=${NB_HOSTNAME}
      - NB_SERVICE=netbird
      - NB_LOG_LEVEL=info
    volumes:
      - ./netbird-client:/etc/netbird
```

Tested on:

```bash
root@host:/opt/docker/netbird# docker exec -it netbird sh
/ # netbird status
OS: linux/amd64
Daemon version: 0.39.2
CLI version: 0.39.2
...
```

in service mode you should be able to see inside docker `/var/run/netbird.sock`
by default - container starts in client mode so `entrypoint: netbird up` - in that case, there is no sock and will not be able to connect to `service`

Solution patch:
diff --git a/.dockerignore-client b/.dockerignore-client
new file mode 100644
index 00000000..a93ef97c
--- /dev/null
+++ b/.dockerignore-client
@@ -0,0 +1,3 @@
+*
+!client/netbird-entrypoint.sh
+!netbird
diff --git a/.gitignore b/.gitignore
index abb728b1..e6c0c0ac 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,3 +30,4 @@ infrastructure_files/setup-*.env
 .vscode
 .DS_Store
 vendor/
+/netbird
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index ca5eafa6..d4a97b44 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -155,13 +155,15 @@ dockers:
     goarch: amd64
     use: buildx
     dockerfile: client/Dockerfile
+    extra_files:
+      - client/netbird-entrypoint.sh
     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=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
       - "--label=maintainer=dev@netbird.io"
   - image_templates:
       - netbirdio/netbird:{{ .Version }}-arm64v8
@@ -171,6 +173,8 @@ dockers:
     goarch: arm64
     use: buildx
     dockerfile: client/Dockerfile
+    extra_files:
+      - client/netbird-entrypoint.sh
     build_flag_templates:
       - "--platform=linux/arm64"
       - "--label=org.opencontainers.image.created={{.Date}}"
@@ -188,6 +192,8 @@ dockers:
     goarm: 6
     use: buildx
     dockerfile: client/Dockerfile
+    extra_files:
+      - client/netbird-entrypoint.sh
     build_flag_templates:
       - "--platform=linux/arm"
       - "--label=org.opencontainers.image.created={{.Date}}"
@@ -205,6 +211,8 @@ dockers:
     goarch: amd64
     use: buildx
     dockerfile: client/Dockerfile-rootless
+    extra_files:
+      - client/netbird-entrypoint.sh
     build_flag_templates:
       - "--platform=linux/amd64"
       - "--label=org.opencontainers.image.created={{.Date}}"
@@ -221,6 +229,8 @@ dockers:
     goarch: arm64
     use: buildx
     dockerfile: client/Dockerfile-rootless
+    extra_files:
+      - client/netbird-entrypoint.sh
     build_flag_templates:
       - "--platform=linux/arm64"
       - "--label=org.opencontainers.image.created={{.Date}}"
@@ -238,6 +248,8 @@ dockers:
     goarm: 6
     use: buildx
     dockerfile: client/Dockerfile-rootless
+    extra_files:
+      - client/netbird-entrypoint.sh
     build_flag_templates:
       - "--platform=linux/arm"
       - "--label=org.opencontainers.image.created={{.Date}}"
diff --git a/client/Dockerfile b/client/Dockerfile
index 5f1f7004..e19a0990 100644
--- a/client/Dockerfile
+++ b/client/Dockerfile
@@ -1,9 +1,27 @@
-FROM alpine:3.21.3
+# build & run locally with:
+#   cd "$(git rev-parse --show-toplevel)"
+#   CGO_ENABLED=0 go build -o netbird ./client
+#   sudo podman build -t localhost/netbird:latest -f client/Dockerfile --ignorefile .dockerignore-client .
+#   sudo podman run --rm -it --cap-add={BPF,NET_ADMIN,NET_RAW} localhost/netbird:latest
+
+FROM alpine:3.22.0
 # iproute2: busybox doesn't display ip rules properly
-RUN apk add --no-cache ca-certificates ip6tables iproute2 iptables
+RUN apk add --no-cache \
+    bash \
+    ca-certificates \
+    ip6tables \
+    iproute2 \
+    iptables
 
-ARG NETBIRD_BINARY=netbird
-COPY ${NETBIRD_BINARY} /usr/local/bin/netbird
+ENV \
+    NETBIRD_BIN="/usr/local/bin/netbird" \
+    NB_LOG_FILE="console,/var/log/netbird/client.log" \
+    NB_DAEMON_ADDR="unix:///var/run/netbird.sock" \
+    NB_ENTRYPOINT_SERVICE_TIMEOUT="5" \
+    NB_ENTRYPOINT_LOGIN_TIMEOUT="1"
 
-ENV NB_FOREGROUND_MODE=true
-ENTRYPOINT [ "/usr/local/bin/netbird","up"]
+ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ]
+
+ARG NETBIRD_BINARY=netbird
+COPY client/netbird-entrypoint.sh /usr/local/bin/netbird-entrypoint.sh
+COPY "${NETBIRD_BINARY}"  /usr/local/bin/netbird
diff --git a/client/Dockerfile-rootless b/client/Dockerfile-rootless
index 5055cb20..5fa8de0a 100644
--- a/client/Dockerfile-rootless
+++ b/client/Dockerfile-rootless
@@ -1,18 +1,33 @@
-FROM alpine:3.21.0
+# build & run locally with:
+#   cd "$(git rev-parse --show-toplevel)"
+#   CGO_ENABLED=0 go build -o netbird ./client
+#   podman build -t localhost/netbird:latest -f client/Dockerfile --ignorefile .dockerignore-client .
+#   podman run --rm -it --cap-add={BPF,NET_ADMIN,NET_RAW} localhost/netbird:latest
 
-ARG NETBIRD_BINARY=netbird
-COPY ${NETBIRD_BINARY} /usr/local/bin/netbird
+FROM alpine:3.22.0
 
-RUN apk add --no-cache ca-certificates \
+RUN apk add --no-cache \
+      bash \
+      ca-certificates \
     && adduser -D -h /var/lib/netbird netbird
+
 WORKDIR /var/lib/netbird
 USER netbird:netbird
 
-ENV NB_FOREGROUND_MODE=true
-ENV NB_USE_NETSTACK_MODE=true
-ENV NB_ENABLE_NETSTACK_LOCAL_FORWARDING=true
-ENV NB_CONFIG=config.json
-ENV NB_DAEMON_ADDR=unix://netbird.sock
-ENV NB_DISABLE_DNS=true
+ENV \
+    NETBIRD_BIN="/usr/local/bin/netbird" \
+    NB_USE_NETSTACK_MODE="true" \
+    NB_ENABLE_NETSTACK_LOCAL_FORWARDING="true" \
+    NB_CONFIG="/var/lib/netbird/config.json" \
+    NB_STATE_DIR="/var/lib/netbird" \
+    NB_DAEMON_ADDR="unix:///var/lib/netbird/netbird.sock" \
+    NB_LOG_FILE="console,/var/lib/netbird/client.log" \
+    NB_DISABLE_DNS="true" \
+    NB_ENTRYPOINT_SERVICE_TIMEOUT="5" \
+    NB_ENTRYPOINT_LOGIN_TIMEOUT="1"
 
-ENTRYPOINT [ "/usr/local/bin/netbird", "up" ]
+ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ]
+
+ARG NETBIRD_BINARY=netbird
+COPY client/netbird-entrypoint.sh /usr/local/bin/netbird-entrypoint.sh
+COPY "${NETBIRD_BINARY}"  /usr/local/bin/netbird
diff --git a/client/cmd/down.go b/client/cmd/down.go
index 3a324cc1..cfa69bce 100644
--- a/client/cmd/down.go
+++ b/client/cmd/down.go
@@ -20,7 +20,7 @@ var downCmd = &cobra.Command{
 
 		cmd.SetOut(cmd.OutOrStdout())
 
-		err := util.InitLog(logLevel, "console")
+		err := util.InitLog(logLevel, util.LogConsole)
 		if err != nil {
 			log.Errorf("failed initializing log %v", err)
 			return err
diff --git a/client/cmd/login.go b/client/cmd/login.go
index 14abcd03..8ac7086b 100644
--- a/client/cmd/login.go
+++ b/client/cmd/login.go
@@ -32,7 +32,7 @@ var loginCmd = &cobra.Command{
 
 		cmd.SetOut(cmd.OutOrStdout())
 
-		err := util.InitLog(logLevel, "console")
+		err := util.InitLog(logLevel, util.LogConsole)
 		if err != nil {
 			return fmt.Errorf("failed initializing log %v", err)
 		}
@@ -50,7 +50,7 @@ var loginCmd = &cobra.Command{
 		}
 
 		// workaround to run without service
-		if logFile == "console" {
+		if util.FindFirstLogPath(logFiles) == "" {
 			err = handleRebrand(cmd)
 			if err != nil {
 				return err
diff --git a/client/cmd/root.go b/client/cmd/root.go
index bfd0d06c..1774602c 100644
--- a/client/cmd/root.go
+++ b/client/cmd/root.go
@@ -10,6 +10,7 @@ import (
 	"os/signal"
 	"path"
 	"runtime"
+	"slices"
 	"strings"
 	"syscall"
 	"time"
@@ -51,7 +52,7 @@ var (
 	defaultLogFile          string
 	oldDefaultLogFileDir    string
 	oldDefaultLogFile       string
-	logFile                 string
+	logFiles                []string
 	daemonAddr              string
 	managementURL           string
 	adminURL                string
@@ -120,7 +121,7 @@ func init() {
 	rootCmd.PersistentFlags().StringVar(&adminURL, "admin-url", "", fmt.Sprintf("Admin Panel URL [http|https]://[host]:[port] (default \"%s\")", internal.DefaultAdminURL))
 	rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", defaultConfigPath, "Netbird config file location")
 	rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", "info", "sets Netbird log level")
-	rootCmd.PersistentFlags().StringVar(&logFile, "log-file", defaultLogFile, "sets Netbird log path. If console is specified the log will be output to stdout. If syslog is specified the log will be sent to syslog daemon.")
+	rootCmd.PersistentFlags().StringSliceVar(&logFiles, "log-file", []string{defaultLogFile}, "sets Netbird log paths written to simultaneously. If `console` is specified the log will be output to stdout. If `syslog` is specified the log will be sent to syslog daemon. You can pass the flag multiple times or separate entries by `,` character")
 	rootCmd.PersistentFlags().StringVarP(&setupKey, "setup-key", "k", "", "Setup key obtained from the Management Service Dashboard (used to register peer)")
 	rootCmd.PersistentFlags().StringVar(&setupKeyPath, "setup-key-file", "", "The path to a setup key obtained from the Management Service Dashboard (used to register peer) This is ignored if the setup-key flag is provided.")
 	rootCmd.MarkFlagsMutuallyExclusive("setup-key", "setup-key-file")
@@ -265,7 +266,7 @@ func getSetupKeyFromFile(setupKeyPath string) (string, error) {
 
 func handleRebrand(cmd *cobra.Command) error {
 	var err error
-	if logFile == defaultLogFile {
+	if slices.Contains(logFiles, defaultLogFile) {
 		if migrateToNetbird(oldDefaultLogFile, defaultLogFile) {
 			cmd.Printf("will copy Log dir %s and its content to %s\n", oldDefaultLogFileDir, defaultLogFileDir)
 			err = cpDir(oldDefaultLogFileDir, defaultLogFileDir)
diff --git a/client/cmd/service_controller.go b/client/cmd/service_controller.go
index 2545623e..df84342c 100644
--- a/client/cmd/service_controller.go
+++ b/client/cmd/service_controller.go
@@ -61,7 +61,7 @@ func (p *program) Start(svc service.Service) error {
 			}
 		}
 
-		serverInstance := server.New(p.ctx, configPath, logFile)
+		serverInstance := server.New(p.ctx, configPath, util.FindFirstLogPath(logFiles))
 		if err := serverInstance.Start(); err != nil {
 			log.Fatalf("failed to start daemon: %v", err)
 		}
@@ -112,7 +112,7 @@ func setupServiceControlCommand(cmd *cobra.Command, ctx context.Context, cancel
 		return nil, err
 	}
 
-	if err := util.InitLog(logLevel, logFile); err != nil {
+	if err := util.InitLog(logLevel, logFiles...); err != nil {
 		return nil, fmt.Errorf("init log: %w", err)
 	}
 
@@ -136,7 +136,7 @@ var runCmd = &cobra.Command{
 		ctx, cancel := context.WithCancel(cmd.Context())
 
 		SetupCloseHandler(ctx, cancel)
-		SetupDebugHandler(ctx, nil, nil, nil, logFile)
+		SetupDebugHandler(ctx, nil, nil, nil, util.FindFirstLogPath(logFiles))
 
 		s, err := setupServiceControlCommand(cmd, ctx, cancel)
 		if err != nil {
diff --git a/client/cmd/service_installer.go b/client/cmd/service_installer.go
index 951efcc7..c994801a 100644
--- a/client/cmd/service_installer.go
+++ b/client/cmd/service_installer.go
@@ -12,6 +12,8 @@ import (
 
 	"github.com/kardianos/service"
 	"github.com/spf13/cobra"
+
+	"github.com/netbirdio/netbird/util"
 )
 
 var ErrGetServiceStatus = fmt.Errorf("failed to get service status")
@@ -41,7 +43,7 @@ func buildServiceArguments() []string {
 		args = append(args, "--management-url", managementURL)
 	}
 
-	if logFile != "" {
+	for _, logFile := range logFiles {
 		args = append(args, "--log-file", logFile)
 	}
 
@@ -54,7 +56,7 @@ func configurePlatformSpecificSettings(svcConfig *service.Config) error {
 		// Respected only by systemd systems
 		svcConfig.Dependencies = []string{"After=network.target syslog.target"}
 
-		if logFile != "console" {
+		if logFile := util.FindFirstLogPath(logFiles); logFile != "" {
 			setStdLogPath := true
 			dir := filepath.Dir(logFile)
 
diff --git a/client/cmd/ssh.go b/client/cmd/ssh.go
index f9dbc26f..264f643e 100644
--- a/client/cmd/ssh.go
+++ b/client/cmd/ssh.go
@@ -46,7 +46,7 @@ var sshCmd = &cobra.Command{
 
 		cmd.SetOut(cmd.OutOrStdout())
 
-		err := util.InitLog(logLevel, "console")
+		err := util.InitLog(logLevel, util.LogConsole)
 		if err != nil {
 			return fmt.Errorf("failed initializing log %v", err)
 		}
diff --git a/client/cmd/status.go b/client/cmd/status.go
index 2d6e41bc..e50156ac 100644
--- a/client/cmd/status.go
+++ b/client/cmd/status.go
@@ -59,7 +59,7 @@ func statusFunc(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	err = util.InitLog(logLevel, "console")
+	err = util.InitLog(logLevel, util.LogConsole)
 	if err != nil {
 		return fmt.Errorf("failed initializing log %v", err)
 	}
diff --git a/client/cmd/up.go b/client/cmd/up.go
index b9781c0d..529beeac 100644
--- a/client/cmd/up.go
+++ b/client/cmd/up.go
@@ -79,7 +79,7 @@ func upFunc(cmd *cobra.Command, args []string) error {
 
 	cmd.SetOut(cmd.OutOrStdout())
 
-	err := util.InitLog(logLevel, "console")
+	err := util.InitLog(logLevel, util.LogConsole)
 	if err != nil {
 		return fmt.Errorf("failed initializing log %v", err)
 	}
@@ -484,7 +484,7 @@ func parseCustomDNSAddress(modified bool) ([]byte, error) {
 		if !isValidAddrPort(customDNSAddress) {
 			return nil, fmt.Errorf("%s is invalid, it should be formatted as IP:Port string or as an empty string like \"\"", customDNSAddress)
 		}
-		if customDNSAddress == "" && logFile != "console" {
+		if customDNSAddress == "" && util.FindFirstLogPath(logFiles) != "" {
 			parsed = []byte("empty")
 		} else {
 			parsed = []byte(customDNSAddress)
diff --git a/client/internal/debug/debug.go b/client/internal/debug/debug.go
index 6455b3aa..220e6854 100644
--- a/client/internal/debug/debug.go
+++ b/client/internal/debug/debug.go
@@ -16,6 +16,7 @@ import (
 	"path/filepath"
 	"runtime"
 	"runtime/pprof"
+	"slices"
 	"sort"
 	"strings"
 	"time"
@@ -28,6 +29,7 @@ import (
 	"github.com/netbirdio/netbird/client/internal/peer"
 	"github.com/netbirdio/netbird/client/internal/statemanager"
 	mgmProto "github.com/netbirdio/netbird/management/proto"
+	"github.com/netbirdio/netbird/util"
 )
 
 const readmeContent = `Netbird debug bundle
@@ -283,7 +285,7 @@ func (g *BundleGenerator) createArchive() error {
 		log.Errorf("Failed to add wg show output: %v", err)
 	}
 
-	if g.logFile != "console" && g.logFile != "" {
+	if g.logFile != "" && !slices.Contains(util.SpecialLogs, g.logFile) {
 		if err := g.addLogfile(); err != nil {
 			log.Errorf("Failed to add log file to debug bundle: %v", err)
 			if err := g.trySystemdLogFallback(); err != nil {
diff --git a/client/internal/engine.go b/client/internal/engine.go
index 1abb8163..079adf7e 100644
--- a/client/internal/engine.go
+++ b/client/internal/engine.go
@@ -1968,21 +1968,24 @@ func (e *Engine) toExcludedLazyPeers(rules []firewallManager.ForwardRule, peers
 }
 
 // isChecksEqual checks if two slices of checks are equal.
-func isChecksEqual(checks []*mgmProto.Checks, oChecks []*mgmProto.Checks) bool {
-	for _, check := range checks {
-		sort.Slice(check.Files, func(i, j int) bool {
-			return check.Files[i] < check.Files[j]
-		})
-	}
-	for _, oCheck := range oChecks {
-		sort.Slice(oCheck.Files, func(i, j int) bool {
-			return oCheck.Files[i] < oCheck.Files[j]
-		})
+func isChecksEqual(checks1, checks2 []*mgmProto.Checks) bool {
+	normalize := func(checks []*mgmProto.Checks) []string {
+		normalized := make([]string, len(checks))
+
+		for i, check := range checks {
+			sortedFiles := slices.Clone(check.Files)
+			sort.Strings(sortedFiles)
+			normalized[i] = strings.Join(sortedFiles, "|")
+		}
+
+		sort.Strings(normalized)
+		return normalized
 	}
 
-	return slices.EqualFunc(checks, oChecks, func(checks, oChecks *mgmProto.Checks) bool {
-		return slices.Equal(checks.Files, oChecks.Files)
-	})
+	n1 := normalize(checks1)
+	n2 := normalize(checks2)
+
+	return slices.Equal(n1, n2)
 }
 
 func getInterfacePrefixes() ([]netip.Prefix, error) {
diff --git a/client/internal/peer/ice/agent.go b/client/internal/peer/ice/agent.go
index 9b63cebf..4a022840 100644
--- a/client/internal/peer/ice/agent.go
+++ b/client/internal/peer/ice/agent.go
@@ -18,17 +18,15 @@ const (
 
 	iceKeepAliveDefault           = 4 * time.Second
 	iceDisconnectedTimeoutDefault = 6 * time.Second
+	iceFailedTimeoutDefault       = 6 * time.Second
 	// iceRelayAcceptanceMinWaitDefault is the same as in the Pion ICE package
 	iceRelayAcceptanceMinWaitDefault = 2 * time.Second
 )
 
-var (
-	failedTimeout = 6 * time.Second
-)
-
 func NewAgent(iFaceDiscover stdnet.ExternalIFaceDiscover, config Config, candidateTypes []ice.CandidateType, ufrag string, pwd string) (*ice.Agent, error) {
 	iceKeepAlive := iceKeepAlive()
 	iceDisconnectedTimeout := iceDisconnectedTimeout()
+	iceFailedTimeout := iceFailedTimeout()
 	iceRelayAcceptanceMinWait := iceRelayAcceptanceMinWait()
 
 	transportNet, err := newStdNet(iFaceDiscover, config.InterfaceBlackList)
@@ -50,7 +48,7 @@ func NewAgent(iFaceDiscover stdnet.ExternalIFaceDiscover, config Config, candida
 		UDPMuxSrflx:            config.UDPMuxSrflx,
 		NAT1To1IPs:             config.NATExternalIPs,
 		Net:                    transportNet,
-		FailedTimeout:          &failedTimeout,
+		FailedTimeout:          &iceFailedTimeout,
 		DisconnectedTimeout:    &iceDisconnectedTimeout,
 		KeepaliveInterval:      &iceKeepAlive,
 		RelayAcceptanceMinWait: &iceRelayAcceptanceMinWait,
diff --git a/client/internal/peer/ice/env.go b/client/internal/peer/ice/env.go
index 3b0cb74a..c11c3544 100644
--- a/client/internal/peer/ice/env.go
+++ b/client/internal/peer/ice/env.go
@@ -13,6 +13,7 @@ const (
 	envICEForceRelayConn            = "NB_ICE_FORCE_RELAY_CONN"
 	envICEKeepAliveIntervalSec      = "NB_ICE_KEEP_ALIVE_INTERVAL_SEC"
 	envICEDisconnectedTimeoutSec    = "NB_ICE_DISCONNECTED_TIMEOUT_SEC"
+	envICEFailedTimeoutSec          = "NB_ICE_FAILED_TIMEOUT_SEC"
 	envICERelayAcceptanceMinWaitSec = "NB_ICE_RELAY_ACCEPTANCE_MIN_WAIT_SEC"
 
 	msgWarnInvalidValue = "invalid value %s set for %s, using default %v"
@@ -55,6 +56,22 @@ func iceDisconnectedTimeout() time.Duration {
 	return time.Duration(disconnectedTimeoutSec) * time.Second
 }
 
+func iceFailedTimeout() time.Duration {
+	failedTimeoutEnv := os.Getenv(envICEFailedTimeoutSec)
+	if failedTimeoutEnv == "" {
+		return iceFailedTimeoutDefault
+	}
+
+	log.Infof("setting ICE failed timeout to %s seconds", failedTimeoutEnv)
+	failedTimeoutSec, err := strconv.Atoi(failedTimeoutEnv)
+	if err != nil {
+		log.Warnf(msgWarnInvalidValue, failedTimeoutEnv, envICEFailedTimeoutSec, iceFailedTimeoutDefault)
+		return iceFailedTimeoutDefault
+	}
+
+	return time.Duration(failedTimeoutSec) * time.Second
+}
+
 func iceRelayAcceptanceMinWait() time.Duration {
 	iceRelayAcceptanceMinWaitEnv := os.Getenv(envICERelayAcceptanceMinWaitSec)
 	if iceRelayAcceptanceMinWaitEnv == "" {
diff --git a/client/netbird-entrypoint.sh b/client/netbird-entrypoint.sh
new file mode 100755
index 00000000..2422d268
--- /dev/null
+++ b/client/netbird-entrypoint.sh
@@ -0,0 +1,105 @@
+#!/usr/bin/env bash
+set -eEuo pipefail
+
+: ${NB_ENTRYPOINT_SERVICE_TIMEOUT:="5"}
+: ${NB_ENTRYPOINT_LOGIN_TIMEOUT:="1"}
+NETBIRD_BIN="${NETBIRD_BIN:-"netbird"}"
+export NB_LOG_FILE="${NB_LOG_FILE:-"console,/var/log/netbird/client.log"}"
+service_pids=()
+log_file_path=""
+
+_log() {
+  # mimic Go logger's output for easier parsing
+  # 2025-04-15T21:32:00+08:00 INFO client/internal/config.go:495: setting notifications to disabled by default
+  printf "$(date -Isec) ${1} ${BASH_SOURCE[1]}:${BASH_LINENO[1]}: ${2}\n" "${@:3}" >&2
+}
+
+info() {
+  _log INFO "$@"
+}
+
+warn() {
+  _log WARN "$@"
+}
+
+on_exit() {
+  info "Shutting down NetBird daemon..."
+  if test "${#service_pids[@]}" -gt 0; then
+    info "terminating service process IDs: ${service_pids[@]@Q}"
+    kill -TERM "${service_pids[@]}" 2>/dev/null || true
+    wait "${service_pids[@]}" 2>/dev/null || true
+  else
+    info "there are no service processes to terminate"
+  fi
+}
+
+wait_for_message() {
+  local timeout="${1}" message="${2}"
+  if test "${timeout}" -eq 0; then
+    info "not waiting for log line ${message@Q} due to zero timeout."
+  elif test -n "${log_file_path}"; then
+    info "waiting for log line ${message@Q} for ${timeout} seconds..."
+    grep -q "${message}" <(timeout "${timeout}" tail -F "${log_file_path}" 2>/dev/null)
+  else
+    info "log file unsupported, sleeping for ${timeout} seconds..."
+    sleep "${timeout}"
+  fi
+}
+
+locate_log_file() {
+  local log_files_string="${1}"
+
+  while read -r log_file; do
+    case "${log_file}" in
+    console | syslog) ;;
+    *)
+      log_file_path="${log_file}"
+      return
+      ;;
+    esac
+  done < <(sed 's#,#\n#g' <<<"${log_files_string}")
+
+  warn "log files parsing for ${log_files_string@Q} is not supported by debug bundles"
+  warn "please consider removing the \$NB_LOG_FILE or setting it to real file, before gathering debug bundles."
+}
+
+wait_for_daemon_startup() {
+  local timeout="${1}"
+
+  if test -n "${log_file_path}"; then
+    if ! wait_for_message "${timeout}" "started daemon server"; then
+      warn "log line containing 'started daemon server' not found after ${timeout} seconds"
+      warn "daemon failed to start, exiting..."
+      exit 1
+    fi
+  else
+    warn "daemon service startup not discovered, sleeping ${timeout} instead"
+    sleep "${timeout}"
+  fi
+}
+
+login_if_needed() {
+  local timeout="${1}"
+
+  if test -n "${log_file_path}" && wait_for_message "${timeout}" 'peer has been successfully registered'; then
+    info "already logged in, skipping 'netbird up'..."
+  else
+    info "logging in..."
+    "${NETBIRD_BIN}" up
+  fi
+}
+
+main() {
+  trap 'on_exit' SIGTERM SIGINT EXIT
+  "${NETBIRD_BIN}" service run &
+  service_pids+=("$!")
+  info "registered new service process 'netbird service run', currently running: ${service_pids[@]@Q}"
+
+  locate_log_file "${NB_LOG_FILE}"
+  wait_for_daemon_startup "${NB_ENTRYPOINT_SERVICE_TIMEOUT}"
+  login_if_needed "${NB_ENTRYPOINT_LOGIN_TIMEOUT}"
+
+  wait "${service_pids[@]}"
+}
+
+main "$@"
diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go
index 753aa62d..26e58d18 100644
--- a/client/proto/daemon.pb.go
+++ b/client/proto/daemon.pb.go
@@ -1330,13 +1330,6 @@ func (x *PeerState) GetRelayAddress() string {
 	return ""
 }
 
-func (x *PeerState) GetConnectionType() string {
-	if x.Relayed {
-		return "Relayed"
-	}
-	return "P2P"
-}
-
 // LocalPeerState contains the latest state of the local peer
 type LocalPeerState struct {
 	state               protoimpl.MessageState `protogen:"open.v1"`
diff --git a/client/status/status.go b/client/status/status.go
index 507c7ea8..d28485bc 100644
--- a/client/status/status.go
+++ b/client/status/status.go
@@ -203,13 +203,18 @@ func mapPeers(
 		localICEEndpoint := ""
 		remoteICEEndpoint := ""
 		relayServerAddress := ""
-		connType := ""
+		connType := "P2P"
 		lastHandshake := time.Time{}
 		transferReceived := int64(0)
 		transferSent := int64(0)
 
 		isPeerConnected := pbPeerState.ConnStatus == peer.StatusConnected.String()
-		if skipDetailByFilters(pbPeerState, pbPeerState.ConnStatus, statusFilter, prefixNamesFilter, prefixNamesFilterMap, ipsFilter, connectionTypeFilter) {
+
+		if pbPeerState.Relayed {
+			connType = "Relayed"
+		}
+
+		if skipDetailByFilters(pbPeerState, pbPeerState.ConnStatus, statusFilter, prefixNamesFilter, prefixNamesFilterMap, ipsFilter, connectionTypeFilter, connType) {
 			continue
 		}
 		if isPeerConnected {
@@ -219,7 +224,6 @@ func mapPeers(
 			remoteICE = pbPeerState.GetRemoteIceCandidateType()
 			localICEEndpoint = pbPeerState.GetLocalIceCandidateEndpoint()
 			remoteICEEndpoint = pbPeerState.GetRemoteIceCandidateEndpoint()
-			connType = pbPeerState.GetConnectionType()
 			relayServerAddress = pbPeerState.GetRelayAddress()
 			lastHandshake = pbPeerState.GetLastWireguardHandshake().AsTime().Local()
 			transferReceived = pbPeerState.GetBytesRx()
@@ -540,7 +544,7 @@ func parsePeers(peers PeersStateOutput, rosenpassEnabled, rosenpassPermissive bo
 	return peersString
 }
 
-func skipDetailByFilters(peerState *proto.PeerState, peerStatus string, statusFilter string, prefixNamesFilter []string, prefixNamesFilterMap map[string]struct{}, ipsFilter map[string]struct{}, connectionTypeFilter string) bool {
+func skipDetailByFilters(peerState *proto.PeerState, peerStatus string, statusFilter string, prefixNamesFilter []string, prefixNamesFilterMap map[string]struct{}, ipsFilter map[string]struct{}, connectionTypeFilter, connType string) bool {
 	statusEval := false
 	ipEval := false
 	nameEval := true
@@ -569,7 +573,7 @@ func skipDetailByFilters(peerState *proto.PeerState, peerStatus string, statusFi
 	} else {
 		nameEval = false
 	}
-	if connectionTypeFilter != "" && !strings.EqualFold(peerState.GetConnectionType(), connectionTypeFilter) {
+	if connectionTypeFilter != "" && !strings.EqualFold(connType, connectionTypeFilter) {
 		connectionTypeEval = true
 	}
 
diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go
index ace5b71e..4480adb5 100644
--- a/client/ui/client_ui.go
+++ b/client/ui/client_ui.go
@@ -66,7 +66,7 @@ func main() {
 		}
 		logFile = file
 	} else {
-		_ = util.InitLog("trace", "console")
+		_ = util.InitLog("trace", util.LogConsole)
 	}
 
 	// Create the Fyne application.
diff --git a/go.mod b/go.mod
index cf2a2375..4d9191d0 100644
--- a/go.mod
+++ b/go.mod
@@ -63,7 +63,7 @@ require (
 	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-20250718071730-f4d133556ff5
+	github.com/netbirdio/management-integrations/integrations v0.0.0-20250724151510-c007bc6b392c
 	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
diff --git a/go.sum b/go.sum
index 699a832d..84dfe240 100644
--- a/go.sum
+++ b/go.sum
@@ -503,8 +503,8 @@ 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-20250718071730-f4d133556ff5 h1:Zfn8d83OVyELCdxgprcyXR3D8uqoxHtXE9PUxVXDx/w=
-github.com/netbirdio/management-integrations/integrations v0.0.0-20250718071730-f4d133556ff5/go.mod h1:Gi9raplYzCCyh07Olw/DVfCJTFgpr1WCXJ/Q+8TSA9Q=
+github.com/netbirdio/management-integrations/integrations v0.0.0-20250724151510-c007bc6b392c h1:OtX903X0FKEE+fcsp/P2701md7X/xbi/W/ojWIJNKSk=
+github.com/netbirdio/management-integrations/integrations v0.0.0-20250724151510-c007bc6b392c/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-20250514131221-a464fd5f30cb h1:Cr6age+ePALqlSvtp7wc6lYY97XN7rkD1K4XEDmY+TU=
diff --git a/management/server/account/manager.go b/management/server/account/manager.go
index f8aa2756..8c7e95e3 100644
--- a/management/server/account/manager.go
+++ b/management/server/account/manager.go
@@ -101,7 +101,7 @@ type Manager interface {
 	DeletePostureChecks(ctx context.Context, accountID, postureChecksID, userID string) error
 	ListPostureChecks(ctx context.Context, accountID, userID string) ([]*posture.Checks, error)
 	GetIdpManager() idp.Manager
-	UpdateIntegratedValidatorGroups(ctx context.Context, accountID string, userID string, groups []string) error
+	UpdateIntegratedValidator(ctx context.Context, accountID, userID, validator string, groups []string) error
 	GroupValidation(ctx context.Context, accountId string, groups []string) (bool, error)
 	GetValidatedPeers(ctx context.Context, accountID string) (map[string]struct{}, error)
 	SyncAndMarkPeer(ctx context.Context, accountID string, peerPubKey string, meta nbpeer.PeerSystemMeta, realIP net.IP) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error)
diff --git a/management/server/integrated_validator.go b/management/server/integrated_validator.go
index e3e47441..b89739be 100644
--- a/management/server/integrated_validator.go
+++ b/management/server/integrated_validator.go
@@ -3,6 +3,7 @@ package server
 import (
 	"context"
 	"errors"
+	"fmt"
 
 	log "github.com/sirupsen/logrus"
 
@@ -12,34 +13,44 @@ import (
 	"github.com/netbirdio/netbird/management/server/types"
 )
 
-// UpdateIntegratedValidatorGroups updates the integrated validator groups for a specified account.
+// UpdateIntegratedValidator updates the integrated validator groups for a specified account.
 // It retrieves the account associated with the provided userID, then updates the integrated validator groups
 // with the provided list of group ids. The updated account is then saved.
 //
 // Parameters:
 //   - accountID: The ID of the account for which integrated validator groups are to be updated.
 //   - userID: The ID of the user whose account is being updated.
+//   - validator: The validator type to use, or empty to remove.
 //   - groups: A slice of strings representing the ids of integrated validator groups to be updated.
 //
 // Returns:
 //   - error: An error if any occurred during the process, otherwise returns nil
-func (am *DefaultAccountManager) UpdateIntegratedValidatorGroups(ctx context.Context, accountID string, userID string, groups []string) error {
-	ok, err := am.GroupValidation(ctx, accountID, groups)
-	if err != nil {
-		log.WithContext(ctx).Debugf("error validating groups: %s", err.Error())
-		return err
+func (am *DefaultAccountManager) UpdateIntegratedValidator(ctx context.Context, accountID, userID, validator string, groups []string) error {
+	if validator != "" && len(groups) == 0 {
+		return fmt.Errorf("at least one group must be specified for validator")
 	}
 
-	if !ok {
-		log.WithContext(ctx).Debugf("invalid groups")
-		return errors.New("invalid groups")
+	if validator != "" {
+		ok, err := am.GroupValidation(ctx, accountID, groups)
+		if err != nil {
+			log.WithContext(ctx).Debugf("error validating groups: %s", err.Error())
+			return err
+		}
+
+		if !ok {
+			log.WithContext(ctx).Debugf("invalid groups")
+			return errors.New("invalid groups")
+		}
+	} else {
+		// ensure groups is empty
+		groups = []string{}
 	}
 
 	unlock := am.Store.AcquireWriteLockByUID(ctx, accountID)
 	defer unlock()
 
 	return am.Store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
-		a, err := transaction.GetAccountByUser(ctx, userID)
+		a, err := transaction.GetAccount(ctx, accountID)
 		if err != nil {
 			return err
 		}
@@ -52,6 +63,8 @@ func (am *DefaultAccountManager) UpdateIntegratedValidatorGroups(ctx context.Con
 			extra = &types.ExtraSettings{}
 			a.Settings.Extra = extra
 		}
+
+		extra.IntegratedValidator = validator
 		extra.IntegratedValidatorGroups = groups
 		return transaction.SaveAccount(ctx, a)
 	})
@@ -99,7 +112,7 @@ func (am *DefaultAccountManager) GetValidatedPeers(ctx context.Context, accountI
 		return nil, err
 	}
 
-	return am.integratedPeerValidator.GetValidatedPeers(accountID, groups, peers, settings.Extra)
+	return am.integratedPeerValidator.GetValidatedPeers(ctx, accountID, groups, peers, settings.Extra)
 }
 
 type MockIntegratedValidator struct {
@@ -118,7 +131,7 @@ func (a MockIntegratedValidator) ValidatePeer(_ context.Context, update *nbpeer.
 	return update, false, nil
 }
 
-func (a MockIntegratedValidator) GetValidatedPeers(accountID string, groups []*types.Group, peers []*nbpeer.Peer, extraSettings *types.ExtraSettings) (map[string]struct{}, error) {
+func (a MockIntegratedValidator) GetValidatedPeers(_ context.Context, accountID string, groups []*types.Group, peers []*nbpeer.Peer, extraSettings *types.ExtraSettings) (map[string]struct{}, error) {
 	validatedPeers := make(map[string]struct{})
 	for _, peer := range peers {
 		validatedPeers[peer.ID] = struct{}{}
@@ -134,7 +147,7 @@ func (MockIntegratedValidator) IsNotValidPeer(_ context.Context, accountID strin
 	return false, false, nil
 }
 
-func (MockIntegratedValidator) PeerDeleted(_ context.Context, _, _ string) error {
+func (MockIntegratedValidator) PeerDeleted(_ context.Context, _, _ string, extraSettings *types.ExtraSettings) error {
 	return nil
 }
 
diff --git a/management/server/integrations/integrated_validator/interface.go b/management/server/integrations/integrated_validator/interface.go
index 245c0168..4d4a8cdf 100644
--- a/management/server/integrations/integrated_validator/interface.go
+++ b/management/server/integrations/integrated_validator/interface.go
@@ -14,8 +14,8 @@ type IntegratedValidator interface {
 	ValidatePeer(ctx context.Context, update *nbpeer.Peer, peer *nbpeer.Peer, userID string, accountID string, dnsDomain string, peersGroup []string, extraSettings *types.ExtraSettings) (*nbpeer.Peer, bool, error)
 	PreparePeer(ctx context.Context, accountID string, peer *nbpeer.Peer, peersGroup []string, extraSettings *types.ExtraSettings) *nbpeer.Peer
 	IsNotValidPeer(ctx context.Context, accountID string, peer *nbpeer.Peer, peersGroup []string, extraSettings *types.ExtraSettings) (bool, bool, error)
-	GetValidatedPeers(accountID string, groups []*types.Group, peers []*nbpeer.Peer, extraSettings *types.ExtraSettings) (map[string]struct{}, error)
-	PeerDeleted(ctx context.Context, accountID, peerID string) error
+	GetValidatedPeers(ctx context.Context, accountID string, groups []*types.Group, peers []*nbpeer.Peer, extraSettings *types.ExtraSettings) (map[string]struct{}, error)
+	PeerDeleted(ctx context.Context, accountID, peerID string, extraSettings *types.ExtraSettings) error
 	SetPeerInvalidationListener(fn func(accountID string))
 	Stop(ctx context.Context)
 	ValidateFlowResponse(ctx context.Context, peerKey string, flowResponse *proto.PKCEAuthorizationFlow) *proto.PKCEAuthorizationFlow
diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go
index b1ec6628..a16e3652 100644
--- a/management/server/mock_server/account_mock.go
+++ b/management/server/mock_server/account_mock.go
@@ -102,7 +102,7 @@ type MockAccountManager struct {
 	DeletePostureChecksFunc               func(ctx context.Context, accountID, postureChecksID, userID string) error
 	ListPostureChecksFunc                 func(ctx context.Context, accountID, userID string) ([]*posture.Checks, error)
 	GetIdpManagerFunc                     func() idp.Manager
-	UpdateIntegratedValidatorGroupsFunc   func(ctx context.Context, accountID string, userID string, groups []string) error
+	UpdateIntegratedValidatorFunc         func(ctx context.Context, accountID, userID, validator string, groups []string) error
 	GroupValidationFunc                   func(ctx context.Context, accountId string, groups []string) (bool, error)
 	SyncPeerMetaFunc                      func(ctx context.Context, peerPubKey string, meta nbpeer.PeerSystemMeta) error
 	FindExistingPostureCheckFunc          func(accountID string, checks *posture.ChecksDefinition) (*posture.Checks, error)
@@ -769,10 +769,10 @@ func (am *MockAccountManager) GetIdpManager() idp.Manager {
 	return nil
 }
 
-// UpdateIntegratedValidatorGroups mocks UpdateIntegratedApprovalGroups of the AccountManager interface
-func (am *MockAccountManager) UpdateIntegratedValidatorGroups(ctx context.Context, accountID string, userID string, groups []string) error {
-	if am.UpdateIntegratedValidatorGroupsFunc != nil {
-		return am.UpdateIntegratedValidatorGroupsFunc(ctx, accountID, userID, groups)
+// UpdateIntegratedValidator mocks UpdateIntegratedApprovalGroups of the AccountManager interface
+func (am *MockAccountManager) UpdateIntegratedValidator(ctx context.Context, accountID, userID, validator string, groups []string) error {
+	if am.UpdateIntegratedValidatorFunc != nil {
+		return am.UpdateIntegratedValidatorFunc(ctx, accountID, userID, validator, groups)
 	}
 	return status.Errorf(codes.Unimplemented, "method UpdateIntegratedValidatorGroups is not implemented")
 }
diff --git a/management/server/peer.go b/management/server/peer.go
index c6ade83c..3c40c6bb 100644
--- a/management/server/peer.go
+++ b/management/server/peer.go
@@ -23,6 +23,7 @@ import (
 	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"
+	"github.com/netbirdio/netbird/util"
 
 	"github.com/netbirdio/netbird/management/server/posture"
 	"github.com/netbirdio/netbird/management/server/store"
@@ -87,7 +88,7 @@ func (am *DefaultAccountManager) getUserAccessiblePeers(ctx context.Context, acc
 		return nil, err
 	}
 
-	approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(accountID, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
+	approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(ctx, accountID, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
 	if err != nil {
 		return nil, err
 	}
@@ -412,7 +413,7 @@ func (am *DefaultAccountManager) GetNetworkMap(ctx context.Context, peerID strin
 		groups[groupID] = group.Peers
 	}
 
-	validatedPeers, err := am.integratedPeerValidator.GetValidatedPeers(account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
+	validatedPeers, err := am.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
 	if err != nil {
 		return nil, err
 	}
@@ -1036,7 +1037,7 @@ func (am *DefaultAccountManager) getValidatedPeerWithMap(ctx context.Context, is
 		return nil, nil, nil, err
 	}
 
-	approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
+	approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
 	if err != nil {
 		return nil, nil, nil, err
 	}
@@ -1156,7 +1157,7 @@ func (am *DefaultAccountManager) checkIfUserOwnsPeer(ctx context.Context, accoun
 		return nil, err
 	}
 
-	approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(accountID, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
+	approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(ctx, accountID, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
 	if err != nil {
 		return nil, err
 	}
@@ -1183,6 +1184,8 @@ func (am *DefaultAccountManager) checkIfUserOwnsPeer(ctx context.Context, accoun
 // UpdateAccountPeers updates all peers that belong to an account.
 // Should be called when changes have to be synced to peers.
 func (am *DefaultAccountManager) UpdateAccountPeers(ctx context.Context, accountID string) {
+	log.WithContext(ctx).Tracef("updating peers for account %s from %s", accountID, util.GetCallerName())
+
 	account, err := am.requestBuffer.GetAccountWithBackpressure(ctx, accountID)
 	if err != nil {
 		log.WithContext(ctx).Errorf("failed to send out updates to peers. failed to get account: %v", err)
@@ -1204,7 +1207,7 @@ func (am *DefaultAccountManager) UpdateAccountPeers(ctx context.Context, account
 		return
 	}
 
-	approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
+	approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
 	if err != nil {
 		log.WithContext(ctx).Errorf("failed to send out updates to peers, failed to get validate peers: %v", err)
 		return
@@ -1288,6 +1291,8 @@ type bufferUpdate struct {
 }
 
 func (am *DefaultAccountManager) BufferUpdateAccountPeers(ctx context.Context, accountID string) {
+	log.WithContext(ctx).Tracef("buffer updating peers for account %s from %s", accountID, util.GetCallerName())
+
 	bufUpd, _ := am.accountUpdateLocks.LoadOrStore(accountID, &bufferUpdate{})
 	b := bufUpd.(*bufferUpdate)
 
@@ -1337,7 +1342,7 @@ func (am *DefaultAccountManager) UpdateAccountPeer(ctx context.Context, accountI
 		return
 	}
 
-	approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
+	approvedPeersMap, err := am.integratedPeerValidator.GetValidatedPeers(ctx, account.Id, maps.Values(account.Groups), maps.Values(account.Peers), account.Settings.Extra)
 	if err != nil {
 		log.WithContext(ctx).Errorf("failed to send update to peer %s, failed to validate peers: %v", peerId, err)
 		return
@@ -1571,7 +1576,7 @@ func deletePeers(ctx context.Context, am *DefaultAccountManager, transaction sto
 			}
 		}
 
-		if err := am.integratedPeerValidator.PeerDeleted(ctx, accountID, peer.ID); err != nil {
+		if err := am.integratedPeerValidator.PeerDeleted(ctx, accountID, peer.ID, settings.Extra); err != nil {
 			return nil, err
 		}
 
diff --git a/management/server/scheduler.go b/management/server/scheduler.go
index df73c9a1..b6164329 100644
--- a/management/server/scheduler.go
+++ b/management/server/scheduler.go
@@ -11,14 +11,17 @@ import (
 // Scheduler is an interface which implementations can schedule and cancel jobs
 type Scheduler interface {
 	Cancel(ctx context.Context, IDs []string)
+	CancelAll(ctx context.Context)
 	Schedule(ctx context.Context, in time.Duration, ID string, job func() (nextRunIn time.Duration, reschedule bool))
 	IsSchedulerRunning(ID string) bool
 }
 
 // MockScheduler is a mock implementation of  Scheduler
 type MockScheduler struct {
-	CancelFunc   func(ctx context.Context, IDs []string)
-	ScheduleFunc func(ctx context.Context, in time.Duration, ID string, job func() (nextRunIn time.Duration, reschedule bool))
+	CancelFunc             func(ctx context.Context, IDs []string)
+	CancelAllFunc          func(ctx context.Context)
+	ScheduleFunc           func(ctx context.Context, in time.Duration, ID string, job func() (nextRunIn time.Duration, reschedule bool))
+	IsSchedulerRunningFunc func(ID string) bool
 }
 
 // Cancel mocks the Cancel function of the Scheduler interface
@@ -30,6 +33,15 @@ func (mock *MockScheduler) Cancel(ctx context.Context, IDs []string) {
 	log.WithContext(ctx).Warnf("MockScheduler doesn't have Cancel function defined ")
 }
 
+// CancelAll mocks the CancelAll function of the Scheduler interface
+func (mock *MockScheduler) CancelAll(ctx context.Context) {
+	if mock.CancelAllFunc != nil {
+		mock.CancelAllFunc(ctx)
+		return
+	}
+	log.WithContext(ctx).Warnf("MockScheduler doesn't have CancelAll function defined ")
+}
+
 // Schedule mocks the Schedule function of the Scheduler interface
 func (mock *MockScheduler) Schedule(ctx context.Context, in time.Duration, ID string, job func() (nextRunIn time.Duration, reschedule bool)) {
 	if mock.ScheduleFunc != nil {
@@ -40,7 +52,9 @@ func (mock *MockScheduler) Schedule(ctx context.Context, in time.Duration, ID st
 }
 
 func (mock *MockScheduler) IsSchedulerRunning(ID string) bool {
-	// MockScheduler does not implement IsSchedulerRunning, so we return false
+	if mock.IsSchedulerRunningFunc != nil {
+		return mock.IsSchedulerRunningFunc(ID)
+	}
 	log.Warnf("MockScheduler doesn't have IsSchedulerRunning function defined")
 	return false
 }
@@ -52,6 +66,15 @@ type DefaultScheduler struct {
 	mu   *sync.Mutex
 }
 
+func (wm *DefaultScheduler) CancelAll(ctx context.Context) {
+	wm.mu.Lock()
+	defer wm.mu.Unlock()
+
+	for id := range wm.jobs {
+		wm.cancel(ctx, id)
+	}
+}
+
 // NewDefaultScheduler creates an instance of a DefaultScheduler
 func NewDefaultScheduler() *DefaultScheduler {
 	return &DefaultScheduler{
diff --git a/management/server/types/settings.go b/management/server/types/settings.go
index a22a36b0..37c728bf 100644
--- a/management/server/types/settings.go
+++ b/management/server/types/settings.go
@@ -77,6 +77,8 @@ type ExtraSettings struct {
 	// PeerApprovalEnabled enables or disables the need for peers bo be approved by an administrator
 	PeerApprovalEnabled bool
 
+	// IntegratedValidator is the string enum for the integrated validator type
+	IntegratedValidator string
 	// IntegratedValidatorGroups list of group IDs to be used with integrated approval configurations
 	IntegratedValidatorGroups []string `gorm:"serializer:json"`
 
@@ -93,5 +95,10 @@ func (e *ExtraSettings) Copy() *ExtraSettings {
 	return &ExtraSettings{
 		PeerApprovalEnabled:       e.PeerApprovalEnabled,
 		IntegratedValidatorGroups: append(cpGroup, e.IntegratedValidatorGroups...),
+		IntegratedValidator:       e.IntegratedValidator,
+		FlowEnabled:               e.FlowEnabled,
+		FlowPacketCounterEnabled:  e.FlowPacketCounterEnabled,
+		FlowENCollectionEnabled:   e.FlowENCollectionEnabled,
+		FlowDnsCollectionEnabled:  e.FlowDnsCollectionEnabled,
 	}
 }
diff --git a/relay/cmd/root.go b/relay/cmd/root.go
index 15090024..7b8e5bbe 100644
--- a/relay/cmd/root.go
+++ b/relay/cmd/root.go
@@ -73,7 +73,7 @@ var (
 )
 
 func init() {
-	_ = util.InitLog("trace", "console")
+	_ = util.InitLog("trace", util.LogConsole)
 	cobraConfig = &Config{}
 	rootCmd.PersistentFlags().StringVarP(&cobraConfig.ListenAddress, "listen-address", "l", ":443", "listen address")
 	rootCmd.PersistentFlags().StringVarP(&cobraConfig.ExposedAddress, "exposed-address", "e", "", "instance domain address (or ip) and port, it will be distributes between peers")
diff --git a/upload-server/main.go b/upload-server/main.go
index dcfb35cd..546c0f58 100644
--- a/upload-server/main.go
+++ b/upload-server/main.go
@@ -10,7 +10,7 @@ import (
 )
 
 func main() {
-	err := util.InitLog("info", "console")
+	err := util.InitLog("info", util.LogConsole)
 	if err != nil {
 		log.Fatalf("Failed to initialize logger: %v", err)
 	}
diff --git a/util/log.go b/util/log.go
index 53d2b068..a951eab8 100644
--- a/util/log.go
+++ b/util/log.go
@@ -16,36 +16,54 @@ import (
 
 const defaultLogSize = 15
 
+const (
+	LogConsole = "console"
+	LogSyslog  = "syslog"
+)
+
+var (
+	SpecialLogs = []string{
+		LogSyslog,
+		LogConsole,
+	}
+)
+
 // InitLog parses and sets log-level input
-func InitLog(logLevel string, logPath string) error {
+func InitLog(logLevel string, logs ...string) error {
 	level, err := log.ParseLevel(logLevel)
 	if err != nil {
 		log.Errorf("Failed parsing log-level %s: %s", logLevel, err)
 		return err
 	}
-	customOutputs := []string{"console", "syslog"}
-
-	if logPath != "" && !slices.Contains(customOutputs, logPath) {
-		maxLogSize := getLogMaxSize()
-		lumberjackLogger := &lumberjack.Logger{
-			// Log file absolute path, os agnostic
-			Filename:   filepath.ToSlash(logPath),
-			MaxSize:    maxLogSize, // MB
-			MaxBackups: 10,
-			MaxAge:     30, // days
-			Compress:   true,
+	var writers []io.Writer
+	logFmt := os.Getenv("NB_LOG_FORMAT")
+
+	for _, logPath := range logs {
+		switch logPath {
+		case LogSyslog:
+			AddSyslogHook()
+			logFmt = "syslog"
+		case LogConsole:
+			writers = append(writers, os.Stderr)
+		case "":
+			log.Warnf("empty log path received: %#v", logPath)
+		default:
+			writers = append(writers, newRotatedOutput(logPath))
 		}
-		log.SetOutput(io.Writer(lumberjackLogger))
-	} else if logPath == "syslog" {
-		AddSyslogHook()
 	}
 
-	//nolint:gocritic
-	if os.Getenv("NB_LOG_FORMAT") == "json" {
+	if len(writers) > 1 {
+		log.SetOutput(io.MultiWriter(writers...))
+	} else if len(writers) == 1 {
+		log.SetOutput(writers[0])
+	}
+
+	switch logFmt {
+	case "json":
 		formatter.SetJSONFormatter(log.StandardLogger())
-	} else if logPath == "syslog" {
+	case "syslog":
 		formatter.SetSyslogFormatter(log.StandardLogger())
-	} else {
+	default:
 		formatter.SetTextFormatter(log.StandardLogger())
 	}
 	log.SetLevel(level)
@@ -55,6 +73,29 @@ func InitLog(logLevel string, logPath string) error {
 	return nil
 }
 
+// FindFirstLogPath returns the first logs entry that could be a log path, that is neither empty, nor a special value
+func FindFirstLogPath(logs []string) string {
+	for _, logFile := range logs {
+		if logFile != "" && !slices.Contains(SpecialLogs, logFile) {
+			return logFile
+		}
+	}
+	return ""
+}
+
+func newRotatedOutput(logPath string) io.Writer {
+	maxLogSize := getLogMaxSize()
+	lumberjackLogger := &lumberjack.Logger{
+		// Log file absolute path, os agnostic
+		Filename:   filepath.ToSlash(logPath),
+		MaxSize:    maxLogSize, // MB
+		MaxBackups: 10,
+		MaxAge:     30, // days
+		Compress:   true,
+	}
+	return lumberjackLogger
+}
+
 func setGRPCLibLogger() {
 	logOut := log.StandardLogger().Writer()
 	if os.Getenv("GRPC_GO_LOG_SEVERITY_LEVEL") != "info" {
diff --git a/util/runtime.go b/util/runtime.go
new file mode 100644
index 00000000..3b420e15
--- /dev/null
+++ b/util/runtime.go
@@ -0,0 +1,15 @@
+package util
+
+import "runtime"
+
+func GetCallerName() string {
+	pc, _, _, ok := runtime.Caller(2)
+	if !ok {
+		return "unknown"
+	}
+	fn := runtime.FuncForPC(pc)
+	if fn == nil {
+		return "unknown"
+	}
+	return fn.Name()
+}



Test patch:
diff --git a/client/cmd/login_test.go b/client/cmd/login_test.go
index fa20435e..cf98a585 100644
--- a/client/cmd/login_test.go
+++ b/client/cmd/login_test.go
@@ -21,7 +21,7 @@ func TestLogin(t *testing.T) {
 		"--config",
 		confPath,
 		"--log-file",
-		"console",
+		util.LogConsole,
 		"--setup-key",
 		strings.ToUpper("a2c8e62b-38f5-4553-b31e-dd66c696cebb"),
 		"--management-url",
diff --git a/client/iface/wgproxy/proxy_test.go b/client/iface/wgproxy/proxy_test.go
index 2165b8ab..6882f9ea 100644
--- a/client/iface/wgproxy/proxy_test.go
+++ b/client/iface/wgproxy/proxy_test.go
@@ -17,7 +17,7 @@ import (
 )
 
 func TestMain(m *testing.M) {
-	_ = util.InitLog("trace", "console")
+	_ = util.InitLog("trace", util.LogConsole)
 	code := m.Run()
 	os.Exit(code)
 }
diff --git a/client/internal/dns/file_repair_unix_test.go b/client/internal/dns/file_repair_unix_test.go
index e948557b..3aa0b859 100644
--- a/client/internal/dns/file_repair_unix_test.go
+++ b/client/internal/dns/file_repair_unix_test.go
@@ -14,7 +14,7 @@ import (
 )
 
 func TestMain(m *testing.M) {
-	_ = util.InitLog("debug", "console")
+	_ = util.InitLog("debug", util.LogConsole)
 	code := m.Run()
 	os.Exit(code)
 }
diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go
index f0213868..69586b47 100644
--- a/client/internal/engine_test.go
+++ b/client/internal/engine_test.go
@@ -196,7 +196,7 @@ func (m *MockWGIface) LastActivities() map[string]monotime.Time {
 }
 
 func TestMain(m *testing.M) {
-	_ = util.InitLog("debug", "console")
+	_ = util.InitLog("debug", util.LogConsole)
 	code := m.Run()
 	os.Exit(code)
 }
@@ -1270,6 +1270,82 @@ func Test_CheckFilesEqual(t *testing.T) {
 			},
 			expectedBool: false,
 		},
+		{
+			name: "Compared Slices with same files but different order should return true",
+			inputChecks1: []*mgmtProto.Checks{
+				{
+					Files: []string{
+						"testfile1",
+						"testfile2",
+					},
+				},
+				{
+					Files: []string{
+						"testfile4",
+						"testfile3",
+					},
+				},
+			},
+			inputChecks2: []*mgmtProto.Checks{
+				{
+					Files: []string{
+						"testfile3",
+						"testfile4",
+					},
+				},
+				{
+					Files: []string{
+						"testfile2",
+						"testfile1",
+					},
+				},
+			},
+			expectedBool: true,
+		},
+		{
+			name: "Compared Slices with same files but different order while first is equal should return true",
+			inputChecks1: []*mgmtProto.Checks{
+				{
+					Files: []string{
+						"testfile0",
+						"testfile1",
+					},
+				},
+				{
+					Files: []string{
+						"testfile0",
+						"testfile2",
+					},
+				},
+				{
+					Files: []string{
+						"testfile0",
+						"testfile3",
+					},
+				},
+			},
+			inputChecks2: []*mgmtProto.Checks{
+				{
+					Files: []string{
+						"testfile0",
+						"testfile1",
+					},
+				},
+				{
+					Files: []string{
+						"testfile0",
+						"testfile3",
+					},
+				},
+				{
+					Files: []string{
+						"testfile0",
+						"testfile2",
+					},
+				},
+			},
+			expectedBool: true,
+		},
 	}
 	for _, testCase := range testCases {
 		t.Run(testCase.name, func(t *testing.T) {
diff --git a/client/internal/peer/conn_test.go b/client/internal/peer/conn_test.go
index c5055e64..7cad4595 100644
--- a/client/internal/peer/conn_test.go
+++ b/client/internal/peer/conn_test.go
@@ -31,7 +31,7 @@ var connConf = ConnConfig{
 }
 
 func TestMain(m *testing.M) {
-	_ = util.InitLog("trace", "console")
+	_ = util.InitLog("trace", util.LogConsole)
 	code := m.Run()
 	os.Exit(code)
 }
diff --git a/client/server/server_test.go b/client/server/server_test.go
index 7c46aac5..11e4d389 100644
--- a/client/server/server_test.go
+++ b/client/server/server_test.go
@@ -32,6 +32,7 @@ import (
 	"github.com/netbirdio/netbird/management/server/types"
 	"github.com/netbirdio/netbird/signal/proto"
 	signalServer "github.com/netbirdio/netbird/signal/server"
+	"github.com/netbirdio/netbird/util"
 )
 
 var (
@@ -92,7 +93,7 @@ func TestConnectWithRetryRuns(t *testing.T) {
 func TestServer_Up(t *testing.T) {
 	ctx := internal.CtxInitState(context.Background())
 
-	s := New(ctx, t.TempDir()+"/config.json", "console")
+	s := New(ctx, t.TempDir()+"/config.json", util.LogConsole)
 
 	err := s.Start()
 	require.NoError(t, err)
@@ -130,7 +131,7 @@ func (m *mockSubscribeEventsServer) Context() context.Context {
 func TestServer_SubcribeEvents(t *testing.T) {
 	ctx := internal.CtxInitState(context.Background())
 
-	s := New(ctx, t.TempDir()+"/config.json", "console")
+	s := New(ctx, t.TempDir()+"/config.json", util.LogConsole)
 
 	err := s.Start()
 	require.NoError(t, err)
diff --git a/management/client/client_test.go b/management/client/client_test.go
index b59b7c98..5b2a8749 100644
--- a/management/client/client_test.go
+++ b/management/client/client_test.go
@@ -41,7 +41,7 @@ import (
 const ValidKey = "A2C8E62B-38F5-4553-B31E-DD66C696CEBB"
 
 func TestMain(m *testing.M) {
-	_ = util.InitLog("debug", "console")
+	_ = util.InitLog("debug", util.LogConsole)
 	code := m.Run()
 	os.Exit(code)
 }
diff --git a/management/server/scheduler_test.go b/management/server/scheduler_test.go
index fa279d4d..e3af551a 100644
--- a/management/server/scheduler_test.go
+++ b/management/server/scheduler_test.go
@@ -75,6 +75,38 @@ func TestScheduler_Cancel(t *testing.T) {
 	assert.NotNil(t, scheduler.jobs[jobID2])
 }
 
+func TestScheduler_CancelAll(t *testing.T) {
+	jobID1 := "test-scheduler-job-1"
+	jobID2 := "test-scheduler-job-2"
+	scheduler := NewDefaultScheduler()
+	tChan := make(chan struct{})
+	p := []string{jobID1, jobID2}
+	scheduletime := 2 * time.Millisecond
+	sleepTime := 4 * time.Millisecond
+	if runtime.GOOS == "windows" {
+		// sleep and ticker are slower on windows see https://github.com/golang/go/issues/44343
+		sleepTime = 20 * time.Millisecond
+	}
+
+	scheduler.Schedule(context.Background(), scheduletime, jobID1, func() (nextRunIn time.Duration, reschedule bool) {
+		tt := p[0]
+		<-tChan
+		t.Logf("job %s", tt)
+		return scheduletime, true
+	})
+	scheduler.Schedule(context.Background(), scheduletime, jobID2, func() (nextRunIn time.Duration, reschedule bool) {
+		return scheduletime, true
+	})
+
+	time.Sleep(sleepTime)
+	assert.Len(t, scheduler.jobs, 2)
+	scheduler.CancelAll(context.Background())
+	close(tChan)
+	p = []string{}
+	time.Sleep(sleepTime)
+	assert.Len(t, scheduler.jobs, 0)
+}
+
 func TestScheduler_Schedule(t *testing.T) {
 	jobID := "test-scheduler-job-1"
 	scheduler := NewDefaultScheduler()
diff --git a/relay/client/client_test.go b/relay/client/client_test.go
index c85ec9fd..2ce8d7e3 100644
--- a/relay/client/client_test.go
+++ b/relay/client/client_test.go
@@ -30,7 +30,7 @@ var (
 )
 
 func TestMain(m *testing.M) {
-	_ = util.InitLog("debug", "console")
+	_ = util.InitLog("debug", util.LogConsole)
 	code := m.Run()
 	os.Exit(code)
 }
diff --git a/relay/test/benchmark_test.go b/relay/test/benchmark_test.go
index 2e67ab80..afbb14b8 100644
--- a/relay/test/benchmark_test.go
+++ b/relay/test/benchmark_test.go
@@ -27,7 +27,7 @@ var (
 )
 
 func TestMain(m *testing.M) {
-	_ = util.InitLog("error", "console")
+	_ = util.InitLog("error", util.LogConsole)
 	code := m.Run()
 	os.Exit(code)
 }
diff --git a/relay/testec2/main.go b/relay/testec2/main.go
index 0c8099a5..6954d6a5 100644
--- a/relay/testec2/main.go
+++ b/relay/testec2/main.go
@@ -233,7 +233,7 @@ func TURNReaderMain() []testResult {
 func main() {
 	var mode string
 
-	_ = util.InitLog("debug", "console")
+	_ = util.InitLog("debug", util.LogConsole)
 	flag.StringVar(&mode, "mode", "sender", "sender or receiver mode")
 	flag.Parse()