package device import ( "bufio" "os" "os/exec" "regexp" "strings" "github.com/nemunaire/repeater/internal/models" ) // GetConnectedDevices returns a list of connected devices func GetConnectedDevices() ([]models.ConnectedDevice, error) { var devices []models.ConnectedDevice // Read DHCP leases leases, err := parseDHCPLeases() if err != nil { return devices, err } // Get ARP information 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 } // parseDHCPLeases reads and parses DHCP lease file func parseDHCPLeases() ([]models.DHCPLease, error) { var leases []models.DHCPLease file, err := os.Open("/var/lib/dhcp/dhcpd.leases") 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" }