rmm2/agent/collector/dhcp.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

208 lines
5.0 KiB
Go

package collector
import (
"encoding/json"
"os/exec"
"strconv"
"strings"
"time"
)
type DHCPLease struct {
IP string `json:"ip"`
MAC string `json:"mac"`
Hostname string `json:"hostname"`
Start string `json:"start,omitempty"`
End string `json:"end,omitempty"`
Status string `json:"status"` // "active", "free", "backup"
Pool string `json:"pool,omitempty"`
}
type DHCPInfo struct {
Server string `json:"server"` // "kea", "isc-dhcpd", "dnsmasq", "none"
Leases []DHCPLease `json:"leases"`
}
func CollectDHCP() DHCPInfo {
// 1. Versuch: KEA via configctl (OPNsense 24.7+)
if info := tryKEA(); info != nil {
return *info
}
// 2. Versuch: ISC dhcpd Lease-Datei
if info := tryISCDhcpd(); info != nil {
return *info
}
// 3. Versuch: dnsmasq Leases
if info := tryDnsmasqLeases(); info != nil {
return *info
}
return DHCPInfo{Server: "none"}
}
// tryKEA versucht KEA-Leases ueber configctl abzufragen
func tryKEA() *DHCPInfo {
out, err := exec.Command("/usr/local/sbin/configctl", "kea", "list", "leases4").Output()
if err != nil {
return nil
}
// JSON-Antwort: {"records": [{"address": "...", "hwaddr": "...", "hostname": "...", "state": "0", "expire": "1772261642", ...}]}
var result struct {
Records []struct {
Address string `json:"address"`
HWAddr string `json:"hwaddr"`
HWAddress string `json:"hw_address"` // Fallback fuer aeltere KEA-Versionen
Hostname string `json:"hostname"`
State string `json:"state"`
SubnetID string `json:"subnet_id"`
Expire string `json:"expire"`
ClientID string `json:"client_id"`
} `json:"records"`
}
if err := json.Unmarshal(out, &result); err != nil {
return nil
}
info := &DHCPInfo{Server: "kea"}
for _, r := range result.Records {
status := "active"
if r.State == "1" {
status = "declined"
} else if r.State == "2" {
status = "expired"
}
mac := r.HWAddr
if mac == "" {
mac = r.HWAddress
}
// Expire-Timestamp in lesbares Datum umwandeln
expireStr := r.Expire
if ts, err := strconv.ParseInt(r.Expire, 10, 64); err == nil && ts > 0 {
expireStr = time.Unix(ts, 0).Format("2006-01-02 15:04:05")
}
info.Leases = append(info.Leases, DHCPLease{
IP: r.Address,
MAC: mac,
Hostname: r.Hostname,
End: expireStr,
Status: status,
})
}
return info
}
// tryISCDhcpd parst ISC dhcpd Lease-Dateien
func tryISCDhcpd() *DHCPInfo {
// Gaengige Pfade auf OPNsense/FreeBSD
paths := []string{
"/var/dhcpd/var/db/dhcpd.leases",
"/var/db/dhcpd.leases",
}
for _, path := range paths {
out, err := exec.Command("cat", path).Output()
if err != nil || len(out) == 0 {
continue
}
leases := parseISCLeases(string(out))
if len(leases) > 0 {
return &DHCPInfo{Server: "isc-dhcpd", Leases: leases}
}
}
return nil
}
// parseISCLeases parst das ISC dhcpd.leases Format
func parseISCLeases(content string) []DHCPLease {
var leases []DHCPLease
var current *DHCPLease
for _, line := range strings.Split(content, "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "lease ") && strings.HasSuffix(line, "{") {
ip := strings.TrimSuffix(strings.TrimPrefix(line, "lease "), " {")
current = &DHCPLease{IP: strings.TrimSpace(ip), Status: "active"}
continue
}
if current == nil {
continue
}
if line == "}" {
leases = append(leases, *current)
current = nil
continue
}
line = strings.TrimSuffix(line, ";")
if strings.HasPrefix(line, "starts ") {
parts := strings.SplitN(line, " ", 3)
if len(parts) >= 3 {
current.Start = parts[2]
}
} else if strings.HasPrefix(line, "ends ") {
parts := strings.SplitN(line, " ", 3)
if len(parts) >= 3 {
current.End = parts[2]
}
} else if strings.HasPrefix(line, "hardware ethernet ") {
current.MAC = strings.TrimPrefix(line, "hardware ethernet ")
} else if strings.HasPrefix(line, "client-hostname ") {
current.Hostname = strings.Trim(strings.TrimPrefix(line, "client-hostname "), "\"")
} else if strings.HasPrefix(line, "binding state ") {
current.Status = strings.TrimPrefix(line, "binding state ")
}
}
return leases
}
// tryDnsmasqLeases sucht dnsmasq-Lease-Dateien
func tryDnsmasqLeases() *DHCPInfo {
paths := []string{
"/var/db/dnsmasq.leases",
"/tmp/dnsmasq.leases",
"/var/run/dnsmasq.leases",
}
for _, path := range paths {
out, err := exec.Command("cat", path).Output()
if err != nil || len(out) == 0 {
continue
}
leases := parseDnsmasqLeases(string(out))
if len(leases) > 0 {
return &DHCPInfo{Server: "dnsmasq", Leases: leases}
}
}
return nil
}
// parseDnsmasqLeases parst dnsmasq lease Format: timestamp mac ip hostname clientid
func parseDnsmasqLeases(content string) []DHCPLease {
var leases []DHCPLease
for _, line := range strings.Split(strings.TrimSpace(content), "\n") {
fields := strings.Fields(line)
if len(fields) < 4 {
continue
}
leases = append(leases, DHCPLease{
MAC: fields[1],
IP: fields[2],
Hostname: fields[3],
Status: "active",
})
}
return leases
}