rmm2/agent/collector/network.go
cynfo3000 6120d0cffa Initial commit: RMM Agent + Backend
- Go Agent (FreeBSD/amd64) fuer OPNsense
- Go Backend (Linux/amd64) mit REST API + SQLite
- Collector: Hardware, CPU, Memory, Disks, Network, Services,
  WireGuard, DHCP (KEA/ISC/dnsmasq), Routes, Gateways,
  Certificates, Plugins, Updates
- Persistente Agent-ID
- TLS + API-Key Auth
- WebSocket-Infrastruktur (WIP): bidirektionaler Command-Kanal,
  TCP-Tunnel fuer Remote-Zugriff auf Firewall-WebUI
- Lastenheft und README
2026-02-28 07:38:14 +01:00

179 lines
4.4 KiB
Go

package collector
import (
"os/exec"
"strconv"
"strings"
)
type NetworkInterface struct {
Name string `json:"name"`
Role string `json:"role"`
IP string `json:"ip"`
MAC string `json:"mac"`
Status string `json:"status"`
RxBytes int64 `json:"rx_bytes"`
TxBytes int64 `json:"tx_bytes"`
}
func CollectNetwork() []NetworkInterface {
ifaces := parseIfconfig()
addTrafficStats(ifaces)
return ifaces
}
// parseIfconfig parst ifconfig -a Output
func parseIfconfig() []NetworkInterface {
out, err := exec.Command("ifconfig", "-a").Output()
if err != nil {
return nil
}
var ifaces []NetworkInterface
var current *NetworkInterface
for _, line := range strings.Split(string(out), "\n") {
// Neues Interface: "igb0: flags=..."
if len(line) > 0 && line[0] != '\t' && line[0] != ' ' && strings.Contains(line, ": flags=") {
if current != nil {
ifaces = append(ifaces, *current)
}
name := strings.Split(line, ":")[0]
// Loopback und virtuelle Interfaces ueberspringen
if name == "lo0" || strings.HasPrefix(name, "pflog") || strings.HasPrefix(name, "pfsync") || strings.HasPrefix(name, "enc") {
current = nil
continue
}
status := "down"
if strings.Contains(line, "UP") {
status = "up"
}
current = &NetworkInterface{
Name: name,
Status: status,
Role: guessRole(name),
}
continue
}
if current == nil {
continue
}
trimmed := strings.TrimSpace(line)
// IPv4 Adresse
if strings.HasPrefix(trimmed, "inet ") && !strings.HasPrefix(trimmed, "inet6") {
fields := strings.Fields(trimmed)
if len(fields) >= 2 {
current.IP = fields[1]
}
}
// MAC Adresse
if strings.HasPrefix(trimmed, "ether ") {
fields := strings.Fields(trimmed)
if len(fields) >= 2 {
current.MAC = fields[1]
}
}
}
if current != nil {
ifaces = append(ifaces, *current)
}
return ifaces
}
// addTrafficStats fuegt RX/TX Bytes via netstat -ib hinzu
func addTrafficStats(ifaces []NetworkInterface) {
out, err := exec.Command("/usr/bin/netstat", "-ib").Output()
if err != nil {
return
}
// Nur <Link#> Zeilen haben die totalen Byte-Zaehler
// Format variiert, aber die letzten 4 Felder sind immer: Opkts Oerrs Obytes Coll
// und davor: Ipkts Ierrs Idrop Ibytes
lines := strings.Split(string(out), "\n")
for _, line := range lines[1:] {
if !strings.Contains(line, "<Link#") {
continue
}
fields := strings.Fields(line)
if len(fields) < 11 {
continue
}
name := fields[0]
// Von hinten zaehlen: Coll=last, Obytes=last-1, Oerrs=last-2, Opkts=last-3
// Ibytes=last-4 von Opkts aus: Ibytes ist 4 vor Opkts
n := len(fields)
obytes, err1 := strconv.ParseInt(fields[n-2], 10, 64)
// Ibytes: 7 von hinten (Coll, Obytes, Oerrs, Opkts, Ibytes, Idrop, Ierrs)
ibytes, err2 := strconv.ParseInt(fields[n-5], 10, 64)
if err1 != nil || err2 != nil {
continue
}
for i := range ifaces {
if ifaces[i].Name == name {
ifaces[i].RxBytes = ibytes
ifaces[i].TxBytes = obytes
}
}
}
}
// guessRole versucht die Rolle anhand des Interface-Namens zu erraten
func guessRole(name string) string {
// OPNsense benutzt typischerweise config.xml fuer Zuordnungen
// Hier: Heuristik basierend auf gaengigen Konventionen
lower := strings.ToLower(name)
// Versuche OPNsense-Config zu lesen
role := getOPNsenseIfRole(name)
if role != "" {
return role
}
// Fallback-Heuristik
if strings.Contains(lower, "wan") {
return "WAN"
}
if strings.Contains(lower, "lan") {
return "LAN"
}
if strings.HasPrefix(lower, "igb0") || strings.HasPrefix(lower, "em0") || strings.HasPrefix(lower, "ix0") {
return "WAN"
}
if strings.HasPrefix(lower, "igb1") || strings.HasPrefix(lower, "em1") || strings.HasPrefix(lower, "ix1") {
return "LAN"
}
return "OPT"
}
// getOPNsenseIfRole liest die Interface-Rolle aus der OPNsense config.xml
func getOPNsenseIfRole(ifName string) string {
// config.xml Struktur: <interfaces><wan><if>vtnet1</if>...
// Wir suchen nach <if>IFNAME</if> und schauen welcher Block (wan/lan/opt) es enthaelt
out, err := exec.Command("grep", "-B5", "<if>"+ifName+"</if>", "/conf/config.xml").Output()
if err != nil {
return ""
}
content := string(out)
// Suche nach dem oeffnenden Tag des Parent-Elements
for _, role := range []string{"wan", "lan", "opt1", "opt2", "opt3", "opt4", "opt5", "opt6"} {
if strings.Contains(content, "<"+role+">") {
return strings.ToUpper(role)
}
}
return ""
}