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:
parent
accd7e75d8
commit
2b3a5b89f8
8 changed files with 156 additions and 28 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"})
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue