repeater/internal/station/hostapd/correlation.go
Pierre-Olivier Mercier a758c331c0 station: Default device type to "unknown" and propagate DHCP hostname
GuessDeviceType silently returned "mobile" for any device whose hostname
or MAC OUI didn't match a known pattern, so the UI labelled every
unidentified device as a phone. Default to "unknown" instead and broaden
hostname matching (pixel/galaxy, thinkpad, imac/-pc, QEMU OUI).

The hostapd backend was also dropping DHCP hostnames on the floor: the
correlator only forwarded MAC->IP, and convertStation hard-coded the
hostname to "". Replace UpdateIPMapping with UpdateLeaseInfo that carries
both maps so hostnames flow through to ConnectedDevice.Name.

Frontend gains a "Sans nom" fallback when no hostname is available and
French labels for the device-type badge.
2026-05-01 22:21:14 +08:00

136 lines
3.2 KiB
Go

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()
}