The owner asked to see the vendor of each MAC where the IEEE registry can be fetched, and never to depend on it. internal/oui downloads MA-L, MA-M and MA-S weekly through the outbound allow-list (timeouts, a size cap and a row-count sanity check, so a truncated or wrong download keeps the previous file), stores one checksummed, atomically written file in <data-dir>/oui that nmon backup includes, and looks MACs up by the longest prefix. A missing registry only leaves the vendor empty. `nmon oui import DIR|FILE` loads the CSVs offline, and the Ansible role can ship them next to the MIB sidecar. internal/hostaddr parses the MAC, prefix, IP and CIDR forms the lookup accepts and classifies a MAC as global, local or multicast. config gains the l2 and oui sections (validated here; the L2 job that reads l2 follows). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T4PdJUPrakM8fHvN8jYv2x
67 lines
1.8 KiB
Go
67 lines
1.8 KiB
Go
package hostaddr
|
|
|
|
import (
|
|
"errors"
|
|
"net/netip"
|
|
"strings"
|
|
)
|
|
|
|
// Query kinds.
|
|
const (
|
|
QueryMAC = "mac" // a complete MAC
|
|
QueryPrefix = "prefix" // at least 6 hex digits of one
|
|
QueryIP = "ip"
|
|
QueryCIDR = "cidr"
|
|
QueryVendor = "vendor" // vendor:<substring>
|
|
)
|
|
|
|
// ErrBadQuery means the text is none of a MAC, a MAC prefix, an IP, a CIDR or a vendor: query.
|
|
var ErrBadQuery = errors.New("not a MAC address, MAC prefix of 6 or more hex digits, IP address, CIDR or vendor:name")
|
|
|
|
// Query is a classified search string.
|
|
type Query struct {
|
|
Kind string
|
|
MAC string // canonical MAC or canonical prefix
|
|
IP netip.Addr
|
|
Prefix netip.Prefix
|
|
Vendor string // lower-case substring
|
|
}
|
|
|
|
// Classify decides what a search string is. Addresses are tried before MAC notations because an IPv6
|
|
// address is also made of hex digits and colons.
|
|
func Classify(q string) (Query, error) {
|
|
q = strings.TrimSpace(q)
|
|
if rest, ok := strings.CutPrefix(strings.ToLower(q), "vendor:"); ok {
|
|
rest = strings.TrimSpace(rest)
|
|
if rest == "" {
|
|
return Query{}, ErrBadQuery
|
|
}
|
|
return Query{Kind: QueryVendor, Vendor: rest}, nil
|
|
}
|
|
if strings.Contains(q, "/") {
|
|
p, err := netip.ParsePrefix(q)
|
|
if err != nil {
|
|
return Query{}, ErrBadQuery
|
|
}
|
|
return Query{Kind: QueryCIDR, Prefix: netip.PrefixFrom(p.Addr().WithZone("").Unmap(), unmapBits(p)).Masked()}, nil
|
|
}
|
|
if a, err := ParseIP(q); err == nil {
|
|
return Query{Kind: QueryIP, IP: a}, nil
|
|
}
|
|
if m, err := ParseMAC(q); err == nil {
|
|
return Query{Kind: QueryMAC, MAC: m}, nil
|
|
}
|
|
if p, err := ParsePrefix(q); err == nil {
|
|
return Query{Kind: QueryPrefix, MAC: p}, nil
|
|
}
|
|
return Query{}, ErrBadQuery
|
|
}
|
|
|
|
// unmapBits keeps the mask length meaningful when an IPv4-mapped IPv6 prefix is unmapped.
|
|
func unmapBits(p netip.Prefix) int {
|
|
if p.Addr().Is4In6() {
|
|
return max(p.Bits()-96, 0)
|
|
}
|
|
return p.Bits()
|
|
}
|