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.
98 lines
2.4 KiB
Go
98 lines
2.4 KiB
Go
package backend
|
|
|
|
import (
|
|
_ "embed"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// IEEE OUI registry as a compact text file: each line is 6 hex characters
|
|
// (uppercase, no separators) followed immediately by the organization name.
|
|
// Sourced from https://standards-oui.ieee.org/oui/oui.csv and pre-processed
|
|
// to strip the address column and common corporate suffixes (Inc, Ltd, ...).
|
|
//
|
|
//go:embed oui_data.txt
|
|
var ouiData string
|
|
|
|
var (
|
|
ouiOnce sync.Once
|
|
ouiTable map[string]string
|
|
)
|
|
|
|
func loadOUITable() {
|
|
ouiTable = make(map[string]string, 40000)
|
|
for line := range strings.SplitSeq(ouiData, "\n") {
|
|
if len(line) < 7 {
|
|
continue
|
|
}
|
|
ouiTable[line[:6]] = line[6:]
|
|
}
|
|
}
|
|
|
|
// LookupVendor returns the organization assigned to the OUI prefix of mac,
|
|
// or an empty string when the prefix is unknown or mac is malformed. Locally
|
|
// administered MACs (the U/L bit is set) are explicitly skipped — they are
|
|
// randomized by the device, not assigned to a vendor, so any match would be
|
|
// a false positive.
|
|
func LookupVendor(mac string) string {
|
|
ouiOnce.Do(loadOUITable)
|
|
|
|
prefix := normalizeOUI(mac)
|
|
if prefix == "" {
|
|
return ""
|
|
}
|
|
return ouiTable[prefix]
|
|
}
|
|
|
|
// normalizeOUI extracts the 6-hex-char OUI from a MAC address (any of
|
|
// "aa:bb:cc:...", "aa-bb-cc-...", "aabb.ccdd.eeff", "aabbcc...") and returns
|
|
// it uppercased. Returns "" for randomized MACs (locally administered bit
|
|
// set) or anything that doesn't parse.
|
|
func normalizeOUI(mac string) string {
|
|
var hex strings.Builder
|
|
hex.Grow(12)
|
|
for i := 0; i < len(mac) && hex.Len() < 6; i++ {
|
|
c := mac[i]
|
|
switch {
|
|
case c >= '0' && c <= '9', c >= 'A' && c <= 'F':
|
|
hex.WriteByte(c)
|
|
case c >= 'a' && c <= 'f':
|
|
hex.WriteByte(c - 32)
|
|
}
|
|
}
|
|
if hex.Len() < 6 {
|
|
return ""
|
|
}
|
|
out := hex.String()
|
|
|
|
// Skip locally administered MACs. The U/L bit is bit 1 of the first
|
|
// octet — when set, the MAC was generated by the device (private
|
|
// address randomization on iOS/Android, virtual interfaces, ...), not
|
|
// assigned by IEEE, so OUI lookup is meaningless.
|
|
first, ok := hexByte(out[0], out[1])
|
|
if !ok || first&0x02 != 0 {
|
|
return ""
|
|
}
|
|
return out
|
|
}
|
|
|
|
func hexByte(hi, lo byte) (byte, bool) {
|
|
h, ok1 := hexNibble(hi)
|
|
l, ok2 := hexNibble(lo)
|
|
if !ok1 || !ok2 {
|
|
return 0, false
|
|
}
|
|
return h<<4 | l, true
|
|
}
|
|
|
|
func hexNibble(c byte) (byte, bool) {
|
|
switch {
|
|
case c >= '0' && c <= '9':
|
|
return c - '0', true
|
|
case c >= 'A' && c <= 'F':
|
|
return c - 'A' + 10, true
|
|
case c >= 'a' && c <= 'f':
|
|
return c - 'a' + 10, true
|
|
}
|
|
return 0, false
|
|
}
|