package hostcollect import ( "sort" "strings" "github.com/itworx/pulse/internal/host" ) // loopbackInterface is excluded: its counters describe the host talking to itself and // only add noise to a network overview. const loopbackInterface = "lo" // parseNetDev reads /proc/net/dev. // // The format is two header lines followed by " name: rx... tx...". The name is // separated by a colon that is NOT always followed by a space — a sufficiently large // rx byte counter runs straight into it ("eth0:18446744073709551615 ...") — so the // line is split on the first colon rather than on whitespace. func parseNetDev(data []byte) []host.RawNetworkInterface { all := lines(data) interfaces := make([]host.RawNetworkInterface, 0, len(all)) for _, line := range all { name, rest, found := strings.Cut(line, ":") name = strings.TrimSpace(name) if !found || name == "" || strings.Contains(name, " ") { continue } fields := strings.Fields(rest) if len(fields) < 16 { continue } values := make([]uint64, 16) malformed := false for index := 0; index < 16; index++ { value, ok := parseUint(fields[index]) if !ok { malformed = true break } values[index] = value } if malformed { continue } interfaces = append(interfaces, host.RawNetworkInterface{ Name: truncate(name, 128), RxBytes: values[0], RxErrors: values[2], RxDrops: values[3], TxBytes: values[8], TxErrors: values[10], TxDrops: values[11], }) } return interfaces } func (c *Collector) network(warnings *warningSet) []host.RawNetworkInterface { data, err := readLimited(c.procPath("net", "dev"), maxFileBytes) if err != nil { warnings.add(WarningNetworkUnavailable) return nil } parsed := parseNetDev(data) interfaces := make([]host.RawNetworkInterface, 0, len(parsed)) for _, item := range parsed { if item.Name == loopbackInterface { continue } item.State = c.interfaceState(item.Name) interfaces = append(interfaces, item) } sort.Slice(interfaces, func(i, j int) bool { return interfaces[i].Name < interfaces[j].Name }) if len(interfaces) > c.options.HostLimits.MaxInterfaces { interfaces = interfaces[:c.options.HostLimits.MaxInterfaces] warnings.add(WarningNetworkTruncated) } if len(interfaces) == 0 { return nil } return interfaces } // interfaceState reads the operational state sysfs publishes ("up", "down", // "unknown"). A missing file is normal for virtual interfaces and simply yields no // state rather than a warning. func (c *Collector) interfaceState(name string) string { if strings.ContainsAny(name, "/\\") { return "" } state, err := readTrimmed(c.sysPath("class", "net", name, "operstate"), 64) if err != nil { return "" } return truncate(state, 32) }