mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-18 04:29:54 +00:00
This also provide fixup for UI no aligning to changed config when coming from cli up with config flags.
228 lines
7.5 KiB
Go
228 lines
7.5 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"google.golang.org/grpc/codes"
|
|
gstatus "google.golang.org/grpc/status"
|
|
|
|
"github.com/netbirdio/netbird/client/internal"
|
|
"github.com/netbirdio/netbird/client/mdm"
|
|
"github.com/netbirdio/netbird/client/proto"
|
|
)
|
|
|
|
// loadMDMPolicy is the indirection used by server handlers to read the
|
|
// 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.
|
|
//
|
|
// Restart sequence:
|
|
// 1. Cancel the active engine context (terminates connectWithRetryRuns).
|
|
// 2. Wait briefly for that goroutine to exit (giveUpChan is closed on exit).
|
|
// 3. Re-resolve Config from disk + MDM policy (Config.apply re-runs
|
|
// applyMDMPolicy with the freshly loaded Policy).
|
|
// 4. Spawn a fresh connectWithRetryRuns with the new context and config.
|
|
// 5. Broadcast a SystemEvent so any GUI / CLI subscriber (SubscribeEvents
|
|
// RPC) can refresh its cached config view without polling.
|
|
//
|
|
// The callback runs in the ticker's own goroutine. Ticker has already
|
|
// logged the per-key diff before invoking this hook.
|
|
func (s *Server) onMDMPolicyChange(_, curr *mdm.Policy) {
|
|
log.Warn("MDM policy changed; restarting engine to apply new configuration")
|
|
|
|
s.mutex.Lock()
|
|
cancel := s.actCancel
|
|
giveUpChan := s.clientGiveUpChan
|
|
s.mutex.Unlock()
|
|
|
|
if cancel != nil {
|
|
cancel()
|
|
}
|
|
|
|
// Wait for previous connectWithRetryRuns to exit so we don't end up
|
|
// with two goroutines fighting over the same status recorder + engine.
|
|
if giveUpChan != nil {
|
|
select {
|
|
case <-giveUpChan:
|
|
case <-time.After(5 * time.Second):
|
|
log.Warn("MDM restart: timeout waiting for previous engine goroutine; proceeding anyway")
|
|
}
|
|
}
|
|
|
|
if err := s.restartEngineForMDM(); err != nil {
|
|
log.Errorf("MDM restart failed: %v", err)
|
|
return
|
|
}
|
|
|
|
// publishConfigChangedEvent has already fired inside
|
|
// restartEngineForMDM with source="mdm". Here we additionally emit an
|
|
// MDM-specific user-visible toast so the operator knows their IT
|
|
// policy was applied (UserMessage != "" triggers the GUI notifier).
|
|
_ = curr
|
|
s.statusRecorder.PublishEvent(
|
|
proto.SystemEvent_INFO,
|
|
proto.SystemEvent_SYSTEM,
|
|
"MDM policy applied",
|
|
"NetBird configuration was updated by your IT policy.",
|
|
map[string]string{"source": "mdm", "type": "policy_applied"},
|
|
)
|
|
}
|
|
|
|
// publishConfigChangedEvent broadcasts a SystemEvent informing any active
|
|
// SubscribeEvents subscriber (typically the GUI tray) that the daemon's
|
|
// effective Config has been replaced and any cached client-side view
|
|
// should be refreshed. Callers pass a stable `source` label so the GUI
|
|
// can distinguish a startup spawn from a user-triggered Up or an
|
|
// MDM-driven restart. Reusing the SYSTEM category keeps the proto enum
|
|
// stable; metadata.type="config_changed" routes to the GUI's refresh
|
|
// handler. UserMessage is left empty so the system tray does not toast
|
|
// for every internal restart; the MDM path emits a separate
|
|
// "policy_applied" event (with UserMessage) for that purpose.
|
|
func (s *Server) publishConfigChangedEvent(source string) {
|
|
if s.statusRecorder == nil {
|
|
return
|
|
}
|
|
var managed []string
|
|
if s.config != nil {
|
|
managed = s.config.Policy().ManagedKeys()
|
|
}
|
|
s.statusRecorder.PublishEvent(
|
|
proto.SystemEvent_INFO,
|
|
proto.SystemEvent_SYSTEM,
|
|
fmt.Sprintf("daemon config changed (source=%s)", source),
|
|
"",
|
|
map[string]string{
|
|
"source": source,
|
|
"type": "config_changed",
|
|
"managed_fields": strings.Join(managed, ","),
|
|
},
|
|
)
|
|
}
|
|
|
|
// restartEngineForMDM re-resolves the active profile config (re-running
|
|
// applyMDMPolicy via Config.apply) and re-spawns connectWithRetryRuns.
|
|
// Mirrors the tail of Server.Start so a runtime MDM change behaves
|
|
// identically to a fresh boot under the new policy.
|
|
func (s *Server) restartEngineForMDM() error {
|
|
activeProf, err := s.profileManager.GetActiveProfileState()
|
|
if err != nil {
|
|
return fmt.Errorf("get active profile state: %w", err)
|
|
}
|
|
config, existingConfig, err := s.getConfig(activeProf)
|
|
if err != nil {
|
|
return fmt.Errorf("get active profile config: %w", err)
|
|
}
|
|
|
|
s.mutex.Lock()
|
|
defer s.mutex.Unlock()
|
|
|
|
s.config = config
|
|
s.statusRecorder.UpdateManagementAddress(config.ManagementURL.String())
|
|
s.statusRecorder.UpdateRosenpass(config.RosenpassEnabled, config.RosenpassPermissive)
|
|
s.statusRecorder.UpdateLazyConnection(config.LazyConnectionEnabled)
|
|
|
|
state := internal.CtxGetState(s.rootCtx)
|
|
if config.DisableAutoConnect {
|
|
log.Info("MDM restart: DisableAutoConnect=true; staying idle")
|
|
state.Set(internal.StatusIdle)
|
|
s.actCancel = nil
|
|
return nil
|
|
}
|
|
if !existingConfig {
|
|
log.Warn("MDM restart: config absent; not reconnecting")
|
|
state.Set(internal.StatusNeedsLogin)
|
|
s.actCancel = nil
|
|
return nil
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(s.rootCtx)
|
|
s.actCancel = cancel
|
|
s.clientRunning = true
|
|
s.clientRunningChan = make(chan struct{})
|
|
s.clientGiveUpChan = make(chan struct{})
|
|
log.Info("MDM restart: spawning connectWithRetryRuns with re-resolved config")
|
|
go s.connectWithRetryRuns(ctx, config, s.statusRecorder, s.clientRunningChan, s.clientGiveUpChan)
|
|
s.publishConfigChangedEvent("mdm")
|
|
return nil
|
|
}
|
|
|
|
// 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.
|
|
func requestedMDMManagedKeys(msg *proto.SetConfigRequest) []string {
|
|
if msg == nil {
|
|
return nil
|
|
}
|
|
var keys []string
|
|
if msg.ManagementUrl != "" {
|
|
keys = append(keys, mdm.KeyManagementURL)
|
|
}
|
|
if msg.OptionalPreSharedKey != nil {
|
|
keys = append(keys, mdm.KeyPreSharedKey)
|
|
}
|
|
if msg.RosenpassEnabled != nil {
|
|
keys = append(keys, mdm.KeyRosenpassEnabled)
|
|
}
|
|
if msg.RosenpassPermissive != nil {
|
|
keys = append(keys, mdm.KeyRosenpassPermissive)
|
|
}
|
|
if msg.DisableAutoConnect != nil {
|
|
keys = append(keys, mdm.KeyDisableAutoConnect)
|
|
}
|
|
if msg.ServerSSHAllowed != nil {
|
|
keys = append(keys, mdm.KeyAllowServerSSH)
|
|
}
|
|
if msg.DisableClientRoutes != nil {
|
|
keys = append(keys, mdm.KeyDisableClientRoutes)
|
|
}
|
|
if msg.DisableServerRoutes != nil {
|
|
keys = append(keys, mdm.KeyDisableServerRoutes)
|
|
}
|
|
if msg.BlockInbound != nil {
|
|
keys = append(keys, mdm.KeyBlockInbound)
|
|
}
|
|
if msg.WireguardPort != nil {
|
|
keys = append(keys, mdm.KeyWireguardPort)
|
|
}
|
|
return keys
|
|
}
|
|
|
|
// rejectMDMManagedFieldConflicts returns a FailedPrecondition gRPC error
|
|
// with an MDMManagedFieldsViolation detail when any of the requested keys
|
|
// is MDM-enforced, and nil otherwise. The whole request is rejected on
|
|
// any conflict; non-conflicting fields in the same request are not
|
|
// applied either (no partial apply).
|
|
func rejectMDMManagedFieldConflicts(policy *mdm.Policy, requested []string) error {
|
|
if policy.IsEmpty() || len(requested) == 0 {
|
|
return nil
|
|
}
|
|
var conflicts []string
|
|
for _, k := range requested {
|
|
if policy.HasKey(k) {
|
|
conflicts = append(conflicts, k)
|
|
}
|
|
}
|
|
if len(conflicts) == 0 {
|
|
return nil
|
|
}
|
|
log.Warnf("MDM rejected request: tried to modify %d managed key(s): %v",
|
|
len(conflicts), conflicts)
|
|
st := gstatus.New(
|
|
codes.FailedPrecondition,
|
|
fmt.Sprintf("fields managed by MDM cannot be modified: %v", conflicts),
|
|
)
|
|
detailed, err := st.WithDetails(&proto.MDMManagedFieldsViolation{Fields: conflicts})
|
|
if err != nil {
|
|
// Detail attachment is best-effort; fall back to the plain status
|
|
// so the caller still gets a usable FailedPrecondition.
|
|
return st.Err()
|
|
}
|
|
return detailed.Err()
|
|
}
|