station: Identify devices by MAC OUI vendor lookup
Embed the IEEE OUI registry (~1MB pre-processed text file) and resolve the vendor for every station MAC. Locally administered MACs (U/L bit set, used by iOS/Android private addresses and virtual interfaces) are skipped so we don't return spurious matches against randomized prefixes. The vendor name shows up in the device card as a secondary line, and falls back to the title position when no DHCP hostname is available — "Apple" with the IP and MAC is far more useful than "Sans nom". The lookup table loads lazily (sync.Once) on the first call so the ~40k-entry parse only runs when the station discovery code is exercised.
This commit is contained in:
parent
950f73371c
commit
70140bc289
10 changed files with 39518 additions and 3 deletions
57
internal/station/backend/oui_test.go
Normal file
57
internal/station/backend/oui_test.go
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
package backend
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestLookupVendor(t *testing.T) {
|
||||
cases := []struct {
|
||||
mac string
|
||||
want string
|
||||
exact bool // when true, want must match exactly; otherwise non-empty is enough
|
||||
comment string
|
||||
}{
|
||||
{"00:1c:b3:11:22:33", "Apple", true, "Apple OUI"},
|
||||
{"f4:0f:24:11:22:33", "Apple", true, "Apple OUI uppercase"},
|
||||
{"B8:27:EB:AA:BB:CC", "", false, "Raspberry Pi Foundation"},
|
||||
{"dc:a6:32:11:22:33", "", false, "Raspberry Pi Trading"},
|
||||
{"02:11:22:33:44:55", "", true, "locally administered (U/L bit set)"},
|
||||
{"aa:bb:cc:dd:ee:ff", "", true, "locally administered randomized"},
|
||||
{"", "", true, "empty mac"},
|
||||
{"not a mac", "", true, "garbage"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
got := LookupVendor(tc.mac)
|
||||
if tc.exact {
|
||||
if got != tc.want {
|
||||
t.Errorf("%s: LookupVendor(%q) = %q, want %q", tc.comment, tc.mac, got, tc.want)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if got == "" {
|
||||
t.Errorf("%s: LookupVendor(%q) returned empty, expected non-empty", tc.comment, tc.mac)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeOUI(t *testing.T) {
|
||||
cases := []struct {
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{"00:1c:b3:11:22:33", "001CB3"},
|
||||
{"00-1c-b3-11-22-33", "001CB3"},
|
||||
{"001c.b311.2233", "001CB3"},
|
||||
{"001cb3112233", "001CB3"},
|
||||
{"02:11:22:33:44:55", ""}, // U/L bit set
|
||||
{"03:11:22:33:44:55", ""}, // U/L bit set
|
||||
{"06:11:22:33:44:55", ""}, // U/L bit set
|
||||
{"01:00:5e:00:00:00", "01005E"}, // bit 0 (multicast) is fine, only bit 1 matters
|
||||
{"abcd", ""}, // too short
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
if got := normalizeOUI(tc.in); got != tc.want {
|
||||
t.Errorf("normalizeOUI(%q) = %q, want %q", tc.in, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue