diff --git a/agent/network.go b/agent/network.go index d8a7b852..7aa0289e 100644 --- a/agent/network.go +++ b/agent/network.go @@ -3,6 +3,7 @@ package agent import ( "fmt" "log/slog" + "path" "strings" "time" @@ -13,6 +14,69 @@ import ( var netInterfaceDeltaTracker = deltatracker.NewDeltaTracker[string, uint64]() +// NicConfig controls inclusion/exclusion of network interfaces via the NICS env var +// +// Behavior mirrors SensorConfig's matching logic: +// - Leading '-' means blacklist mode; otherwise whitelist mode +// - Supports '*' wildcards using path.Match +// - In whitelist mode with an empty list, no NICs are selected +// - In blacklist mode with an empty list, all NICs are selected +type NicConfig struct { + nics map[string]struct{} + isBlacklist bool + hasWildcards bool +} + +func newNicConfig(nicsEnvVal string) *NicConfig { + cfg := &NicConfig{ + nics: make(map[string]struct{}), + } + if strings.HasPrefix(nicsEnvVal, "-") { + cfg.isBlacklist = true + nicsEnvVal = nicsEnvVal[1:] + } + for nic := range strings.SplitSeq(nicsEnvVal, ",") { + nic = strings.TrimSpace(nic) + if nic != "" { + cfg.nics[nic] = struct{}{} + if strings.Contains(nic, "*") { + cfg.hasWildcards = true + } + } + } + return cfg +} + +// isValidNic determines if a NIC should be included based on NicConfig rules +func isValidNic(nicName string, cfg *NicConfig) bool { + // Empty list behavior differs by mode: blacklist: allow all; whitelist: allow none + if len(cfg.nics) == 0 { + return cfg.isBlacklist + } + + // Exact match: return true if whitelist, false if blacklist + if _, exactMatch := cfg.nics[nicName]; exactMatch { + return !cfg.isBlacklist + } + + // If no wildcards, return true if blacklist, false if whitelist + if !cfg.hasWildcards { + return cfg.isBlacklist + } + + // Check for wildcard patterns + for pattern := range cfg.nics { + if !strings.Contains(pattern, "*") { + continue + } + if match, _ := path.Match(pattern, nicName); match { + return !cfg.isBlacklist + } + } + + return cfg.isBlacklist +} + func (a *Agent) updateNetworkStats(systemStats *system.Stats) { // network stats if len(a.netInterfaces) == 0 { @@ -89,14 +153,11 @@ func (a *Agent) initializeNetIoStats() { // reset valid network interfaces a.netInterfaces = make(map[string]struct{}, 0) - // map of network interface names passed in via NICS env var - var nicsMap map[string]struct{} - nics, nicsEnvExists := GetEnv("NICS") + // parse NICS env var for whitelist / blacklist + nicsEnvVal, nicsEnvExists := GetEnv("NICS") + var nicCfg *NicConfig if nicsEnvExists { - nicsMap = make(map[string]struct{}, 0) - for nic := range strings.SplitSeq(nics, ",") { - nicsMap[nic] = struct{}{} - } + nicCfg = newNicConfig(nicsEnvVal) } // reset network I/O stats @@ -107,17 +168,11 @@ func (a *Agent) initializeNetIoStats() { if netIO, err := psutilNet.IOCounters(true); err == nil { a.netIoStats.Time = time.Now() for _, v := range netIO { - switch { - // skip if nics exists and the interface is not in the list - case nicsEnvExists: - if _, nameInNics := nicsMap[v.Name]; !nameInNics { - continue - } - // otherwise run the interface name through the skipNetworkInterface function - default: - if a.skipNetworkInterface(v) { - continue - } + if nicsEnvExists && !isValidNic(v.Name, nicCfg) { + continue + } + if a.skipNetworkInterface(v) { + continue } slog.Info("Detected network interface", "name", v.Name, "sent", v.BytesSent, "recv", v.BytesRecv) a.netIoStats.BytesSent += v.BytesSent diff --git a/agent/network_test.go b/agent/network_test.go new file mode 100644 index 00000000..beb56fbe --- /dev/null +++ b/agent/network_test.go @@ -0,0 +1,259 @@ +//go:build testing + +package agent + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIsValidNic(t *testing.T) { + tests := []struct { + name string + nicName string + config *NicConfig + expectedValid bool + }{ + { + name: "Whitelist - NIC in list", + nicName: "eth0", + config: &NicConfig{ + nics: map[string]struct{}{"eth0": {}}, + isBlacklist: false, + }, + expectedValid: true, + }, + { + name: "Whitelist - NIC not in list", + nicName: "wlan0", + config: &NicConfig{ + nics: map[string]struct{}{"eth0": {}}, + isBlacklist: false, + }, + expectedValid: false, + }, + { + name: "Blacklist - NIC in list", + nicName: "eth0", + config: &NicConfig{ + nics: map[string]struct{}{"eth0": {}}, + isBlacklist: true, + }, + expectedValid: false, + }, + { + name: "Blacklist - NIC not in list", + nicName: "wlan0", + config: &NicConfig{ + nics: map[string]struct{}{"eth0": {}}, + isBlacklist: true, + }, + expectedValid: true, + }, + { + name: "Whitelist with wildcard - matching pattern", + nicName: "eth1", + config: &NicConfig{ + nics: map[string]struct{}{"eth*": {}}, + isBlacklist: false, + hasWildcards: true, + }, + expectedValid: true, + }, + { + name: "Whitelist with wildcard - non-matching pattern", + nicName: "wlan0", + config: &NicConfig{ + nics: map[string]struct{}{"eth*": {}}, + isBlacklist: false, + hasWildcards: true, + }, + expectedValid: false, + }, + { + name: "Blacklist with wildcard - matching pattern", + nicName: "eth1", + config: &NicConfig{ + nics: map[string]struct{}{"eth*": {}}, + isBlacklist: true, + hasWildcards: true, + }, + expectedValid: false, + }, + { + name: "Blacklist with wildcard - non-matching pattern", + nicName: "wlan0", + config: &NicConfig{ + nics: map[string]struct{}{"eth*": {}}, + isBlacklist: true, + hasWildcards: true, + }, + expectedValid: true, + }, + { + name: "Empty whitelist config - no NICs allowed", + nicName: "eth0", + config: &NicConfig{ + nics: map[string]struct{}{}, + isBlacklist: false, + }, + expectedValid: false, + }, + { + name: "Empty blacklist config - all NICs allowed", + nicName: "eth0", + config: &NicConfig{ + nics: map[string]struct{}{}, + isBlacklist: true, + }, + expectedValid: true, + }, + { + name: "Multiple patterns - exact match", + nicName: "eth0", + config: &NicConfig{ + nics: map[string]struct{}{"eth0": {}, "wlan*": {}}, + isBlacklist: false, + }, + expectedValid: true, + }, + { + name: "Multiple patterns - wildcard match", + nicName: "wlan1", + config: &NicConfig{ + nics: map[string]struct{}{"eth0": {}, "wlan*": {}}, + isBlacklist: false, + hasWildcards: true, + }, + expectedValid: true, + }, + { + name: "Multiple patterns - no match", + nicName: "bond0", + config: &NicConfig{ + nics: map[string]struct{}{"eth0": {}, "wlan*": {}}, + isBlacklist: false, + hasWildcards: true, + }, + expectedValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isValidNic(tt.nicName, tt.config) + assert.Equal(t, tt.expectedValid, result) + }) + } +} + +func TestNewNicConfig(t *testing.T) { + tests := []struct { + name string + nicsEnvVal string + expectedCfg *NicConfig + }{ + { + name: "Empty string", + nicsEnvVal: "", + expectedCfg: &NicConfig{ + nics: map[string]struct{}{}, + isBlacklist: false, + hasWildcards: false, + }, + }, + { + name: "Single NIC whitelist", + nicsEnvVal: "eth0", + expectedCfg: &NicConfig{ + nics: map[string]struct{}{"eth0": {}}, + isBlacklist: false, + hasWildcards: false, + }, + }, + { + name: "Multiple NICs whitelist", + nicsEnvVal: "eth0,wlan0", + expectedCfg: &NicConfig{ + nics: map[string]struct{}{"eth0": {}, "wlan0": {}}, + isBlacklist: false, + hasWildcards: false, + }, + }, + { + name: "Blacklist mode", + nicsEnvVal: "-eth0,wlan0", + expectedCfg: &NicConfig{ + nics: map[string]struct{}{"eth0": {}, "wlan0": {}}, + isBlacklist: true, + hasWildcards: false, + }, + }, + { + name: "With wildcards", + nicsEnvVal: "eth*,wlan0", + expectedCfg: &NicConfig{ + nics: map[string]struct{}{"eth*": {}, "wlan0": {}}, + isBlacklist: false, + hasWildcards: true, + }, + }, + { + name: "Blacklist with wildcards", + nicsEnvVal: "-eth*,wlan0", + expectedCfg: &NicConfig{ + nics: map[string]struct{}{"eth*": {}, "wlan0": {}}, + isBlacklist: true, + hasWildcards: true, + }, + }, + { + name: "With whitespace", + nicsEnvVal: "eth0, wlan0 , eth1", + expectedCfg: &NicConfig{ + nics: map[string]struct{}{"eth0": {}, "wlan0": {}, "eth1": {}}, + isBlacklist: false, + hasWildcards: false, + }, + }, + { + name: "Only wildcards", + nicsEnvVal: "eth*,wlan*", + expectedCfg: &NicConfig{ + nics: map[string]struct{}{"eth*": {}, "wlan*": {}}, + isBlacklist: false, + hasWildcards: true, + }, + }, + { + name: "Leading dash only", + nicsEnvVal: "-", + expectedCfg: &NicConfig{ + nics: map[string]struct{}{}, + isBlacklist: true, + hasWildcards: false, + }, + }, + { + name: "Mixed exact and wildcard", + nicsEnvVal: "eth0,br-*", + expectedCfg: &NicConfig{ + nics: map[string]struct{}{"eth0": {}, "br-*": {}}, + isBlacklist: false, + hasWildcards: true, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := newNicConfig(tt.nicsEnvVal) + require.NotNil(t, cfg) + assert.Equal(t, tt.expectedCfg.isBlacklist, cfg.isBlacklist) + assert.Equal(t, tt.expectedCfg.hasWildcards, cfg.hasWildcards) + assert.Equal(t, tt.expectedCfg.nics, cfg.nics) + }) + } +} diff --git a/supplemental/CHANGELOG.md b/supplemental/CHANGELOG.md index 65abadd3..98c8a9ef 100644 --- a/supplemental/CHANGELOG.md +++ b/supplemental/CHANGELOG.md @@ -2,6 +2,8 @@ - Adjust calculation of cached memory (fixes #1187, #1196) +- Add pattern matching and blacklist functionality to `NICS` env var. (#1190) + - Update Intel GPU collector to parse plain text (`-l`) instead of JSON output (#1150) ## 0.12.10