- 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
208 lines
5.0 KiB
Go
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
|
|
}
|