Track hosts by MAC and IP during scans
This commit is contained in:
parent
ddb9da18ac
commit
85a158d712
2 changed files with 220 additions and 12 deletions
|
|
@ -1,6 +1,8 @@
|
|||
package routines
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aceberg/WatchYourLAN/internal/arp"
|
||||
|
|
@ -28,14 +30,9 @@ func startScan(quit chan bool) {
|
|||
if nowDate.After(plusDate) {
|
||||
|
||||
foundHosts = arp.Scan(conf.AppConfig.Ifaces, conf.AppConfig.ArpArgs, conf.AppConfig.ArpStrs)
|
||||
foundHosts = check.EnrichHosts(foundHosts)
|
||||
|
||||
// Make map of found hosts
|
||||
foundHostsMap := make(map[string]models.Host)
|
||||
for _, fHost := range foundHosts {
|
||||
foundHostsMap[fHost.Mac] = fHost
|
||||
}
|
||||
|
||||
compareHosts(foundHostsMap)
|
||||
compareHosts(newFoundHostIndex(foundHosts))
|
||||
|
||||
lastDate = time.Now()
|
||||
}
|
||||
|
|
@ -45,24 +42,42 @@ func startScan(quit chan bool) {
|
|||
}
|
||||
}
|
||||
|
||||
func compareHosts(foundHostsMap map[string]models.Host) {
|
||||
func compareHosts(foundHosts *foundHostIndex) {
|
||||
|
||||
allHosts, ok := gdb.Select("now")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
existingMACCounts := make(map[string]int)
|
||||
for _, aHost := range allHosts {
|
||||
mac := normalizeMAC(aHost.Mac)
|
||||
if mac != "" {
|
||||
existingMACCounts[mac]++
|
||||
}
|
||||
}
|
||||
|
||||
for _, aHost := range allHosts {
|
||||
|
||||
fHost, exists := foundHostsMap[aHost.Mac]
|
||||
mac := normalizeMAC(aHost.Mac)
|
||||
fHost, exists := foundHosts.match(aHost, existingMACCounts[mac] == 1)
|
||||
if exists {
|
||||
|
||||
aHost.Iface = fHost.Iface
|
||||
aHost.IP = fHost.IP
|
||||
if aHost.Name == "" && fHost.Name != "" {
|
||||
aHost.Name = fHost.Name
|
||||
}
|
||||
if aHost.DNS == "" && fHost.DNS != "" {
|
||||
aHost.DNS = fHost.DNS
|
||||
}
|
||||
if isUnknownHardware(aHost.Hw) && fHost.Hw != "" {
|
||||
aHost.Hw = fHost.Hw
|
||||
}
|
||||
aHost.Date = fHost.Date
|
||||
aHost.Now = 1
|
||||
|
||||
delete(foundHostsMap, aHost.Mac)
|
||||
foundHosts.delete(fHost)
|
||||
|
||||
} else {
|
||||
aHost.Now = 0
|
||||
|
|
@ -81,11 +96,147 @@ func compareHosts(foundHostsMap map[string]models.Host) {
|
|||
}
|
||||
}
|
||||
|
||||
for _, fHost := range foundHostsMap {
|
||||
for _, fHost := range foundHosts.remaining() {
|
||||
|
||||
fHost.Name, fHost.DNS = check.DNS(fHost)
|
||||
if fHost.Name == "" || fHost.DNS == "" {
|
||||
fHost.Name, fHost.DNS = check.DNS(fHost)
|
||||
}
|
||||
notify.Unknown(fHost) // Log and Shoutrrr
|
||||
|
||||
gdb.Update("now", fHost)
|
||||
}
|
||||
}
|
||||
|
||||
type foundHostIndex struct {
|
||||
hosts map[string]models.Host
|
||||
keysByIP map[string][]string
|
||||
keysByMAC map[string][]string
|
||||
}
|
||||
|
||||
func newFoundHostIndex(hosts []models.Host) *foundHostIndex {
|
||||
index := &foundHostIndex{
|
||||
hosts: make(map[string]models.Host),
|
||||
keysByIP: make(map[string][]string),
|
||||
keysByMAC: make(map[string][]string),
|
||||
}
|
||||
|
||||
for _, host := range hosts {
|
||||
key := hostIdentityKey(host)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
index.hosts[key] = host
|
||||
|
||||
if host.IP != "" {
|
||||
index.keysByIP[host.IP] = appendUnique(index.keysByIP[host.IP], key)
|
||||
}
|
||||
|
||||
mac := normalizeMAC(host.Mac)
|
||||
if mac != "" {
|
||||
index.keysByMAC[mac] = appendUnique(index.keysByMAC[mac], key)
|
||||
}
|
||||
}
|
||||
|
||||
return index
|
||||
}
|
||||
|
||||
func (index *foundHostIndex) match(existing models.Host, allowMACFallback bool) (models.Host, bool) {
|
||||
key := hostIdentityKey(existing)
|
||||
if host, ok := index.hosts[key]; ok {
|
||||
return host, true
|
||||
}
|
||||
|
||||
if existing.IP != "" {
|
||||
if host, ok := index.only(index.keysByIP[existing.IP]); ok {
|
||||
return host, true
|
||||
}
|
||||
}
|
||||
|
||||
mac := normalizeMAC(existing.Mac)
|
||||
if allowMACFallback && mac != "" {
|
||||
if host, ok := index.only(index.keysByMAC[mac]); ok {
|
||||
return host, true
|
||||
}
|
||||
}
|
||||
|
||||
return models.Host{}, false
|
||||
}
|
||||
|
||||
func (index *foundHostIndex) only(keys []string) (models.Host, bool) {
|
||||
active := make([]string, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
if _, ok := index.hosts[key]; ok {
|
||||
active = append(active, key)
|
||||
}
|
||||
}
|
||||
|
||||
if len(active) != 1 {
|
||||
return models.Host{}, false
|
||||
}
|
||||
|
||||
host, ok := index.hosts[active[0]]
|
||||
return host, ok
|
||||
}
|
||||
|
||||
func (index *foundHostIndex) delete(host models.Host) {
|
||||
key := hostIdentityKey(host)
|
||||
delete(index.hosts, key)
|
||||
}
|
||||
|
||||
func (index *foundHostIndex) remaining() []models.Host {
|
||||
keys := make([]string, 0, len(index.hosts))
|
||||
for key := range index.hosts {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Slice(keys, func(i, j int) bool {
|
||||
return index.hosts[keys[i]].IP < index.hosts[keys[j]].IP
|
||||
})
|
||||
|
||||
hosts := make([]models.Host, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
hosts = append(hosts, index.hosts[key])
|
||||
}
|
||||
|
||||
return hosts
|
||||
}
|
||||
|
||||
func hostIdentityKey(host models.Host) string {
|
||||
ip := strings.TrimSpace(host.IP)
|
||||
mac := normalizeMAC(host.Mac)
|
||||
|
||||
if mac != "" && ip != "" {
|
||||
return mac + "|" + ip
|
||||
}
|
||||
if mac != "" {
|
||||
return mac
|
||||
}
|
||||
return ip
|
||||
}
|
||||
|
||||
func normalizeMAC(value string) string {
|
||||
return strings.ToLower(strings.TrimSpace(value))
|
||||
}
|
||||
|
||||
func appendUnique(items []string, more ...string) []string {
|
||||
seen := make(map[string]struct{}, len(items)+len(more))
|
||||
out := make([]string, 0, len(items)+len(more))
|
||||
|
||||
for _, item := range append(items, more...) {
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[item]; ok {
|
||||
continue
|
||||
}
|
||||
seen[item] = struct{}{}
|
||||
out = append(out, item)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func isUnknownHardware(value string) bool {
|
||||
value = strings.TrimSpace(value)
|
||||
return value == "" || strings.HasPrefix(value, "(Unknown")
|
||||
}
|
||||
|
|
|
|||
57
backend/internal/routines/scan-routine_test.go
Normal file
57
backend/internal/routines/scan-routine_test.go
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
package routines
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/aceberg/WatchYourLAN/internal/models"
|
||||
)
|
||||
|
||||
func TestFoundHostIndexKeepsSameMACDifferentIPs(t *testing.T) {
|
||||
index := newFoundHostIndex([]models.Host{
|
||||
{IP: "192.168.57.146", Mac: "48:b0:2d:eb:f5:15"},
|
||||
{IP: "192.168.57.209", Mac: "48:b0:2d:eb:f5:15"},
|
||||
})
|
||||
|
||||
host, ok := index.match(models.Host{IP: "192.168.57.209", Mac: "48:b0:2d:eb:f5:15"}, false)
|
||||
if !ok {
|
||||
t.Fatal("expected exact MAC+IP match")
|
||||
}
|
||||
if host.IP != "192.168.57.209" {
|
||||
t.Fatalf("matched wrong IP: %s", host.IP)
|
||||
}
|
||||
|
||||
index.delete(host)
|
||||
|
||||
remaining := index.remaining()
|
||||
if len(remaining) != 1 {
|
||||
t.Fatalf("expected one remaining host, got %d", len(remaining))
|
||||
}
|
||||
if remaining[0].IP != "192.168.57.146" {
|
||||
t.Fatalf("expected 192.168.57.146 to remain, got %s", remaining[0].IP)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFoundHostIndexUsesMACForSingleAddressChange(t *testing.T) {
|
||||
index := newFoundHostIndex([]models.Host{
|
||||
{IP: "192.168.57.210", Mac: "48:b0:2d:eb:f5:15"},
|
||||
})
|
||||
|
||||
host, ok := index.match(models.Host{IP: "192.168.57.209", Mac: "48:b0:2d:eb:f5:15"}, true)
|
||||
if !ok {
|
||||
t.Fatal("expected single MAC match for address change")
|
||||
}
|
||||
if host.IP != "192.168.57.210" {
|
||||
t.Fatalf("matched wrong IP: %s", host.IP)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFoundHostIndexDoesNotUseMACFallbackForKnownMultiIPHost(t *testing.T) {
|
||||
index := newFoundHostIndex([]models.Host{
|
||||
{IP: "192.168.57.146", Mac: "48:b0:2d:eb:f5:15"},
|
||||
})
|
||||
|
||||
_, ok := index.match(models.Host{IP: "192.168.57.209", Mac: "48:b0:2d:eb:f5:15"}, false)
|
||||
if ok {
|
||||
t.Fatal("did not expect MAC fallback when same MAC is already known on multiple IPs")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue