repeater/internal/station/backend/oui.go
Pierre-Olivier Mercier 70140bc289 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.
2026-05-01 22:32:57 +08:00

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
}