Refactor stations discovery and add hostapd discovery
This commit is contained in:
parent
69594c2fe4
commit
2922a03724
15 changed files with 1339 additions and 249 deletions
|
|
@ -5,10 +5,10 @@ import (
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/nemunaire/repeater/internal/config"
|
"github.com/nemunaire/repeater/internal/config"
|
||||||
"github.com/nemunaire/repeater/internal/device"
|
|
||||||
"github.com/nemunaire/repeater/internal/hotspot"
|
"github.com/nemunaire/repeater/internal/hotspot"
|
||||||
"github.com/nemunaire/repeater/internal/logging"
|
"github.com/nemunaire/repeater/internal/logging"
|
||||||
"github.com/nemunaire/repeater/internal/models"
|
"github.com/nemunaire/repeater/internal/models"
|
||||||
|
"github.com/nemunaire/repeater/internal/station"
|
||||||
"github.com/nemunaire/repeater/internal/wifi"
|
"github.com/nemunaire/repeater/internal/wifi"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -120,7 +120,7 @@ func ToggleHotspot(c *gin.Context, status *models.SystemStatus) {
|
||||||
|
|
||||||
// GetDevices returns connected devices
|
// GetDevices returns connected devices
|
||||||
func GetDevices(c *gin.Context, cfg *config.Config) {
|
func GetDevices(c *gin.Context, cfg *config.Config) {
|
||||||
devices, err := device.GetConnectedDevices(cfg)
|
devices, err := station.GetStations()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.AddLog("Système", "Erreur lors de la récupération des appareils: "+err.Error())
|
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"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur lors de la récupération des appareils"})
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,11 @@ import (
|
||||||
|
|
||||||
"github.com/nemunaire/repeater/internal/api"
|
"github.com/nemunaire/repeater/internal/api"
|
||||||
"github.com/nemunaire/repeater/internal/config"
|
"github.com/nemunaire/repeater/internal/config"
|
||||||
"github.com/nemunaire/repeater/internal/device"
|
|
||||||
"github.com/nemunaire/repeater/internal/hotspot"
|
"github.com/nemunaire/repeater/internal/hotspot"
|
||||||
"github.com/nemunaire/repeater/internal/logging"
|
"github.com/nemunaire/repeater/internal/logging"
|
||||||
"github.com/nemunaire/repeater/internal/models"
|
"github.com/nemunaire/repeater/internal/models"
|
||||||
|
"github.com/nemunaire/repeater/internal/station"
|
||||||
|
"github.com/nemunaire/repeater/internal/station/backend"
|
||||||
"github.com/nemunaire/repeater/internal/syslog"
|
"github.com/nemunaire/repeater/internal/syslog"
|
||||||
"github.com/nemunaire/repeater/internal/wifi"
|
"github.com/nemunaire/repeater/internal/wifi"
|
||||||
)
|
)
|
||||||
|
|
@ -62,6 +63,28 @@ func (a *App) Initialize(cfg *config.Config) error {
|
||||||
// Don't fail - polling fallback still works
|
// Don't fail - polling fallback still works
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize station backend
|
||||||
|
stationConfig := backend.BackendConfig{
|
||||||
|
InterfaceName: cfg.HotspotInterface,
|
||||||
|
ARPTablePath: cfg.ARPTablePath,
|
||||||
|
DHCPLeasesPath: cfg.DHCPLeasesPath,
|
||||||
|
HostapdInterface: cfg.HotspotInterface,
|
||||||
|
}
|
||||||
|
if err := station.Initialize(cfg.StationBackend, stationConfig); err != nil {
|
||||||
|
log.Printf("Warning: Station backend initialization failed: %v", err)
|
||||||
|
// Don't fail - will continue without station discovery
|
||||||
|
} else {
|
||||||
|
// Start event monitoring for station events
|
||||||
|
if err := station.StartEventMonitoring(backend.EventCallbacks{
|
||||||
|
OnStationConnected: a.handleStationConnected,
|
||||||
|
OnStationDisconnected: a.handleStationDisconnected,
|
||||||
|
OnStationUpdated: a.handleStationUpdated,
|
||||||
|
}); err != nil {
|
||||||
|
log.Printf("Warning: Station event monitoring failed: %v", err)
|
||||||
|
// Don't fail - polling fallback still works
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Start syslog tailing if enabled
|
// Start syslog tailing if enabled
|
||||||
if cfg.SyslogEnabled {
|
if cfg.SyslogEnabled {
|
||||||
a.SyslogTailer = syslog.NewSyslogTailer(
|
a.SyslogTailer = syslog.NewSyslogTailer(
|
||||||
|
|
@ -98,6 +121,10 @@ func (a *App) Shutdown() {
|
||||||
a.SyslogTailer.Stop()
|
a.SyslogTailer.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop station monitoring and close backend
|
||||||
|
station.StopEventMonitoring()
|
||||||
|
station.Close()
|
||||||
|
|
||||||
wifi.StopEventMonitoring()
|
wifi.StopEventMonitoring()
|
||||||
wifi.Close()
|
wifi.Close()
|
||||||
logging.AddLog("Système", "Application arrêtée")
|
logging.AddLog("Système", "Application arrêtée")
|
||||||
|
|
@ -181,10 +208,9 @@ func (a *App) periodicDeviceUpdate() {
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
devices, err := device.GetConnectedDevices(a.Config)
|
devices, err := station.GetStations()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error getting connected devices: %v", err)
|
log.Printf("Error getting connected devices: %v", err)
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a.StatusMutex.Lock()
|
a.StatusMutex.Lock()
|
||||||
|
|
@ -193,3 +219,69 @@ func (a *App) periodicDeviceUpdate() {
|
||||||
a.StatusMutex.Unlock()
|
a.StatusMutex.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleStationConnected handles station connection events
|
||||||
|
func (a *App) handleStationConnected(st backend.Station) {
|
||||||
|
a.StatusMutex.Lock()
|
||||||
|
defer a.StatusMutex.Unlock()
|
||||||
|
|
||||||
|
// Convert backend.Station to models.ConnectedDevice
|
||||||
|
device := models.ConnectedDevice{
|
||||||
|
Name: st.Hostname,
|
||||||
|
Type: st.Type,
|
||||||
|
MAC: st.MAC,
|
||||||
|
IP: st.IP,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if device already exists
|
||||||
|
found := false
|
||||||
|
for i, d := range a.Status.ConnectedDevices {
|
||||||
|
if d.MAC == device.MAC {
|
||||||
|
a.Status.ConnectedDevices[i] = device
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new device if not found
|
||||||
|
if !found {
|
||||||
|
a.Status.ConnectedDevices = append(a.Status.ConnectedDevices, device)
|
||||||
|
a.Status.ConnectedCount = len(a.Status.ConnectedDevices)
|
||||||
|
logging.AddLog("Stations", "Device connected: "+device.MAC+" ("+device.IP+")")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleStationDisconnected handles station disconnection events
|
||||||
|
func (a *App) handleStationDisconnected(mac string) {
|
||||||
|
a.StatusMutex.Lock()
|
||||||
|
defer a.StatusMutex.Unlock()
|
||||||
|
|
||||||
|
// Remove device from list
|
||||||
|
for i, d := range a.Status.ConnectedDevices {
|
||||||
|
if d.MAC == mac {
|
||||||
|
a.Status.ConnectedDevices = append(a.Status.ConnectedDevices[:i], a.Status.ConnectedDevices[i+1:]...)
|
||||||
|
a.Status.ConnectedCount = len(a.Status.ConnectedDevices)
|
||||||
|
logging.AddLog("Stations", "Device disconnected: "+mac)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleStationUpdated handles station update events
|
||||||
|
func (a *App) handleStationUpdated(st backend.Station) {
|
||||||
|
a.StatusMutex.Lock()
|
||||||
|
defer a.StatusMutex.Unlock()
|
||||||
|
|
||||||
|
// Update existing device
|
||||||
|
for i, d := range a.Status.ConnectedDevices {
|
||||||
|
if d.MAC == st.MAC {
|
||||||
|
a.Status.ConnectedDevices[i] = models.ConnectedDevice{
|
||||||
|
Name: st.Hostname,
|
||||||
|
Type: st.Type,
|
||||||
|
MAC: st.MAC,
|
||||||
|
IP: st.IP,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,9 @@ import (
|
||||||
func declareFlags(o *Config) {
|
func declareFlags(o *Config) {
|
||||||
flag.StringVar(&o.Bind, "bind", ":8081", "Bind port/socket")
|
flag.StringVar(&o.Bind, "bind", ":8081", "Bind port/socket")
|
||||||
flag.StringVar(&o.WifiInterface, "wifi-interface", "wlan0", "WiFi interface name")
|
flag.StringVar(&o.WifiInterface, "wifi-interface", "wlan0", "WiFi interface name")
|
||||||
|
flag.StringVar(&o.HotspotInterface, "hotspot-interface", "wlan1", "Hotspot WiFi interface name")
|
||||||
flag.StringVar(&o.WifiBackend, "wifi-backend", "", "WiFi backend to use: 'iwd' or 'wpasupplicant' (required)")
|
flag.StringVar(&o.WifiBackend, "wifi-backend", "", "WiFi backend to use: 'iwd' or 'wpasupplicant' (required)")
|
||||||
flag.BoolVar(&o.UseARPDiscovery, "use-arp-discovery", true, "Use ARP table for device discovery instead of DHCP leases")
|
flag.StringVar(&o.StationBackend, "station-backend", "hostapd", "Station discovery backend: 'arp', 'dhcp', or 'hostapd'")
|
||||||
flag.StringVar(&o.DHCPLeasesPath, "dhcp-leases-path", "/var/lib/dhcp/dhcpd.leases", "Path to DHCP leases file")
|
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")
|
flag.StringVar(&o.ARPTablePath, "arp-table-path", "/proc/net/arp", "Path to ARP table file")
|
||||||
flag.BoolVar(&o.SyslogEnabled, "syslog-enabled", false, "Enable syslog tailing for iwd messages")
|
flag.BoolVar(&o.SyslogEnabled, "syslog-enabled", false, "Enable syslog tailing for iwd messages")
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,9 @@ import (
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Bind string
|
Bind string
|
||||||
WifiInterface string
|
WifiInterface string
|
||||||
|
HotspotInterface string
|
||||||
WifiBackend string
|
WifiBackend string
|
||||||
UseARPDiscovery bool
|
StationBackend string // "arp", "dhcp", or "hostapd"
|
||||||
DHCPLeasesPath string
|
DHCPLeasesPath string
|
||||||
ARPTablePath string
|
ARPTablePath string
|
||||||
SyslogEnabled bool
|
SyslogEnabled bool
|
||||||
|
|
@ -30,7 +31,7 @@ func ConsolidateConfig() (opts *Config, err error) {
|
||||||
opts = &Config{
|
opts = &Config{
|
||||||
Bind: ":8080",
|
Bind: ":8080",
|
||||||
WifiInterface: "wlan0",
|
WifiInterface: "wlan0",
|
||||||
UseARPDiscovery: true,
|
HotspotInterface: "wlan1",
|
||||||
DHCPLeasesPath: "/var/lib/dhcp/dhcpd.leases",
|
DHCPLeasesPath: "/var/lib/dhcp/dhcpd.leases",
|
||||||
ARPTablePath: "/proc/net/arp",
|
ARPTablePath: "/proc/net/arp",
|
||||||
SyslogEnabled: false,
|
SyslogEnabled: false,
|
||||||
|
|
@ -81,6 +82,10 @@ func ConsolidateConfig() (opts *Config, err error) {
|
||||||
log.Fatalf("wifi-backend must be set to 'iwd' or 'wpasupplicant' (got: '%s')", opts.WifiBackend)
|
log.Fatalf("wifi-backend must be set to 'iwd' or 'wpasupplicant' (got: '%s')", opts.WifiBackend)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.StationBackend != "arp" && opts.StationBackend != "dhcp" && opts.StationBackend != "hostapd" {
|
||||||
|
log.Fatalf("station-backend must be set to 'arp', 'dhcp', or 'hostapd' (got: '%s')", opts.StationBackend)
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,224 +0,0 @@
|
||||||
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(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
|
|
||||||
|
|
||||||
arpEntries, err := parseARPTable(cfg.ARPTablePath)
|
|
||||||
if err != nil {
|
|
||||||
return devices, err
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, lease := range leases {
|
|
||||||
device := models.ConnectedDevice{
|
|
||||||
Name: lease.Hostname,
|
|
||||||
MAC: lease.MAC,
|
|
||||||
IP: lease.IP,
|
|
||||||
Type: guessDeviceType(lease.Hostname, lease.MAC),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the device is still connected via ARP
|
|
||||||
if _, exists := arpInfo[lease.IP]; exists {
|
|
||||||
devices = append(devices, device)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(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, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getARPInfo retrieves ARP table information
|
|
||||||
func getARPInfo() (map[string]string, error) {
|
|
||||||
arpInfo := make(map[string]string)
|
|
||||||
|
|
||||||
cmd := exec.Command("arp", "-a")
|
|
||||||
output, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
return arpInfo, err
|
|
||||||
}
|
|
||||||
|
|
||||||
lines := strings.Split(string(output), "\n")
|
|
||||||
for _, line := range lines {
|
|
||||||
if matches := regexp.MustCompile(`\(([^)]+)\) at ([0-9a-fA-F:]{17})`).FindStringSubmatch(line); len(matches) > 2 {
|
|
||||||
ip := matches[1]
|
|
||||||
mac := matches[2]
|
|
||||||
arpInfo[ip] = mac
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return arpInfo, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// guessDeviceType attempts to guess device type from hostname and MAC address
|
|
||||||
func guessDeviceType(hostname, mac string) string {
|
|
||||||
hostname = strings.ToLower(hostname)
|
|
||||||
|
|
||||||
if strings.Contains(hostname, "iphone") || strings.Contains(hostname, "android") {
|
|
||||||
return "mobile"
|
|
||||||
} else if strings.Contains(hostname, "ipad") || strings.Contains(hostname, "tablet") {
|
|
||||||
return "tablet"
|
|
||||||
} else if strings.Contains(hostname, "macbook") || strings.Contains(hostname, "laptop") {
|
|
||||||
return "laptop"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Guess by MAC prefix (OUI)
|
|
||||||
if len(mac) >= 8 {
|
|
||||||
macPrefix := strings.ToUpper(mac[:8])
|
|
||||||
switch macPrefix {
|
|
||||||
case "00:50:56", "00:0C:29", "00:05:69": // VMware
|
|
||||||
return "laptop"
|
|
||||||
case "08:00:27": // VirtualBox
|
|
||||||
return "laptop"
|
|
||||||
default:
|
|
||||||
return "mobile"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "unknown"
|
|
||||||
}
|
|
||||||
177
internal/station/arp/backend.go
Normal file
177
internal/station/arp/backend.go
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
package arp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nemunaire/repeater/internal/station/backend"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Backend implements StationBackend using ARP table discovery
|
||||||
|
type Backend struct {
|
||||||
|
arpTablePath string
|
||||||
|
lastStations map[string]backend.Station // Key: MAC address
|
||||||
|
callbacks backend.EventCallbacks
|
||||||
|
stopChan chan struct{}
|
||||||
|
mu sync.RWMutex
|
||||||
|
running bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBackend creates a new ARP backend
|
||||||
|
func NewBackend() *Backend {
|
||||||
|
return &Backend{
|
||||||
|
lastStations: make(map[string]backend.Station),
|
||||||
|
stopChan: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize initializes the ARP backend
|
||||||
|
func (b *Backend) Initialize(config backend.BackendConfig) error {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
b.arpTablePath = config.ARPTablePath
|
||||||
|
if b.arpTablePath == "" {
|
||||||
|
b.arpTablePath = "/proc/net/arp"
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close cleans up backend resources
|
||||||
|
func (b *Backend) Close() error {
|
||||||
|
b.StopEventMonitoring()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStations returns all connected stations from ARP table
|
||||||
|
func (b *Backend) GetStations() ([]backend.Station, error) {
|
||||||
|
b.mu.RLock()
|
||||||
|
arpTablePath := b.arpTablePath
|
||||||
|
b.mu.RUnlock()
|
||||||
|
|
||||||
|
arpEntries, err := parseARPTable(arpTablePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var stations []backend.Station
|
||||||
|
for _, entry := range arpEntries {
|
||||||
|
// Only include entries with valid flags (2 = COMPLETE, 6 = COMPLETE|PERM)
|
||||||
|
if entry.Flags == 2 || entry.Flags == 6 {
|
||||||
|
st := backend.Station{
|
||||||
|
MAC: entry.HWAddress.String(),
|
||||||
|
IP: entry.IP.String(),
|
||||||
|
Hostname: "", // No hostname available from ARP
|
||||||
|
Type: backend.GuessDeviceType("", entry.HWAddress.String()),
|
||||||
|
Signal: 0, // Not available from ARP
|
||||||
|
RxBytes: 0, // Not available from ARP
|
||||||
|
TxBytes: 0, // Not available from ARP
|
||||||
|
ConnectedAt: time.Now(),
|
||||||
|
}
|
||||||
|
stations = append(stations, st)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stations, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartEventMonitoring starts monitoring for station events via polling
|
||||||
|
func (b *Backend) StartEventMonitoring(callbacks backend.EventCallbacks) error {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
if b.running {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b.callbacks = callbacks
|
||||||
|
b.running = true
|
||||||
|
|
||||||
|
// Start polling goroutine
|
||||||
|
go b.pollLoop()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopEventMonitoring stops event monitoring
|
||||||
|
func (b *Backend) StopEventMonitoring() {
|
||||||
|
b.mu.Lock()
|
||||||
|
if !b.running {
|
||||||
|
b.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.running = false
|
||||||
|
b.mu.Unlock()
|
||||||
|
|
||||||
|
close(b.stopChan)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportsRealTimeEvents returns false (ARP is polling-based)
|
||||||
|
func (b *Backend) SupportsRealTimeEvents() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// pollLoop polls the ARP table and simulates events
|
||||||
|
func (b *Backend) pollLoop() {
|
||||||
|
ticker := time.NewTicker(10 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
b.checkForChanges()
|
||||||
|
case <-b.stopChan:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkForChanges compares current state with last state and triggers callbacks
|
||||||
|
func (b *Backend) checkForChanges() {
|
||||||
|
// Get current stations
|
||||||
|
current, err := b.GetStations()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build map of current stations
|
||||||
|
currentMap := make(map[string]backend.Station)
|
||||||
|
for _, station := range current {
|
||||||
|
currentMap[station.MAC] = station
|
||||||
|
}
|
||||||
|
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
// Check for new stations (connected)
|
||||||
|
for mac, station := range currentMap {
|
||||||
|
if _, exists := b.lastStations[mac]; !exists {
|
||||||
|
// New station connected
|
||||||
|
if b.callbacks.OnStationConnected != nil {
|
||||||
|
go b.callbacks.OnStationConnected(station)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Check for updates (IP change, etc.)
|
||||||
|
oldStation := b.lastStations[mac]
|
||||||
|
if oldStation.IP != station.IP || oldStation.Hostname != station.Hostname {
|
||||||
|
if b.callbacks.OnStationUpdated != nil {
|
||||||
|
go b.callbacks.OnStationUpdated(station)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for disconnected stations
|
||||||
|
for mac := range b.lastStations {
|
||||||
|
if _, exists := currentMap[mac]; !exists {
|
||||||
|
// Station disconnected
|
||||||
|
if b.callbacks.OnStationDisconnected != nil {
|
||||||
|
go b.callbacks.OnStationDisconnected(mac)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update last state
|
||||||
|
b.lastStations = currentMap
|
||||||
|
}
|
||||||
64
internal/station/arp/parser.go
Normal file
64
internal/station/arp/parser.go
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
package arp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
99
internal/station/backend/types.go
Normal file
99
internal/station/backend/types.go
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StationBackend defines the interface for station/device discovery backends.
|
||||||
|
// Implementations include ARP-based, DHCP-based, and Hostapd DBus-based discovery.
|
||||||
|
type StationBackend interface {
|
||||||
|
// Initialize initializes the backend with the given configuration
|
||||||
|
Initialize(config BackendConfig) error
|
||||||
|
|
||||||
|
// Close cleans up backend resources
|
||||||
|
Close() error
|
||||||
|
|
||||||
|
// GetStations returns all currently connected stations
|
||||||
|
GetStations() ([]Station, error)
|
||||||
|
|
||||||
|
// StartEventMonitoring starts monitoring for station events
|
||||||
|
// Backends that don't support real-time events will poll and simulate events
|
||||||
|
StartEventMonitoring(callbacks EventCallbacks) error
|
||||||
|
|
||||||
|
// StopEventMonitoring stops event monitoring
|
||||||
|
StopEventMonitoring()
|
||||||
|
|
||||||
|
// SupportsRealTimeEvents returns true if backend supports real-time events (e.g., DBus)
|
||||||
|
// Returns false for polling-based backends (ARP, DHCP)
|
||||||
|
SupportsRealTimeEvents() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Station represents a connected device in a backend-agnostic format
|
||||||
|
type Station struct {
|
||||||
|
MAC string // Hardware MAC address (required, primary identifier)
|
||||||
|
IP string // IP address (may be empty for some backends initially)
|
||||||
|
Hostname string // Device hostname (may be empty)
|
||||||
|
Type string // Device type: "mobile", "laptop", "tablet", "unknown"
|
||||||
|
Signal int32 // Signal strength in dBm (0 if not available)
|
||||||
|
RxBytes uint64 // Received bytes (0 if not available)
|
||||||
|
TxBytes uint64 // Transmitted bytes (0 if not available)
|
||||||
|
ConnectedAt time.Time // When station connected (best effort)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EventCallbacks defines callback functions for station events.
|
||||||
|
// Backends call these when stations connect, disconnect, or update.
|
||||||
|
type EventCallbacks struct {
|
||||||
|
// OnStationConnected is called when a new station connects
|
||||||
|
OnStationConnected func(station Station)
|
||||||
|
|
||||||
|
// OnStationDisconnected is called when a station disconnects
|
||||||
|
OnStationDisconnected func(mac string)
|
||||||
|
|
||||||
|
// OnStationUpdated is called when station information changes
|
||||||
|
// (e.g., IP discovered, signal strength changed)
|
||||||
|
OnStationUpdated func(station Station)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BackendConfig provides configuration for backend initialization
|
||||||
|
type BackendConfig struct {
|
||||||
|
// Common
|
||||||
|
InterfaceName string // Network interface (e.g., "wlan1")
|
||||||
|
|
||||||
|
// ARP-specific
|
||||||
|
ARPTablePath string // Path to /proc/net/arp (default: "/proc/net/arp")
|
||||||
|
|
||||||
|
// DHCP-specific
|
||||||
|
DHCPLeasesPath string // Path to DHCP leases file
|
||||||
|
|
||||||
|
// Hostapd-specific
|
||||||
|
HostapdInterface string // Hostapd interface name for DBus
|
||||||
|
}
|
||||||
|
|
||||||
|
// GuessDeviceType attempts to guess device type from hostname and MAC address
|
||||||
|
func GuessDeviceType(hostname, mac string) string {
|
||||||
|
hostname = strings.ToLower(hostname)
|
||||||
|
|
||||||
|
if strings.Contains(hostname, "iphone") || strings.Contains(hostname, "android") {
|
||||||
|
return "mobile"
|
||||||
|
} else if strings.Contains(hostname, "ipad") || strings.Contains(hostname, "tablet") {
|
||||||
|
return "tablet"
|
||||||
|
} else if strings.Contains(hostname, "macbook") || strings.Contains(hostname, "laptop") {
|
||||||
|
return "laptop"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guess by MAC prefix (OUI)
|
||||||
|
if len(mac) >= 8 {
|
||||||
|
macPrefix := strings.ToUpper(mac[:8])
|
||||||
|
switch macPrefix {
|
||||||
|
case "00:50:56", "00:0C:29", "00:05:69": // VMware
|
||||||
|
return "laptop"
|
||||||
|
case "08:00:27": // VirtualBox
|
||||||
|
return "laptop"
|
||||||
|
default:
|
||||||
|
return "mobile"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
184
internal/station/dhcp/backend.go
Normal file
184
internal/station/dhcp/backend.go
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
package dhcp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nemunaire/repeater/internal/station/backend"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Backend implements StationBackend using DHCP lease discovery
|
||||||
|
type Backend struct {
|
||||||
|
dhcpLeasesPath string
|
||||||
|
lastStations map[string]backend.Station // Key: MAC address
|
||||||
|
callbacks backend.EventCallbacks
|
||||||
|
stopChan chan struct{}
|
||||||
|
mu sync.RWMutex
|
||||||
|
running bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBackend creates a new DHCP backend
|
||||||
|
func NewBackend() *Backend {
|
||||||
|
return &Backend{
|
||||||
|
lastStations: make(map[string]backend.Station),
|
||||||
|
stopChan: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize initializes the DHCP backend
|
||||||
|
func (b *Backend) Initialize(config backend.BackendConfig) error {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
b.dhcpLeasesPath = config.DHCPLeasesPath
|
||||||
|
if b.dhcpLeasesPath == "" {
|
||||||
|
b.dhcpLeasesPath = "/var/lib/dhcp/dhcpd.leases"
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close cleans up backend resources
|
||||||
|
func (b *Backend) Close() error {
|
||||||
|
b.StopEventMonitoring()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStations returns all connected stations from DHCP leases validated by ARP
|
||||||
|
func (b *Backend) GetStations() ([]backend.Station, error) {
|
||||||
|
b.mu.RLock()
|
||||||
|
dhcpLeasesPath := b.dhcpLeasesPath
|
||||||
|
b.mu.RUnlock()
|
||||||
|
|
||||||
|
// Read DHCP leases
|
||||||
|
leases, err := parseDHCPLeases(dhcpLeasesPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ARP information for validation
|
||||||
|
arpInfo, err := getARPInfo()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var stations []backend.Station
|
||||||
|
for _, lease := range leases {
|
||||||
|
// Check if the device is still connected via ARP
|
||||||
|
if _, exists := arpInfo[lease.IP]; exists {
|
||||||
|
st := backend.Station{
|
||||||
|
MAC: lease.MAC,
|
||||||
|
IP: lease.IP,
|
||||||
|
Hostname: lease.Hostname,
|
||||||
|
Type: backend.GuessDeviceType(lease.Hostname, lease.MAC),
|
||||||
|
Signal: 0, // Not available from DHCP
|
||||||
|
RxBytes: 0, // Not available from DHCP
|
||||||
|
TxBytes: 0, // Not available from DHCP
|
||||||
|
ConnectedAt: time.Now(),
|
||||||
|
}
|
||||||
|
stations = append(stations, st)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stations, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartEventMonitoring starts monitoring for station events via polling
|
||||||
|
func (b *Backend) StartEventMonitoring(callbacks backend.EventCallbacks) error {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
if b.running {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b.callbacks = callbacks
|
||||||
|
b.running = true
|
||||||
|
|
||||||
|
// Start polling goroutine
|
||||||
|
go b.pollLoop()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopEventMonitoring stops event monitoring
|
||||||
|
func (b *Backend) StopEventMonitoring() {
|
||||||
|
b.mu.Lock()
|
||||||
|
if !b.running {
|
||||||
|
b.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.running = false
|
||||||
|
b.mu.Unlock()
|
||||||
|
|
||||||
|
close(b.stopChan)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportsRealTimeEvents returns false (DHCP is polling-based)
|
||||||
|
func (b *Backend) SupportsRealTimeEvents() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// pollLoop polls DHCP leases and simulates events
|
||||||
|
func (b *Backend) pollLoop() {
|
||||||
|
ticker := time.NewTicker(10 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
b.checkForChanges()
|
||||||
|
case <-b.stopChan:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkForChanges compares current state with last state and triggers callbacks
|
||||||
|
func (b *Backend) checkForChanges() {
|
||||||
|
// Get current stations
|
||||||
|
current, err := b.GetStations()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build map of current stations
|
||||||
|
currentMap := make(map[string]backend.Station)
|
||||||
|
for _, st := range current {
|
||||||
|
currentMap[st.MAC] = st
|
||||||
|
}
|
||||||
|
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
// Check for new stations (connected)
|
||||||
|
for mac, st := range currentMap {
|
||||||
|
if _, exists := b.lastStations[mac]; !exists {
|
||||||
|
// New station connected
|
||||||
|
if b.callbacks.OnStationConnected != nil {
|
||||||
|
go b.callbacks.OnStationConnected(st)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Check for updates (IP change, hostname change, etc.)
|
||||||
|
oldStation := b.lastStations[mac]
|
||||||
|
if oldStation.IP != st.IP || oldStation.Hostname != st.Hostname {
|
||||||
|
if b.callbacks.OnStationUpdated != nil {
|
||||||
|
go b.callbacks.OnStationUpdated(st)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for disconnected stations
|
||||||
|
for mac := range b.lastStations {
|
||||||
|
if _, exists := currentMap[mac]; !exists {
|
||||||
|
// Station disconnected
|
||||||
|
if b.callbacks.OnStationDisconnected != nil {
|
||||||
|
go b.callbacks.OnStationDisconnected(mac)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update last state
|
||||||
|
b.lastStations = currentMap
|
||||||
|
}
|
||||||
72
internal/station/dhcp/parser.go
Normal file
72
internal/station/dhcp/parser.go
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
package dhcp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/nemunaire/repeater/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getARPInfo retrieves ARP table information using arp command
|
||||||
|
// Returns a map of IP -> MAC address
|
||||||
|
func getARPInfo() (map[string]string, error) {
|
||||||
|
arpInfo := make(map[string]string)
|
||||||
|
|
||||||
|
cmd := exec.Command("arp", "-a")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return arpInfo, err
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(string(output), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if matches := regexp.MustCompile(`\(([^)]+)\) at ([0-9a-fA-F:]{17})`).FindStringSubmatch(line); len(matches) > 2 {
|
||||||
|
ip := matches[1]
|
||||||
|
mac := matches[2]
|
||||||
|
arpInfo[ip] = mac
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return arpInfo, nil
|
||||||
|
}
|
||||||
24
internal/station/factory.go
Normal file
24
internal/station/factory.go
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
package station
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/nemunaire/repeater/internal/station/arp"
|
||||||
|
"github.com/nemunaire/repeater/internal/station/backend"
|
||||||
|
"github.com/nemunaire/repeater/internal/station/dhcp"
|
||||||
|
"github.com/nemunaire/repeater/internal/station/hostapd"
|
||||||
|
)
|
||||||
|
|
||||||
|
// createBackend creates a station backend based on the backend name
|
||||||
|
func createBackend(backendName string) (backend.StationBackend, error) {
|
||||||
|
switch backendName {
|
||||||
|
case "arp":
|
||||||
|
return arp.NewBackend(), nil
|
||||||
|
case "dhcp":
|
||||||
|
return dhcp.NewBackend(), nil
|
||||||
|
case "hostapd":
|
||||||
|
return hostapd.NewBackend(), nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid station backend: %s (must be 'arp', 'dhcp', or 'hostapd')", backendName)
|
||||||
|
}
|
||||||
|
}
|
||||||
345
internal/station/hostapd/backend.go
Normal file
345
internal/station/hostapd/backend.go
Normal file
|
|
@ -0,0 +1,345 @@
|
||||||
|
package hostapd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nemunaire/repeater/internal/station/backend"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Backend implements StationBackend using hostapd_cli
|
||||||
|
type Backend struct {
|
||||||
|
interfaceName string
|
||||||
|
hostapdCLI string // Path to hostapd_cli executable
|
||||||
|
|
||||||
|
stations map[string]*HostapdStation // Key: MAC address
|
||||||
|
callbacks backend.EventCallbacks
|
||||||
|
|
||||||
|
mu sync.RWMutex
|
||||||
|
running bool
|
||||||
|
stopCh chan struct{}
|
||||||
|
|
||||||
|
// IP correlation - will be populated by periodic DHCP lease correlation
|
||||||
|
ipByMAC map[string]string // MAC -> IP mapping
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBackend creates a new hostapd backend
|
||||||
|
func NewBackend() *Backend {
|
||||||
|
return &Backend{
|
||||||
|
stations: make(map[string]*HostapdStation),
|
||||||
|
ipByMAC: make(map[string]string),
|
||||||
|
hostapdCLI: "hostapd_cli",
|
||||||
|
stopCh: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize initializes the hostapd backend
|
||||||
|
func (b *Backend) Initialize(config backend.BackendConfig) error {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
b.interfaceName = config.InterfaceName
|
||||||
|
if b.interfaceName == "" {
|
||||||
|
b.interfaceName = "wlan1" // Default AP interface
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if hostapd_cli is available
|
||||||
|
if _, err := exec.LookPath(b.hostapdCLI); err != nil {
|
||||||
|
return fmt.Errorf("hostapd_cli not found in PATH: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify we can communicate with hostapd
|
||||||
|
if err := b.runCommand("ping"); err != nil {
|
||||||
|
return fmt.Errorf("failed to communicate with hostapd on interface %s: %w", b.interfaceName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Hostapd backend initialized for interface %s", b.interfaceName)
|
||||||
|
|
||||||
|
// Load initial station list
|
||||||
|
if err := b.loadStations(); err != nil {
|
||||||
|
log.Printf("Warning: Failed to load initial stations: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close cleans up backend resources
|
||||||
|
func (b *Backend) Close() error {
|
||||||
|
b.StopEventMonitoring()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// runCommand executes a hostapd_cli command and returns the output
|
||||||
|
func (b *Backend) runCommand(args ...string) error {
|
||||||
|
cmdArgs := []string{"-i", b.interfaceName}
|
||||||
|
cmdArgs = append(cmdArgs, args...)
|
||||||
|
cmd := exec.Command(b.hostapdCLI, cmdArgs...)
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// runCommandOutput executes a hostapd_cli command and returns the output
|
||||||
|
func (b *Backend) runCommandOutput(args ...string) (string, error) {
|
||||||
|
cmdArgs := []string{"-i", b.interfaceName}
|
||||||
|
cmdArgs = append(cmdArgs, args...)
|
||||||
|
cmd := exec.Command(b.hostapdCLI, cmdArgs...)
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(out), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStations returns all connected stations
|
||||||
|
func (b *Backend) GetStations() ([]backend.Station, error) {
|
||||||
|
b.mu.RLock()
|
||||||
|
defer b.mu.RUnlock()
|
||||||
|
|
||||||
|
stations := make([]backend.Station, 0, len(b.stations))
|
||||||
|
for mac, hs := range b.stations {
|
||||||
|
station := b.convertStation(mac, hs)
|
||||||
|
stations = append(stations, station)
|
||||||
|
}
|
||||||
|
|
||||||
|
return stations, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartEventMonitoring starts monitoring for station events via polling
|
||||||
|
func (b *Backend) StartEventMonitoring(callbacks backend.EventCallbacks) error {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
if b.running {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b.callbacks = callbacks
|
||||||
|
b.running = true
|
||||||
|
|
||||||
|
// Start polling goroutine
|
||||||
|
go b.pollStations()
|
||||||
|
|
||||||
|
log.Printf("Hostapd event monitoring started (polling mode)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopEventMonitoring stops event monitoring
|
||||||
|
func (b *Backend) StopEventMonitoring() {
|
||||||
|
b.mu.Lock()
|
||||||
|
if !b.running {
|
||||||
|
b.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.running = false
|
||||||
|
b.mu.Unlock()
|
||||||
|
|
||||||
|
close(b.stopCh)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportsRealTimeEvents returns false (hostapd_cli uses polling)
|
||||||
|
func (b *Backend) SupportsRealTimeEvents() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// pollStations periodically polls for station changes
|
||||||
|
func (b *Backend) pollStations() {
|
||||||
|
ticker := time.NewTicker(2 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-b.stopCh:
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
if err := b.checkStationChanges(); err != nil {
|
||||||
|
log.Printf("Error polling stations: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkStationChanges checks for station connect/disconnect events
|
||||||
|
func (b *Backend) checkStationChanges() error {
|
||||||
|
// Get current stations from hostapd
|
||||||
|
currentStations, err := b.fetchStations()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
// Build a map of current MACs
|
||||||
|
currentMACs := make(map[string]bool)
|
||||||
|
for mac := range currentStations {
|
||||||
|
currentMACs[mac] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for new stations
|
||||||
|
for mac, station := range currentStations {
|
||||||
|
if _, exists := b.stations[mac]; !exists {
|
||||||
|
// New station connected
|
||||||
|
b.stations[mac] = station
|
||||||
|
if b.callbacks.OnStationConnected != nil {
|
||||||
|
st := b.convertStation(mac, station)
|
||||||
|
go b.callbacks.OnStationConnected(st)
|
||||||
|
}
|
||||||
|
log.Printf("Station connected: %s", mac)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for removed stations
|
||||||
|
for mac := range b.stations {
|
||||||
|
if !currentMACs[mac] {
|
||||||
|
// Station disconnected
|
||||||
|
delete(b.stations, mac)
|
||||||
|
delete(b.ipByMAC, mac)
|
||||||
|
if b.callbacks.OnStationDisconnected != nil {
|
||||||
|
go b.callbacks.OnStationDisconnected(mac)
|
||||||
|
}
|
||||||
|
log.Printf("Station disconnected: %s", mac)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadStations loads the initial list of stations from hostapd
|
||||||
|
func (b *Backend) loadStations() error {
|
||||||
|
stations, err := b.fetchStations()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
b.stations = stations
|
||||||
|
log.Printf("Loaded %d initial stations from hostapd", len(b.stations))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchStations fetches all stations using hostapd_cli all_sta command
|
||||||
|
func (b *Backend) fetchStations() (map[string]*HostapdStation, error) {
|
||||||
|
output, err := b.runCommandOutput("all_sta")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get stations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.parseAllStaOutput(output), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseAllStaOutput parses the output of "hostapd_cli all_sta"
|
||||||
|
func (b *Backend) parseAllStaOutput(output string) map[string]*HostapdStation {
|
||||||
|
stations := make(map[string]*HostapdStation)
|
||||||
|
scanner := bufio.NewScanner(bytes.NewBufferString(output))
|
||||||
|
|
||||||
|
var currentMAC string
|
||||||
|
var currentStation *HostapdStation
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a MAC address line (starts the station block)
|
||||||
|
if !strings.Contains(line, "=") && len(line) == 17 && strings.Count(line, ":") == 5 {
|
||||||
|
// Save previous station if exists
|
||||||
|
if currentMAC != "" && currentStation != nil {
|
||||||
|
stations[currentMAC] = currentStation
|
||||||
|
}
|
||||||
|
// Start new station
|
||||||
|
currentMAC = strings.ToLower(line)
|
||||||
|
currentStation = &HostapdStation{}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse key=value pairs
|
||||||
|
if currentStation != nil && strings.Contains(line, "=") {
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := strings.TrimSpace(parts[0])
|
||||||
|
value := strings.TrimSpace(parts[1])
|
||||||
|
|
||||||
|
switch key {
|
||||||
|
case "signal":
|
||||||
|
if v, err := strconv.Atoi(value); err == nil {
|
||||||
|
currentStation.Signal = int32(v)
|
||||||
|
}
|
||||||
|
case "rx_bytes":
|
||||||
|
if v, err := strconv.ParseUint(value, 10, 64); err == nil {
|
||||||
|
currentStation.RxBytes = v
|
||||||
|
}
|
||||||
|
case "tx_bytes":
|
||||||
|
if v, err := strconv.ParseUint(value, 10, 64); err == nil {
|
||||||
|
currentStation.TxBytes = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save last station
|
||||||
|
if currentMAC != "" && currentStation != nil {
|
||||||
|
stations[currentMAC] = currentStation
|
||||||
|
}
|
||||||
|
|
||||||
|
return stations
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertStation converts HostapdStation to backend.Station
|
||||||
|
func (b *Backend) convertStation(mac string, hs *HostapdStation) backend.Station {
|
||||||
|
// Get IP address if available from correlation
|
||||||
|
ip := b.ipByMAC[mac]
|
||||||
|
|
||||||
|
// Attempt hostname resolution if we have an IP
|
||||||
|
hostname := ""
|
||||||
|
// TODO: Could do reverse DNS lookup here if needed
|
||||||
|
|
||||||
|
return backend.Station{
|
||||||
|
MAC: mac,
|
||||||
|
IP: ip,
|
||||||
|
Hostname: hostname,
|
||||||
|
Type: backend.GuessDeviceType(hostname, mac),
|
||||||
|
Signal: hs.Signal,
|
||||||
|
RxBytes: hs.RxBytes,
|
||||||
|
TxBytes: hs.TxBytes,
|
||||||
|
ConnectedAt: time.Now(), // We don't have exact connection time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateIPMapping updates the MAC -> IP mapping from external source (e.g., DHCP)
|
||||||
|
// This should be called periodically to correlate hostapd stations with IP addresses
|
||||||
|
func (b *Backend) UpdateIPMapping(macToIP map[string]string) {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
// Track which stations got IP updates
|
||||||
|
updated := make(map[string]bool)
|
||||||
|
|
||||||
|
for mac, ip := range macToIP {
|
||||||
|
if oldIP, exists := b.ipByMAC[mac]; exists && oldIP != ip {
|
||||||
|
// IP changed
|
||||||
|
updated[mac] = true
|
||||||
|
} else if !exists {
|
||||||
|
// New IP mapping
|
||||||
|
updated[mac] = true
|
||||||
|
}
|
||||||
|
b.ipByMAC[mac] = ip
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger update callbacks for stations that got new/changed IPs
|
||||||
|
for mac := range updated {
|
||||||
|
if station, exists := b.stations[mac]; exists {
|
||||||
|
if b.callbacks.OnStationUpdated != nil {
|
||||||
|
st := b.convertStation(mac, station)
|
||||||
|
go b.callbacks.OnStationUpdated(st)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
130
internal/station/hostapd/correlation.go
Normal file
130
internal/station/hostapd/correlation.go
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
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 mapping
|
||||||
|
macToIP := make(map[string]string)
|
||||||
|
for _, lease := range leases {
|
||||||
|
macToIP[lease.MAC] = lease.IP
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update backend with IP mappings
|
||||||
|
dc.backend.UpdateIPMapping(macToIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
10
internal/station/hostapd/types.go
Normal file
10
internal/station/hostapd/types.go
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
package hostapd
|
||||||
|
|
||||||
|
// HostapdStation represents station properties from hostapd_cli
|
||||||
|
type HostapdStation struct {
|
||||||
|
RxPackets uint64
|
||||||
|
TxPackets uint64
|
||||||
|
RxBytes uint64
|
||||||
|
TxBytes uint64
|
||||||
|
Signal int32 // Signal in dBm
|
||||||
|
}
|
||||||
111
internal/station/station.go
Normal file
111
internal/station/station.go
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
package station
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/nemunaire/repeater/internal/models"
|
||||||
|
"github.com/nemunaire/repeater/internal/station/backend"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
currentBackend backend.StationBackend
|
||||||
|
mu sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
// Initialize initializes the station discovery backend
|
||||||
|
func Initialize(backendName string, config backend.BackendConfig) error {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
// Close existing backend if any
|
||||||
|
if currentBackend != nil {
|
||||||
|
currentBackend.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new backend
|
||||||
|
b, err := createBackend(backendName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the backend
|
||||||
|
if err := b.Initialize(config); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
currentBackend = b
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStations returns all connected stations as ConnectedDevice models
|
||||||
|
func GetStations() ([]models.ConnectedDevice, error) {
|
||||||
|
mu.RLock()
|
||||||
|
defer mu.RUnlock()
|
||||||
|
|
||||||
|
if currentBackend == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
stations, err := currentBackend.GetStations()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert backend.Station to models.ConnectedDevice
|
||||||
|
devices := make([]models.ConnectedDevice, len(stations))
|
||||||
|
for i, s := range stations {
|
||||||
|
devices[i] = models.ConnectedDevice{
|
||||||
|
Name: s.Hostname,
|
||||||
|
Type: s.Type,
|
||||||
|
MAC: s.MAC,
|
||||||
|
IP: s.IP,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return devices, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartEventMonitoring starts monitoring for station events
|
||||||
|
func StartEventMonitoring(callbacks backend.EventCallbacks) error {
|
||||||
|
mu.RLock()
|
||||||
|
defer mu.RUnlock()
|
||||||
|
|
||||||
|
if currentBackend == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentBackend.StartEventMonitoring(callbacks)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopEventMonitoring stops monitoring for station events
|
||||||
|
func StopEventMonitoring() {
|
||||||
|
mu.RLock()
|
||||||
|
defer mu.RUnlock()
|
||||||
|
|
||||||
|
if currentBackend != nil {
|
||||||
|
currentBackend.StopEventMonitoring()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the current backend
|
||||||
|
func Close() {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
if currentBackend != nil {
|
||||||
|
currentBackend.Close()
|
||||||
|
currentBackend = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportsRealTimeEvents returns true if the current backend supports real-time events
|
||||||
|
func SupportsRealTimeEvents() bool {
|
||||||
|
mu.RLock()
|
||||||
|
defer mu.RUnlock()
|
||||||
|
|
||||||
|
if currentBackend == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentBackend.SupportsRealTimeEvents()
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue