diff --git a/client/internal/profilemanager/config.go b/client/internal/profilemanager/config.go index f509147bb..26ce1d394 100644 --- a/client/internal/profilemanager/config.go +++ b/client/internal/profilemanager/config.go @@ -100,6 +100,7 @@ type Config struct { WgPort int NetworkMonitor *bool IFaceBlackList []string + IFaceBlackListAppliedDefaults []string `json:",omitempty"` DisableIPv6Discovery bool RosenpassEnabled bool RosenpassPermissive bool @@ -359,10 +360,7 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) { updated = true } - if len(config.IFaceBlackList) == 0 { - log.Infof("filling in interface blacklist with defaults: [ %s ]", - strings.Join(DefaultInterfaceBlacklist, " ")) - config.IFaceBlackList = append(config.IFaceBlackList, DefaultInterfaceBlacklist...) + if changed := config.mergeDefaultIFaceBlacklist(); changed { updated = true } @@ -596,6 +594,37 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) { return updated, nil } +// mergeDefaultIFaceBlacklist ensures that new entries added to DefaultInterfaceBlacklist +// are merged into an existing IFaceBlackList on upgrade, while respecting entries that +// the user deliberately removed. It tracks which defaults have been offered via +// IFaceBlackListAppliedDefaults so removals are not undone. +func (config *Config) mergeDefaultIFaceBlacklist() (updated bool) { + if len(config.IFaceBlackList) == 0 { + log.Infof("filling in interface blacklist with defaults: [ %s ]", + strings.Join(DefaultInterfaceBlacklist, " ")) + config.IFaceBlackList = append(config.IFaceBlackList, DefaultInterfaceBlacklist...) + config.IFaceBlackListAppliedDefaults = append([]string{}, DefaultInterfaceBlacklist...) + return true + } + + // Find defaults not yet tracked in AppliedDefaults — these are genuinely new. + // Entries already in AppliedDefaults were either kept or deliberately removed by the user. + newDefaults := util.SliceDiff(DefaultInterfaceBlacklist, config.IFaceBlackListAppliedDefaults) + if len(newDefaults) == 0 { + return false + } + + // Only add entries not already present in the blacklist (avoid duplicates) + toAdd := util.SliceDiff(newDefaults, config.IFaceBlackList) + if len(toAdd) > 0 { + log.Infof("merging new default interface blacklist entries: [ %s ]", + strings.Join(toAdd, " ")) + config.IFaceBlackList = append(config.IFaceBlackList, toAdd...) + } + config.IFaceBlackListAppliedDefaults = append(config.IFaceBlackListAppliedDefaults, newDefaults...) + return true +} + // parseURL parses and validates a service URL func parseURL(serviceName, serviceURL string) (*url.URL, error) { parsedMgmtURL, err := url.ParseRequestURI(serviceURL) diff --git a/client/internal/profilemanager/config_test.go b/client/internal/profilemanager/config_test.go index ab13cf389..5f0c44f60 100644 --- a/client/internal/profilemanager/config_test.go +++ b/client/internal/profilemanager/config_test.go @@ -108,6 +108,87 @@ func TestExtraIFaceBlackList(t *testing.T) { assert.Contains(t, readConf.(*Config).IFaceBlackList, "eth1") } +func TestIFaceBlackListMigratesNewDefaults(t *testing.T) { + tempDir := t.TempDir() + configPath := filepath.Join(tempDir, "config.json") + + // Create a config that simulates an old install with a partial IFaceBlackList + // (missing the newer CNI entries like "cilium_", "cali", etc.) + config, err := UpdateOrCreateConfig(ConfigInput{ + ConfigPath: configPath, + }) + require.NoError(t, err) + + // Simulate an old config that predates AppliedDefaults tracking: + // it has only the original entries, no CNI prefixes, and no AppliedDefaults. + oldList := []string{iface.WgInterfaceDefault, "wt", "utun", "tun0", "zt", "ZeroTier", "wg", "ts", + "Tailscale", "tailscale", "docker", "veth", "br-", "lo"} + config.IFaceBlackList = oldList + config.IFaceBlackListAppliedDefaults = nil + err = WriteOutConfig(configPath, config) + require.NoError(t, err) + + // Re-read the config — apply() should merge in missing defaults + reloaded, err := GetConfig(configPath) + require.NoError(t, err) + + for _, entry := range DefaultInterfaceBlacklist { + assert.Contains(t, reloaded.IFaceBlackList, entry, + "IFaceBlackList should contain default entry %q after migration", entry) + } + + // Verify no duplicates were introduced + seen := make(map[string]bool) + for _, entry := range reloaded.IFaceBlackList { + assert.False(t, seen[entry], "duplicate entry %q in IFaceBlackList", entry) + seen[entry] = true + } + + // AppliedDefaults should now track all current defaults + for _, entry := range DefaultInterfaceBlacklist { + assert.Contains(t, reloaded.IFaceBlackListAppliedDefaults, entry, + "AppliedDefaults should track %q", entry) + } + + // Re-read again — should not change (idempotent) + reloaded2, err := GetConfig(configPath) + require.NoError(t, err) + assert.Equal(t, reloaded.IFaceBlackList, reloaded2.IFaceBlackList, + "IFaceBlackList should be stable on subsequent reads") +} + +func TestIFaceBlackListRespectsUserRemoval(t *testing.T) { + tempDir := t.TempDir() + configPath := filepath.Join(tempDir, "config.json") + + // Create a fresh config (all defaults applied) + config, err := UpdateOrCreateConfig(ConfigInput{ + ConfigPath: configPath, + }) + require.NoError(t, err) + require.Contains(t, config.IFaceBlackList, "cali") + + // User deliberately removes "cali" from their blacklist + filtered := make([]string, 0, len(config.IFaceBlackList)) + for _, entry := range config.IFaceBlackList { + if entry != "cali" { + filtered = append(filtered, entry) + } + } + config.IFaceBlackList = filtered + err = WriteOutConfig(configPath, config) + require.NoError(t, err) + + // Re-read — "cali" should NOT be re-added because it's in AppliedDefaults + reloaded, err := GetConfig(configPath) + require.NoError(t, err) + assert.NotContains(t, reloaded.IFaceBlackList, "cali", + "user-removed entry should not be re-added") + + // AppliedDefaults should still contain "cali" (it was offered) + assert.Contains(t, reloaded.IFaceBlackListAppliedDefaults, "cali") +} + func TestHiddenPreSharedKey(t *testing.T) { hidden := "**********" samplePreSharedKey := "mysecretpresharedkey"