package hostapd import ( "bufio" "log" "os" "strings" "time" "github.com/nemunaire/repeater/internal/models" ) // DHCPCorrelator helps correlate hostapd stations with DHCP leases to discover IP addresses type DHCPCorrelator struct { backend *Backend dhcpLeasesPath string stopChan chan struct{} running bool } // NewDHCPCorrelator creates a new DHCP correlator func NewDHCPCorrelator(backend *Backend, dhcpLeasesPath string) *DHCPCorrelator { if dhcpLeasesPath == "" { dhcpLeasesPath = "/var/lib/dhcp/dhcpd.leases" } return &DHCPCorrelator{ backend: backend, dhcpLeasesPath: dhcpLeasesPath, stopChan: make(chan struct{}), } } // Start begins periodic correlation of DHCP leases with hostapd stations func (dc *DHCPCorrelator) Start() { if dc.running { return } dc.running = true go dc.correlationLoop() log.Printf("DHCP correlation started for hostapd backend") } // Stop stops the correlation loop func (dc *DHCPCorrelator) Stop() { if !dc.running { return } dc.running = false close(dc.stopChan) log.Printf("DHCP correlation stopped") } // correlationLoop periodically correlates DHCP leases with stations func (dc *DHCPCorrelator) correlationLoop() { // Do an initial correlation immediately dc.correlate() // Then correlate every 10 seconds ticker := time.NewTicker(10 * time.Second) defer ticker.Stop() for { select { case <-ticker.C: dc.correlate() case <-dc.stopChan: return } } } // correlate performs one correlation cycle func (dc *DHCPCorrelator) correlate() { // Parse DHCP leases leases, err := parseDHCPLeases(dc.dhcpLeasesPath) if err != nil { log.Printf("Warning: Failed to parse DHCP leases: %v", err) return } // Build MAC -> IP and MAC -> hostname mappings. Hostnames live in the // dhcpd "client-hostname" field and let us label devices instead of // falling back to bare MAC addresses. macToIP := make(map[string]string) macToHostname := make(map[string]string) for _, lease := range leases { mac := strings.ToLower(lease.MAC) macToIP[mac] = lease.IP if lease.Hostname != "" { macToHostname[mac] = lease.Hostname } } dc.backend.UpdateLeaseInfo(macToIP, macToHostname) } // parseDHCPLeases reads and parses DHCP lease file func parseDHCPLeases(path string) ([]models.DHCPLease, error) { var leases []models.DHCPLease file, err := os.Open(path) if err != nil { return leases, err } defer file.Close() scanner := bufio.NewScanner(file) var currentLease models.DHCPLease for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if strings.HasPrefix(line, "lease ") { ip := strings.Fields(line)[1] currentLease = models.DHCPLease{IP: ip} } else if strings.Contains(line, "hardware ethernet") { mac := strings.Fields(line)[2] mac = strings.TrimSuffix(mac, ";") currentLease.MAC = mac } else if strings.Contains(line, "client-hostname") { hostname := strings.Fields(line)[1] hostname = strings.Trim(hostname, `";`) currentLease.Hostname = hostname } else if line == "}" { if currentLease.IP != "" && currentLease.MAC != "" { leases = append(leases, currentLease) } currentLease = models.DHCPLease{} } } return leases, scanner.Err() }