mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-17 03:59:56 +00:00
* [client] Add autostart preference marker and MDM disableAutostart key Adds the autostartInitialized marker to the Wails UI preferences store so the one-time autostart default decision can persist per OS user, and a UI-only disableAutostart MDM policy key that suppresses the default and flows into GetConfigResponse.mDMManagedFields like disableAutoConnect. * [client] Enable launch-on-login by default on fresh GUI installs On the first interactive run the GUI persists the autostartInitialized marker before any enable attempt, then enables autostart only when the platform supports it, MDM policy does not disable it, the process was not relaunched by an installer/updater (--post-update), and the installer's fresh-install breadcrumb is present. Upgrading users have no breadcrumb, so an update can never write login items, and a user's disable in Settings is never overridden. * [release] Write fresh-install breadcrumb from installers Installers write a .fresh-install breadcrumb on fresh installs only and delete stale breadcrumbs on upgrade; none of them writes login items or registry Run keys. Windows NSIS detects upgrades via the uninstall registry entry or an existing installed executable; the macOS pkg via the previous pkgutil receipt; Linux deb/rpm via the standard postinstall arguments. Post-update GUI relaunches (macOS open, Linux ui-post-install.sh) pass --post-update so the first-run autostart default cannot fire on updates. * Revert installer breadcrumb changes The real Windows installer does uninstall-then-install and deletes $INSTDIR, so a breadcrumb written there cannot survive or discriminate a fresh install from an upgrade. Restore the three installer files to their main versions; no installer or updater writes an autostart entry. * Detect fresh install from NetBird footprint instead of installer breadcrumb Replace the installer-written breadcrumb discriminator with a GUI-side check. netbirdFootprintExists inspects the daemon config/state files (default.json, legacy config.json, state.json) under profilemanager's default config dir; combined with whether the UI preferences file already existed, this tells a genuinely fresh machine from an existing or upgrading user. Only the signed GUI, via Wails, ever enables launch-on-login, and a user's later manual disable is never overridden. The preferences store now exposes ExistedAtLoad and the --post-update flag is dropped. * Update tests for footprint-based autostart default Table tests for shouldEnableAutostartDefault now cover supported, mdmDisabled, and priorInstall guards plus precedence; breadcrumb and post-update cases are removed. Add a store test asserting ExistedAtLoad is false with no file and true after persisting and reopening.
269 lines
8.1 KiB
Go
269 lines
8.1 KiB
Go
// Package mdm reads MDM-managed configuration from platform-native sources
|
|
// (plist on macOS, registry on Windows, UserDefaults on iOS,
|
|
// RestrictionsManager on Android). The returned Policy is consumed by
|
|
// profilemanager.Config.apply() as the highest-priority override layer.
|
|
//
|
|
// An empty Policy (no source present, or source present with zero keys)
|
|
// means no MDM enforcement is active and the client behaves as if the
|
|
// feature did not exist.
|
|
package mdm
|
|
|
|
import (
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// Well-known policy keys. Names mirror the corresponding ConfigInput Go field
|
|
// names (lowerCamelCase) so the daemon can map a Policy key directly to a
|
|
// configuration field.
|
|
const (
|
|
KeyManagementURL = "managementURL"
|
|
KeyDisableUpdateSettings = "disableUpdateSettings"
|
|
KeyDisableProfiles = "disableProfiles"
|
|
KeyDisableNetworks = "disableNetworks"
|
|
// KeyDisableAdvancedView gates the advanced-view section in the
|
|
// upcoming UI revision. UI-only: NOT stored on Config, not
|
|
// applied by applyMDMPolicy, not rejectable via SetConfig. The
|
|
// daemon surfaces it through GetFeatures (tristate: present
|
|
// true / present false / absent) and the same key appears in
|
|
// GetConfigResponse.mDMManagedFields when set.
|
|
KeyDisableAdvancedView = "disableAdvancedView"
|
|
KeyDisableClientRoutes = "disableClientRoutes"
|
|
KeyDisableServerRoutes = "disableServerRoutes"
|
|
KeyBlockInbound = "blockInbound"
|
|
KeyDisableMetricsCollection = "disableMetricsCollection"
|
|
KeyAllowServerSSH = "allowServerSSH"
|
|
KeyDisableAutoConnect = "disableAutoConnect"
|
|
// KeyDisableAutostart suppresses the GUI's fresh-install
|
|
// launch-on-login default and marks the Settings toggle as
|
|
// MDM-managed. UI-only: NOT stored on Config and not applied by
|
|
// applyMDMPolicy; the GUI reads it directly and it appears in
|
|
// GetConfigResponse.mDMManagedFields when set.
|
|
KeyDisableAutostart = "disableAutostart"
|
|
KeyPreSharedKey = "preSharedKey"
|
|
KeyRosenpassEnabled = "rosenpassEnabled"
|
|
KeyRosenpassPermissive = "rosenpassPermissive"
|
|
KeyWireguardPort = "wireguardPort"
|
|
|
|
// Split tunnel is modeled as a single conceptual policy with two
|
|
// registry/plist values. KeySplitTunnelMode is the discriminator
|
|
// ("allow" or "disallow"); KeySplitTunnelApps is a comma-separated
|
|
// list of package names. The values are mutually exclusive by
|
|
// construction — only one mode can be set at a time.
|
|
KeySplitTunnelMode = "splitTunnelMode"
|
|
KeySplitTunnelApps = "splitTunnelApps"
|
|
|
|
// KeyLazyConnection forces the lazy-connection feature on or off, overriding
|
|
// the management feature flag. Read as a bool (native bool, or on/off,
|
|
// true/false, 1/0, yes/no); absent = defer to management.
|
|
KeyLazyConnection = "lazyConnection"
|
|
)
|
|
|
|
// Split-tunnel mode literals (KeySplitTunnelMode values).
|
|
const (
|
|
SplitTunnelModeAllow = "allow"
|
|
SplitTunnelModeDisallow = "disallow"
|
|
)
|
|
|
|
// SecretKeys lists keys whose values must be redacted in logs.
|
|
var SecretKeys = map[string]struct{}{
|
|
KeyPreSharedKey: {},
|
|
}
|
|
|
|
// boolStringLiterals enumerates the textual boolean encodings the
|
|
// platform loaders may produce (Windows REG_SZ "true", iOS / Android
|
|
// managed-config booleans-as-strings, etc.). Lookup keeps GetBool flat
|
|
// (no nested switch on the string case).
|
|
var boolStringLiterals = map[string]bool{
|
|
"true": true,
|
|
"1": true,
|
|
"yes": true,
|
|
"on": true,
|
|
"false": false,
|
|
"0": false,
|
|
"no": false,
|
|
"off": false,
|
|
}
|
|
|
|
// Policy holds MDM-managed settings read from the platform source. A nil or
|
|
// empty Policy means no enforcement is active.
|
|
type Policy struct {
|
|
values map[string]any
|
|
}
|
|
|
|
// NewPolicy constructs a Policy from a key→value map. Pass nil or an
|
|
// empty map to construct an empty (no-enforcement) Policy. The returned
|
|
// *Policy is always non-nil.
|
|
func NewPolicy(values map[string]any) *Policy {
|
|
if values == nil {
|
|
values = map[string]any{}
|
|
}
|
|
return &Policy{values: values}
|
|
}
|
|
|
|
// LoadPolicy reads the platform-native MDM configuration. Returns an
|
|
// empty (but non-nil) Policy when no source is present, the source is
|
|
// empty, or the platform is unsupported.
|
|
//
|
|
// Diagnostic logging differentiates the three states:
|
|
// - source absent / unsupported platform: trace log only
|
|
// - source present, zero keys: info "MDM enrolled (no managed keys)"
|
|
// - source present, N keys: info "MDM enrolled with N managed keys: [...]"
|
|
func LoadPolicy() *Policy {
|
|
values, err := loadPlatformPolicy()
|
|
if err != nil {
|
|
log.Tracef("MDM policy load: %v", err)
|
|
return &Policy{values: map[string]any{}}
|
|
}
|
|
if values == nil {
|
|
return &Policy{values: map[string]any{}}
|
|
}
|
|
if len(values) == 0 {
|
|
log.Info("MDM enrolled (no managed keys)")
|
|
} else {
|
|
log.Infof("MDM enrolled with %d managed key(s): %v", len(values), sortedKeys(values))
|
|
}
|
|
return &Policy{values: values}
|
|
}
|
|
|
|
// IsEmpty reports whether the Policy has no managed keys.
|
|
func (p *Policy) IsEmpty() bool {
|
|
return p == nil || len(p.values) == 0
|
|
}
|
|
|
|
// HasKey reports whether the given key is MDM-managed.
|
|
func (p *Policy) HasKey(key string) bool {
|
|
if p == nil {
|
|
return false
|
|
}
|
|
_, ok := p.values[key]
|
|
return ok
|
|
}
|
|
|
|
// ManagedKeys returns the sorted list of managed key names. Returns an empty
|
|
// slice (not nil) on an empty Policy.
|
|
func (p *Policy) ManagedKeys() []string {
|
|
if p == nil {
|
|
return []string{}
|
|
}
|
|
return sortedKeys(p.values)
|
|
}
|
|
|
|
// GetString returns the managed value for key coerced to string, and whether
|
|
// the key was set. A non-string value returns ("", false).
|
|
func (p *Policy) GetString(key string) (string, bool) {
|
|
if p == nil {
|
|
return "", false
|
|
}
|
|
v, ok := p.values[key]
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
s, ok := v.(string)
|
|
if !ok || s == "" {
|
|
return "", false
|
|
}
|
|
return s, true
|
|
}
|
|
|
|
// GetBool returns the managed value for key coerced to bool, and whether the
|
|
// key was set. Accepts native bool and string literals (true/false, 1/0,
|
|
// yes/no, on/off), case-insensitively and trimmed of surrounding whitespace.
|
|
func (p *Policy) GetBool(key string) (bool, bool) {
|
|
if p == nil {
|
|
return false, false
|
|
}
|
|
v, ok := p.values[key]
|
|
if !ok {
|
|
return false, false
|
|
}
|
|
switch t := v.(type) {
|
|
case bool:
|
|
return t, true
|
|
case string:
|
|
b, known := boolStringLiterals[strings.ToLower(strings.TrimSpace(t))]
|
|
return b, known
|
|
case int:
|
|
return t != 0, true
|
|
case int64:
|
|
return t != 0, true
|
|
}
|
|
return false, false
|
|
}
|
|
|
|
// GetInt returns the managed value for key as int64, and whether the key
|
|
// was set. Accepts native int / int64 (as produced by the Windows registry
|
|
// loader for REG_DWORD/REG_QWORD) and numeric strings (decimal).
|
|
func (p *Policy) GetInt(key string) (int64, bool) {
|
|
if p == nil {
|
|
return 0, false
|
|
}
|
|
v, ok := p.values[key]
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
switch t := v.(type) {
|
|
case int64:
|
|
return t, true
|
|
case int:
|
|
return int64(t), true
|
|
case int32:
|
|
return int64(t), true
|
|
case uint64:
|
|
return int64(t), true
|
|
case float64:
|
|
return int64(t), true
|
|
case string:
|
|
if n, err := strconv.ParseInt(t, 10, 64); err == nil {
|
|
return n, true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
// GetStringSlice returns the managed value for key as []string, and whether
|
|
// the key was set. Accepts []string, []any (of strings), and a single string
|
|
// (treated as a one-element list).
|
|
func (p *Policy) GetStringSlice(key string) ([]string, bool) {
|
|
if p == nil {
|
|
return nil, false
|
|
}
|
|
v, ok := p.values[key]
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
switch t := v.(type) {
|
|
case []string:
|
|
return append([]string(nil), t...), true
|
|
case []any:
|
|
out := make([]string, 0, len(t))
|
|
for _, item := range t {
|
|
s, ok := item.(string)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
out = append(out, s)
|
|
}
|
|
return out, true
|
|
case string:
|
|
return []string{t}, true
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
// sortedKeys returns the keys of m as a deterministic, lexicographically
|
|
// sorted slice. Used internally by Policy.ManagedKeys and LoadPolicy's
|
|
// diagnostic log line so callers see a stable key order across runs
|
|
// regardless of Go's randomised map iteration.
|
|
func sortedKeys(m map[string]any) []string {
|
|
out := make([]string, 0, len(m))
|
|
for k := range m {
|
|
out = append(out, k)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|