Add GET /api/v1/probe endpoint for device inspection
Fast (~1-3s) endpoint that gathers network info about a device before full stream discovery. Runs ping first, then parallel probes. Features: - Ping with ICMP + TCP fallback (works without root) - Reverse DNS hostname lookup - ARP table MAC address + OUI vendor identification (2403 entries, 51 camera vendors) - mDNS HomeKit detection (camera/doorbell, paired status) - Extensible Prober interface for adding new probe types - 3-second overall timeout, parallel execution Response includes "type" field: - "unreachable" - device not responding - "standard" - normal IP camera (RTSP/HTTP/ONVIF flow) - "homekit" - Apple HomeKit camera (PIN pairing flow)
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/eduard256/Strix/internal/models"
|
||||
)
|
||||
|
||||
// ARPProber looks up the MAC address from the system ARP table
|
||||
// and resolves it to a vendor name using the OUI database.
|
||||
type ARPProber struct {
|
||||
ouiDB *OUIDatabase
|
||||
}
|
||||
|
||||
// NewARPProber creates a new ARP prober with the given OUI database.
|
||||
func NewARPProber(ouiDB *OUIDatabase) *ARPProber {
|
||||
return &ARPProber{ouiDB: ouiDB}
|
||||
}
|
||||
|
||||
func (p *ARPProber) Name() string { return "arp" }
|
||||
|
||||
// Probe looks up the MAC address for the given IP in the ARP table.
|
||||
// Returns nil if the IP is not in the ARP table (e.g., different subnet, VPN).
|
||||
// This only works on Linux (reads /proc/net/arp).
|
||||
func (p *ARPProber) Probe(ctx context.Context, ip string) (any, error) {
|
||||
mac, err := p.lookupARP(ip)
|
||||
if err != nil || mac == "" {
|
||||
return nil, nil // Not in ARP table is not an error
|
||||
}
|
||||
|
||||
vendor := ""
|
||||
if p.ouiDB != nil {
|
||||
vendor = p.ouiDB.LookupVendor(mac)
|
||||
}
|
||||
|
||||
return &models.ARPProbeResult{
|
||||
MAC: mac,
|
||||
Vendor: vendor,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// lookupARP reads /proc/net/arp to find the MAC address for the given IP.
|
||||
//
|
||||
// Format of /proc/net/arp:
|
||||
//
|
||||
// IP address HW type Flags HW address Mask Device
|
||||
// 192.168.1.1 0x1 0x2 aa:bb:cc:dd:ee:ff * eth0
|
||||
func (p *ARPProber) lookupARP(ip string) (string, error) {
|
||||
file, err := os.Open("/proc/net/arp")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open ARP table: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
scanner.Scan() // Skip header line
|
||||
|
||||
for scanner.Scan() {
|
||||
fields := strings.Fields(scanner.Text())
|
||||
if len(fields) < 4 {
|
||||
continue
|
||||
}
|
||||
|
||||
// fields[0] = IP address, fields[3] = HW address
|
||||
if fields[0] == ip {
|
||||
mac := fields[3]
|
||||
// "00:00:00:00:00:00" means incomplete ARP entry
|
||||
if mac == "00:00:00:00:00:00" {
|
||||
return "", nil
|
||||
}
|
||||
return strings.ToUpper(mac), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
Reference in New Issue
Block a user