mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-23 17:01:29 +02:00
Adds MDM 1m diff checker & reloader
This commit is contained in:
118
client/mdm/ticker.go
Normal file
118
client/mdm/ticker.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package mdm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// DefaultReloadInterval is the cadence at which the desktop daemon re-reads
|
||||
// the OS-native MDM policy. Picked to balance responsiveness against
|
||||
// registry/plist I/O overhead. Mobile builds use OS-side notifications
|
||||
// instead and bypass this ticker entirely.
|
||||
const DefaultReloadInterval = 1 * time.Minute
|
||||
|
||||
// Ticker periodically re-reads the OS-native MDM policy via LoadPolicy and
|
||||
// invokes onChange whenever the observed Policy diverges from the last
|
||||
// observation (added / removed / changed keys). Launch with Run from a
|
||||
// goroutine; cancel the supplied context to stop.
|
||||
type Ticker struct {
|
||||
interval time.Duration
|
||||
onChange func(prev, curr *Policy)
|
||||
prev *Policy
|
||||
}
|
||||
|
||||
// NewTicker constructs a Ticker that calls LoadPolicy every `interval` and
|
||||
// invokes `onChange` on any diff. Pass a zero interval to fall back to
|
||||
// DefaultReloadInterval. onChange may be nil for a log-only ticker.
|
||||
func NewTicker(interval time.Duration, onChange func(prev, curr *Policy)) *Ticker {
|
||||
if interval <= 0 {
|
||||
interval = DefaultReloadInterval
|
||||
}
|
||||
return &Ticker{
|
||||
interval: interval,
|
||||
onChange: onChange,
|
||||
prev: LoadPolicy(),
|
||||
}
|
||||
}
|
||||
|
||||
// Run blocks until ctx is cancelled, polling the OS-native policy store at
|
||||
// the configured cadence and emitting log lines + onChange callback on
|
||||
// every observed diff.
|
||||
func (t *Ticker) Run(ctx context.Context) {
|
||||
tk := time.NewTicker(t.interval)
|
||||
defer tk.Stop()
|
||||
log.Infof("MDM policy reload ticker started (interval=%s)", t.interval)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Info("MDM policy reload ticker stopped")
|
||||
return
|
||||
case <-tk.C:
|
||||
curr := LoadPolicy()
|
||||
if PoliciesEqual(t.prev, curr) {
|
||||
continue
|
||||
}
|
||||
added, removed, changed := diffPolicies(t.prev, curr)
|
||||
log.Infof("MDM policy changed: added=%v removed=%v changed=%v",
|
||||
added, removed, changed)
|
||||
prev := t.prev
|
||||
t.prev = curr
|
||||
if t.onChange != nil {
|
||||
t.onChange(prev, curr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PoliciesEqual reports whether two Policy instances carry the same managed
|
||||
// key set with identical values. Nil and empty policies compare equal.
|
||||
func PoliciesEqual(a, b *Policy) bool {
|
||||
if a.IsEmpty() && b.IsEmpty() {
|
||||
return true
|
||||
}
|
||||
if a == nil || b == nil {
|
||||
return false
|
||||
}
|
||||
return reflect.DeepEqual(a.values, b.values)
|
||||
}
|
||||
|
||||
// diffPolicies returns the keys added in curr, removed from prev, and whose
|
||||
// value changed. Returned slices are sorted for stable log output.
|
||||
func diffPolicies(prev, curr *Policy) (added, removed, changed []string) {
|
||||
prevKeys := mapOf(prev)
|
||||
currKeys := mapOf(curr)
|
||||
for k := range currKeys {
|
||||
if _, ok := prevKeys[k]; !ok {
|
||||
added = append(added, k)
|
||||
} else if !reflect.DeepEqual(prevKeys[k], currKeys[k]) {
|
||||
changed = append(changed, k)
|
||||
}
|
||||
}
|
||||
for k := range prevKeys {
|
||||
if _, ok := currKeys[k]; !ok {
|
||||
removed = append(removed, k)
|
||||
}
|
||||
}
|
||||
sort.Strings(added)
|
||||
sort.Strings(removed)
|
||||
sort.Strings(changed)
|
||||
return added, removed, changed
|
||||
}
|
||||
|
||||
// mapOf returns a (possibly empty, never nil) copy of the underlying values
|
||||
// map of a Policy so callers outside this package can compare across the
|
||||
// public Policy boundary without touching unexported state.
|
||||
func mapOf(p *Policy) map[string]any {
|
||||
if p == nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
out := make(map[string]any, len(p.values))
|
||||
for k, v := range p.values {
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -15,6 +15,29 @@ import (
|
||||
// active MDM policy. Tests override this to inject a fake policy.
|
||||
var loadMDMPolicy = mdm.LoadPolicy
|
||||
|
||||
// onMDMPolicyChange is invoked by the MDM reload ticker every time the
|
||||
// OS-native managed-config store reports a diff vs the last observation.
|
||||
// The handler cancels the active engine context (if any) so the
|
||||
// connectWithRetryRuns goroutine re-resolves the profile config —
|
||||
// re-running profilemanager.Config.apply() picks up the freshly-loaded
|
||||
// policy as the highest-priority override layer, and the engine restarts
|
||||
// with the new values.
|
||||
//
|
||||
// The callback runs in the ticker's own goroutine; it must not block on
|
||||
// long-running work nor take s.mutex for longer than the cancel call.
|
||||
// Ticker has already logged the per-key diff before invoking this hook.
|
||||
func (s *Server) onMDMPolicyChange(_, _ *mdm.Policy) {
|
||||
log.Warn("MDM policy changed; cancelling engine context so connectWithRetryRuns re-resolves config")
|
||||
|
||||
s.mutex.Lock()
|
||||
cancel := s.actCancel
|
||||
s.mutex.Unlock()
|
||||
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
// requestedMDMManagedKeys returns the names of MDM-managed keys whose
|
||||
// corresponding field is set in the SetConfigRequest. Only keys with an
|
||||
// MDM mapping are considered; other fields are ignored.
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/netbirdio/netbird/client/internal/expose"
|
||||
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
||||
sleephandler "github.com/netbirdio/netbird/client/internal/sleep/handler"
|
||||
"github.com/netbirdio/netbird/client/mdm"
|
||||
"github.com/netbirdio/netbird/client/system"
|
||||
mgm "github.com/netbirdio/netbird/shared/management/client"
|
||||
"github.com/netbirdio/netbird/shared/management/domain"
|
||||
@@ -98,6 +99,11 @@ type Server struct {
|
||||
|
||||
sleepHandler *sleephandler.SleepHandler
|
||||
|
||||
// mdmTicker periodically re-reads the OS-native MDM policy and triggers
|
||||
// an engine restart when the policy changes. Launched once by Start;
|
||||
// stopped by the rootCtx cancellation.
|
||||
mdmTicker *mdm.Ticker
|
||||
|
||||
updateManager *updater.Manager
|
||||
|
||||
jwtCache *jwtCache
|
||||
@@ -155,6 +161,17 @@ func (s *Server) Start() error {
|
||||
s.updateManager.CheckUpdateSuccess(s.rootCtx)
|
||||
}
|
||||
|
||||
// MDM policy reload ticker: every minute the desktop daemon re-reads
|
||||
// the OS-native managed-config store and, on diff vs the previous
|
||||
// observation, cancels the active engine context so connectWithRetry-
|
||||
// Runs re-resolves Config (re-running profilemanager.Config.apply which
|
||||
// applies the freshly-read MDM policy as the last layer) and brings
|
||||
// the engine back with the new values.
|
||||
if s.mdmTicker == nil {
|
||||
s.mdmTicker = mdm.NewTicker(mdm.DefaultReloadInterval, s.onMDMPolicyChange)
|
||||
go s.mdmTicker.Run(s.rootCtx)
|
||||
}
|
||||
|
||||
// if current state contains any error, return it
|
||||
// in all other cases we can continue execution only if status is idle and up command was
|
||||
// not in the progress or already successfully established connection.
|
||||
|
||||
Reference in New Issue
Block a user