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
|
package routines
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/aceberg/WatchYourLAN/internal/arp"
|
"github.com/aceberg/WatchYourLAN/internal/arp"
|
||||||
|
|
@ -28,14 +30,9 @@ func startScan(quit chan bool) {
|
||||||
if nowDate.After(plusDate) {
|
if nowDate.After(plusDate) {
|
||||||
|
|
||||||
foundHosts = arp.Scan(conf.AppConfig.Ifaces, conf.AppConfig.ArpArgs, conf.AppConfig.ArpStrs)
|
foundHosts = arp.Scan(conf.AppConfig.Ifaces, conf.AppConfig.ArpArgs, conf.AppConfig.ArpStrs)
|
||||||
|
foundHosts = check.EnrichHosts(foundHosts)
|
||||||
|
|
||||||
// Make map of found hosts
|
compareHosts(newFoundHostIndex(foundHosts))
|
||||||
foundHostsMap := make(map[string]models.Host)
|
|
||||||
for _, fHost := range foundHosts {
|
|
||||||
foundHostsMap[fHost.Mac] = fHost
|
|
||||||
}
|
|
||||||
|
|
||||||
compareHosts(foundHostsMap)
|
|
||||||
|
|
||||||
lastDate = time.Now()
|
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")
|
allHosts, ok := gdb.Select("now")
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
existingMACCounts := make(map[string]int)
|
||||||
|
for _, aHost := range allHosts {
|
||||||
|
mac := normalizeMAC(aHost.Mac)
|
||||||
|
if mac != "" {
|
||||||
|
existingMACCounts[mac]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, aHost := range allHosts {
|
for _, aHost := range allHosts {
|
||||||
|
|
||||||
fHost, exists := foundHostsMap[aHost.Mac]
|
mac := normalizeMAC(aHost.Mac)
|
||||||
|
fHost, exists := foundHosts.match(aHost, existingMACCounts[mac] == 1)
|
||||||
if exists {
|
if exists {
|
||||||
|
|
||||||
aHost.Iface = fHost.Iface
|
aHost.Iface = fHost.Iface
|
||||||
aHost.IP = fHost.IP
|
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.Date = fHost.Date
|
||||||
aHost.Now = 1
|
aHost.Now = 1
|
||||||
|
|
||||||
delete(foundHostsMap, aHost.Mac)
|
foundHosts.delete(fHost)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
aHost.Now = 0
|
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
|
notify.Unknown(fHost) // Log and Shoutrrr
|
||||||
|
|
||||||
gdb.Update("now", fHost)
|
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