Add configuration system and ARP-based device discovery

Implement comprehensive configuration management with CLI flags for WiFi interface, device discovery method, and file paths. Add ARP table parsing as an alternative to DHCP leases for more reliable device detection. Improve WiFi scanning to handle concurrent scan requests gracefully.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
nemunaire 2025-12-28 18:51:15 +07:00
commit 2b3a5b89f8
8 changed files with 156 additions and 28 deletions

View file

@ -25,7 +25,7 @@ func main() {
application := app.New(assets)
// Initialize the application
if err := application.Initialize(); err != nil {
if err := application.Initialize(cfg); err != nil {
log.Fatalf("Failed to initialize application: %v", err)
}
defer application.Shutdown()

View file

@ -4,6 +4,7 @@ import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/nemunaire/repeater/internal/config"
"github.com/nemunaire/repeater/internal/device"
"github.com/nemunaire/repeater/internal/hotspot"
"github.com/nemunaire/repeater/internal/logging"
@ -103,8 +104,8 @@ func ToggleHotspot(c *gin.Context, status *models.SystemStatus) {
}
// GetDevices returns connected devices
func GetDevices(c *gin.Context) {
devices, err := device.GetConnectedDevices()
func GetDevices(c *gin.Context, cfg *config.Config) {
devices, err := device.GetConnectedDevices(cfg)
if err != nil {
logging.AddLog("Système", "Erreur lors de la récupération des appareils: "+err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur lors de la récupération des appareils"})

View file

@ -7,11 +7,12 @@ import (
"github.com/gin-gonic/gin"
"github.com/nemunaire/repeater/internal/api/handlers"
"github.com/nemunaire/repeater/internal/config"
"github.com/nemunaire/repeater/internal/models"
)
// SetupRouter creates and configures the Gin router
func SetupRouter(status *models.SystemStatus, assets embed.FS) *gin.Engine {
func SetupRouter(status *models.SystemStatus, cfg *config.Config, assets embed.FS) *gin.Engine {
// Set Gin to release mode (can be overridden with GIN_MODE env var)
gin.SetMode(gin.ReleaseMode)
@ -38,7 +39,9 @@ func SetupRouter(status *models.SystemStatus, assets embed.FS) *gin.Engine {
}
// Device endpoints
api.GET("/devices", handlers.GetDevices)
api.GET("/devices", func(c *gin.Context) {
handlers.GetDevices(c, cfg)
})
// Status endpoint
api.GET("/status", func(c *gin.Context) {

View file

@ -7,6 +7,7 @@ import (
"time"
"github.com/nemunaire/repeater/internal/api"
"github.com/nemunaire/repeater/internal/config"
"github.com/nemunaire/repeater/internal/device"
"github.com/nemunaire/repeater/internal/logging"
"github.com/nemunaire/repeater/internal/models"
@ -19,6 +20,7 @@ type App struct {
StatusMutex sync.RWMutex
StartTime time.Time
Assets embed.FS
Config *config.Config
}
// New creates a new application instance
@ -38,9 +40,12 @@ func New(assets embed.FS) *App {
}
// Initialize initializes the application
func (a *App) Initialize() error {
func (a *App) Initialize(cfg *config.Config) error {
// Store config reference
a.Config = cfg
// Initialize WiFi D-Bus connection
if err := wifi.Initialize(); err != nil {
if err := wifi.Initialize(cfg.WifiInterface); err != nil {
return err
}
@ -54,7 +59,7 @@ func (a *App) Initialize() error {
// Run starts the HTTP server
func (a *App) Run(addr string) error {
router := api.SetupRouter(&a.Status, a.Assets)
router := api.SetupRouter(&a.Status, a.Config, a.Assets)
logging.AddLog("Système", "Serveur API démarré sur "+addr)
return router.Run(addr)
@ -88,7 +93,7 @@ func (a *App) periodicDeviceUpdate() {
defer ticker.Stop()
for range ticker.C {
devices, err := device.GetConnectedDevices()
devices, err := device.GetConnectedDevices(a.Config)
if err != nil {
log.Printf("Error getting connected devices: %v", err)
continue

View file

@ -7,6 +7,10 @@ import (
// declareFlags registers flags for the structure Options.
func declareFlags(o *Config) {
flag.StringVar(&o.Bind, "bind", ":8081", "Bind port/socket")
flag.StringVar(&o.WifiInterface, "wifi-interface", "wlan0", "WiFi interface name")
flag.BoolVar(&o.UseARPDiscovery, "use-arp-discovery", true, "Use ARP table for device discovery instead of DHCP leases")
flag.StringVar(&o.DHCPLeasesPath, "dhcp-leases-path", "/var/lib/dhcp/dhcpd.leases", "Path to DHCP leases file")
flag.StringVar(&o.ARPTablePath, "arp-table-path", "/proc/net/arp", "Path to ARP table file")
}
// parseCLI parse the flags and treats extra args as configuration filename.

View file

@ -9,7 +9,11 @@ import (
)
type Config struct {
Bind string
Bind string
WifiInterface string
UseARPDiscovery bool
DHCPLeasesPath string
ARPTablePath string
}
// ConsolidateConfig fills an Options struct by reading configuration from
@ -19,7 +23,11 @@ type Config struct {
func ConsolidateConfig() (opts *Config, err error) {
// Define defaults options
opts = &Config{
Bind: ":8080",
Bind: ":8080",
WifiInterface: "wlan0",
UseARPDiscovery: true,
DHCPLeasesPath: "/var/lib/dhcp/dhcpd.leases",
ARPTablePath: "/proc/net/arp",
}
declareFlags(opts)

View file

@ -2,25 +2,71 @@ package device
import (
"bufio"
"fmt"
"net"
"os"
"os/exec"
"regexp"
"strings"
"github.com/nemunaire/repeater/internal/config"
"github.com/nemunaire/repeater/internal/models"
)
// ARPEntry represents an entry in the ARP table
type ARPEntry struct {
IP net.IP
HWType int
Flags int
HWAddress net.HardwareAddr
Mask string
Device string
}
// GetConnectedDevices returns a list of connected devices
func GetConnectedDevices() ([]models.ConnectedDevice, error) {
func GetConnectedDevices(cfg *config.Config) ([]models.ConnectedDevice, error) {
if cfg.UseARPDiscovery {
return getDevicesFromARP(cfg)
}
return getDevicesFromDHCP(cfg)
}
// getDevicesFromARP discovers devices using ARP table
func getDevicesFromARP(cfg *config.Config) ([]models.ConnectedDevice, error) {
var devices []models.ConnectedDevice
// Read DHCP leases
leases, err := parseDHCPLeases()
arpEntries, err := parseARPTable(cfg.ARPTablePath)
if err != nil {
return devices, err
}
// Get ARP information
for _, entry := range arpEntries {
// Only include entries with valid flags (2 = COMPLETE, 6 = COMPLETE|PERM)
if entry.Flags == 2 || entry.Flags == 6 {
device := models.ConnectedDevice{
Name: "", // No hostname available from ARP
MAC: entry.HWAddress.String(),
IP: entry.IP.String(),
Type: guessDeviceType("", entry.HWAddress.String()),
}
devices = append(devices, device)
}
}
return devices, nil
}
// getDevicesFromDHCP discovers devices using DHCP leases and ARP validation
func getDevicesFromDHCP(cfg *config.Config) ([]models.ConnectedDevice, error) {
var devices []models.ConnectedDevice
// Read DHCP leases
leases, err := parseDHCPLeases(cfg.DHCPLeasesPath)
if err != nil {
return devices, err
}
// Get ARP information for validation
arpInfo, err := getARPInfo()
if err != nil {
return devices, err
@ -43,11 +89,57 @@ func GetConnectedDevices() ([]models.ConnectedDevice, error) {
return devices, nil
}
// parseARPTable reads and parses ARP table from /proc/net/arp format
func parseARPTable(path string) ([]ARPEntry, error) {
var entries []ARPEntry
content, err := os.ReadFile(path)
if err != nil {
return entries, err
}
for _, line := range strings.Split(string(content), "\n") {
fields := strings.Fields(line)
if len(fields) > 5 {
var entry ARPEntry
// Parse HWType (hex format)
if _, err := fmt.Sscanf(fields[1], "0x%x", &entry.HWType); err != nil {
continue
}
// Parse Flags (hex format)
if _, err := fmt.Sscanf(fields[2], "0x%x", &entry.Flags); err != nil {
continue
}
// Parse IP address
entry.IP = net.ParseIP(fields[0])
if entry.IP == nil {
continue
}
// Parse MAC address
entry.HWAddress, err = net.ParseMAC(fields[3])
if err != nil {
continue
}
entry.Mask = fields[4]
entry.Device = fields[5]
entries = append(entries, entry)
}
}
return entries, nil
}
// parseDHCPLeases reads and parses DHCP lease file
func parseDHCPLeases() ([]models.DHCPLease, error) {
func parseDHCPLeases(path string) ([]models.DHCPLease, error) {
var leases []models.DHCPLease
file, err := os.Open("/var/lib/dhcp/dhcpd.leases")
file, err := os.Open(path)
if err != nil {
return leases, err
}

View file

@ -12,8 +12,7 @@ import (
)
const (
WLAN_INTERFACE = "wlan0"
WPA_CONF = "/etc/wpa_supplicant/wpa_supplicant.conf"
WPA_CONF = "/etc/wpa_supplicant/wpa_supplicant.conf"
// D-Bus constants for wpa_supplicant
WPA_SUPPLICANT_SERVICE = "fi.w1.wpa_supplicant1"
@ -25,12 +24,14 @@ const (
)
var (
wlanInterface string
dbusConn *dbus.Conn
wpaSupplicant dbus.BusObject
)
// Initialize initializes the WiFi service with D-Bus connection
func Initialize() error {
func Initialize(interfaceName string) error {
wlanInterface = interfaceName
var err error
dbusConn, err = dbus.SystemBus()
if err != nil {
@ -55,15 +56,29 @@ func ScanNetworks() ([]models.WiFiNetwork, error) {
return nil, fmt.Errorf("impossible d'obtenir l'interface WiFi: %v", err)
}
// Trigger a scan
wifiInterface := dbusConn.Object(WPA_SUPPLICANT_SERVICE, interfacePath)
call := wifiInterface.Call(WPA_INTERFACE_IFACE+".Scan", 0, map[string]dbus.Variant{"Type": dbus.MakeVariant("active")})
if call.Err != nil {
return nil, fmt.Errorf("erreur lors du scan: %v", call.Err)
}
// Wait for scan to complete
time.Sleep(2 * time.Second)
// Check current scanning state
scanning, err := wifiInterface.GetProperty(WPA_INTERFACE_IFACE + ".Scanning")
if err == nil && scanning.Value().(bool) {
// Scan already in progress, wait for it to complete
time.Sleep(3 * time.Second)
} else {
// Trigger a scan
call := wifiInterface.Call(WPA_INTERFACE_IFACE+".Scan", 0, map[string]dbus.Variant{"Type": dbus.MakeVariant("active")})
if call.Err != nil {
// If scan is rejected, it might be too soon after a previous scan
// Try to use cached results instead
if strings.Contains(call.Err.Error(), "rejected") {
// Continue to retrieve existing BSS list
} else {
return nil, fmt.Errorf("erreur lors du scan: %v", call.Err)
}
} else {
// Wait for scan to complete
time.Sleep(2 * time.Second)
}
}
// Retrieve BSS list
bssePaths, err := wifiInterface.GetProperty(WPA_INTERFACE_IFACE + ".BSSs")
@ -224,7 +239,7 @@ func IsConnected() bool {
// IsConnectedLegacy checks if WiFi is connected using iwconfig (fallback)
func IsConnectedLegacy() bool {
cmd := exec.Command("iwconfig", WLAN_INTERFACE)
cmd := exec.Command("iwconfig", wlanInterface)
output, err := cmd.Output()
if err != nil {
return false
@ -236,7 +251,7 @@ func IsConnectedLegacy() bool {
// getWiFiInterfacePath retrieves the D-Bus path for the WiFi interface
func getWiFiInterfacePath() (dbus.ObjectPath, error) {
var interfacePath dbus.ObjectPath
err := wpaSupplicant.Call(WPA_SUPPLICANT_IFACE+".GetInterface", 0, WLAN_INTERFACE).Store(&interfacePath)
err := wpaSupplicant.Call(WPA_SUPPLICANT_IFACE+".GetInterface", 0, wlanInterface).Store(&interfacePath)
if err != nil {
return "", fmt.Errorf("erreur lors de la récupération des interfaces: %v", err)
}