station/hostapd: Resolve station IPs via udhcpd leases and ARP fallback
The hostapd backend never populated IPs: NewDHCPCorrelator was defined
but never instantiated, and even when it was, the parser only handled
ISC dhcpd's text format. On a BusyBox-based router using udhcpd, every
device showed up with an empty IP.
Two fixes:
- Add a udhcpd binary lease parser. The format is documented in
busybox/networking/udhcp/dhcpd.{h,c}: an 8-byte big-endian unix-time
header followed by 36-byte dyn_lease records (expires, IP, MAC,
20-byte hostname, 2-byte pad). ParseLeases auto-detects the format
by inspecting the header so the same code path handles both udhcpd
and ISC text leases.
- Wire the DHCPCorrelator into Backend.Initialize and have it merge
two sources: ARP first (universal IP fallback for any station that
has been talked to) and DHCP leases on top (authoritative, carries
the hostname). ARP fills the gap when leases are missing or the
station uses a static IP; DHCP wins on conflict.
Default DHCPLeasesPath updated to /var/lib/udhcpd/udhcpd.leases — the
common BusyBox path. Configurable as before.
This commit is contained in:
parent
249217d4ad
commit
5a3942f351
8 changed files with 267 additions and 72 deletions
|
|
@ -30,6 +30,8 @@ type Backend struct {
|
|||
// IP / hostname correlation - populated by periodic DHCP lease correlation
|
||||
ipByMAC map[string]string // MAC -> IP mapping
|
||||
hostnameByMAC map[string]string // MAC -> hostname mapping
|
||||
|
||||
correlator *DHCPCorrelator
|
||||
}
|
||||
|
||||
// NewBackend creates a new hostapd backend
|
||||
|
|
@ -70,11 +72,20 @@ func (b *Backend) Initialize(config backend.BackendConfig) error {
|
|||
log.Printf("Warning: Failed to load initial stations: %v", err)
|
||||
}
|
||||
|
||||
// Hostapd only knows MACs. Pair every station with its IP/hostname by
|
||||
// running a background correlator that polls the DHCP lease file and
|
||||
// the ARP table. Without this, the device list shows MACs only.
|
||||
b.correlator = NewDHCPCorrelator(b, config.DHCPLeasesPath, config.ARPTablePath)
|
||||
b.correlator.Start()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close cleans up backend resources
|
||||
func (b *Backend) Close() error {
|
||||
if b.correlator != nil {
|
||||
b.correlator.Stop()
|
||||
}
|
||||
b.StopEventMonitoring()
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,32 +1,38 @@
|
|||
package hostapd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/nemunaire/repeater/internal/models"
|
||||
"github.com/nemunaire/repeater/internal/station/arp"
|
||||
"github.com/nemunaire/repeater/internal/station/dhcp"
|
||||
)
|
||||
|
||||
// DHCPCorrelator helps correlate hostapd stations with DHCP leases to discover IP addresses
|
||||
// DHCPCorrelator helps correlate hostapd stations with DHCP leases (and the
|
||||
// ARP table as a fallback) so the UI gets an IP and hostname for every
|
||||
// station, not just a MAC. Hostapd itself only knows MACs.
|
||||
type DHCPCorrelator struct {
|
||||
backend *Backend
|
||||
dhcpLeasesPath string
|
||||
arpTablePath string
|
||||
stopChan chan struct{}
|
||||
running bool
|
||||
}
|
||||
|
||||
// NewDHCPCorrelator creates a new DHCP correlator
|
||||
func NewDHCPCorrelator(backend *Backend, dhcpLeasesPath string) *DHCPCorrelator {
|
||||
func NewDHCPCorrelator(backend *Backend, dhcpLeasesPath, arpTablePath string) *DHCPCorrelator {
|
||||
if dhcpLeasesPath == "" {
|
||||
dhcpLeasesPath = "/var/lib/dhcp/dhcpd.leases"
|
||||
dhcpLeasesPath = "/var/lib/misc/udhcpd.leases"
|
||||
}
|
||||
if arpTablePath == "" {
|
||||
arpTablePath = "/proc/net/arp"
|
||||
}
|
||||
|
||||
return &DHCPCorrelator{
|
||||
backend: backend,
|
||||
dhcpLeasesPath: dhcpLeasesPath,
|
||||
arpTablePath: arpTablePath,
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
|
@ -39,7 +45,7 @@ func (dc *DHCPCorrelator) Start() {
|
|||
|
||||
dc.running = true
|
||||
go dc.correlationLoop()
|
||||
log.Printf("DHCP correlation started for hostapd backend")
|
||||
log.Printf("DHCP/ARP correlation started (leases=%s, arp=%s)", dc.dhcpLeasesPath, dc.arpTablePath)
|
||||
}
|
||||
|
||||
// Stop stops the correlation loop
|
||||
|
|
@ -72,65 +78,54 @@ func (dc *DHCPCorrelator) correlationLoop() {
|
|||
}
|
||||
}
|
||||
|
||||
// correlate performs one correlation cycle
|
||||
// correlate performs one correlation cycle. We pull from two sources:
|
||||
//
|
||||
// - The DHCP lease file (udhcpd binary or ISC text — auto-detected) is the
|
||||
// authoritative source for hostnames and the IP that was actually
|
||||
// assigned.
|
||||
// - The ARP table is a universal IP fallback. If the lease file is missing
|
||||
// or the station uses a static IP, ARP still gives us its current IPv4.
|
||||
//
|
||||
// The two are merged: ARP fills gaps, DHCP wins on conflict because it
|
||||
// carries the hostname and is less prone to stale entries.
|
||||
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
|
||||
|
||||
// ARP first, so DHCP overrides on conflict.
|
||||
if entries, err := arp.ParseTable(dc.arpTablePath); err == nil {
|
||||
for _, entry := range entries {
|
||||
// Flags 2 (COMPLETE) and 6 (COMPLETE|PERM) — incomplete
|
||||
// entries have a zero MAC and would pollute the mapping.
|
||||
if entry.Flags != 2 && entry.Flags != 6 {
|
||||
continue
|
||||
}
|
||||
mac := strings.ToLower(entry.HWAddress.String())
|
||||
if mac == "" || mac == "00:00:00:00:00:00" {
|
||||
continue
|
||||
}
|
||||
macToIP[mac] = entry.IP.String()
|
||||
}
|
||||
} else {
|
||||
log.Printf("ARP table read failed (%s): %v", dc.arpTablePath, err)
|
||||
}
|
||||
|
||||
if leases, err := dhcp.ParseLeases(dc.dhcpLeasesPath); err == nil {
|
||||
for _, lease := range leases {
|
||||
mac := strings.ToLower(lease.MAC)
|
||||
if mac == "" {
|
||||
continue
|
||||
}
|
||||
if lease.IP != "" {
|
||||
macToIP[mac] = lease.IP
|
||||
}
|
||||
if lease.Hostname != "" {
|
||||
macToHostname[mac] = lease.Hostname
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Printf("DHCP lease parse failed (%s): %v", dc.dhcpLeasesPath, err)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue