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 }