Files
nmon/internal/hostaddr/query.go
Vibe Coder c4d14a67d1 Add the optional IEEE vendor registry for MAC addresses and the l2/oui settings
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
2026-10-24 01:30:11 +00:00

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()
}