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
146 lines
4.7 KiB
Go
146 lines
4.7 KiB
Go
package hostaddr
|
|
|
|
import (
|
|
"net/netip"
|
|
"testing"
|
|
)
|
|
|
|
func TestParseMACAcceptsEveryCommonFormat(t *testing.T) {
|
|
for _, in := range []string{
|
|
"aa:bb:cc:dd:ee:ff", "AA:BB:CC:DD:EE:FF", "aa-bb-cc-dd-ee-ff", "aabb.ccdd.eeff", "aabbccddeeff", " AaBb.CcDd.EeFf ",
|
|
} {
|
|
got, err := ParseMAC(in)
|
|
if err != nil || got != "aa:bb:cc:dd:ee:ff" {
|
|
t.Errorf("ParseMAC(%q) = %q, %v", in, got, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseMACRefusesGarbage(t *testing.T) {
|
|
for _, in := range []string{"", "xyz", "aa:bb:cc", "aa:bb:cc:dd:ee:gg", "aabbccddeeff00", "aa bb cc dd ee ff", "2001:db8::1 ", "1.2.3.4"} {
|
|
if got, err := ParseMAC(in); err == nil {
|
|
t.Errorf("ParseMAC(%q) = %q, want an error", in, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParsePrefixNeedsSixDigits(t *testing.T) {
|
|
cases := map[string]string{
|
|
"aabbcc": "aa:bb:cc", "AA-BB-CC": "aa:bb:cc", "aabb.ccd": "aa:bb:cc:d", "aa:bb:cc:dd:ee:ff": "aa:bb:cc:dd:ee:ff",
|
|
}
|
|
for in, want := range cases {
|
|
if got, err := ParsePrefix(in); err != nil || got != want {
|
|
t.Errorf("ParsePrefix(%q) = %q, %v; want %q", in, got, err, want)
|
|
}
|
|
}
|
|
for _, in := range []string{"aabb", "aabbc", "", "zzzzzz", "aabbccddeeff0"} {
|
|
if got, err := ParsePrefix(in); err == nil {
|
|
t.Errorf("ParsePrefix(%q) = %q, want an error", in, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestKindDetectsLocalAndMulticastBits(t *testing.T) {
|
|
cases := map[string]string{
|
|
"00:11:22:33:44:55": KindGlobal, "02:11:22:33:44:55": KindLocal, "da:a1:19:00:00:01": KindLocal,
|
|
"01:00:5e:00:00:01": KindMulticast, "33:33:00:00:00:01": KindMulticast, "ff:ff:ff:ff:ff:ff": KindMulticast,
|
|
"a2:00:00:00:00:00": KindLocal, "f0:9f:c2:00:00:01": KindGlobal,
|
|
}
|
|
for mac, want := range cases {
|
|
if got := Kind(mac); got != want {
|
|
t.Errorf("Kind(%s) = %s, want %s", mac, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPrefixRangeCoversExactlyTheMatchingMACs(t *testing.T) {
|
|
lo, hi := PrefixRange("aa:bb:cc")
|
|
in := []string{"aa:bb:cc:00:00:00", "aa:bb:cc:ff:ff:ff", "aa:bb:cc"}
|
|
out := []string{"aa:bb:cd:00:00:00", "aa:bb:cb:ff:ff:ff", "aa:bb:cc" + string(rune(0x7f))}
|
|
for _, m := range in[:2] {
|
|
if m < lo || m >= hi {
|
|
t.Errorf("%s outside [%s,%s)", m, lo, hi)
|
|
}
|
|
}
|
|
for _, m := range out[:2] {
|
|
if m >= lo && m < hi {
|
|
t.Errorf("%s inside [%s,%s)", m, lo, hi)
|
|
}
|
|
}
|
|
lo, hi = PrefixRange("aa:bb:cf")
|
|
if in := "aa:bb:cf:12:34:56"; in < lo || in >= hi || "aa:bb:d0:00:00:00" < hi {
|
|
t.Errorf("f boundary wrong: [%s,%s)", lo, hi)
|
|
}
|
|
}
|
|
|
|
func TestFromBytesRefusesWrongLengthAndZero(t *testing.T) {
|
|
if m, ok := FromBytes([]byte{0xAA, 1, 2, 3, 4, 5}); !ok || m != "aa:01:02:03:04:05" {
|
|
t.Errorf("got %q %v", m, ok)
|
|
}
|
|
if _, ok := FromBytes([]byte{0, 0, 0, 0, 0, 0}); ok {
|
|
t.Error("all-zero MAC accepted")
|
|
}
|
|
if _, ok := FromBytes([]byte{1, 2, 3}); ok {
|
|
t.Error("short MAC accepted")
|
|
}
|
|
}
|
|
|
|
func TestIPKeyOrdersNumericallyAcrossFamilies(t *testing.T) {
|
|
a := IPKey(netip.MustParseAddr("10.0.0.9"))
|
|
b := IPKey(netip.MustParseAddr("10.0.0.10"))
|
|
if a >= b {
|
|
t.Errorf("10.0.0.9 (%s) must sort before 10.0.0.10 (%s)", a, b)
|
|
}
|
|
if len(IPKey(netip.MustParseAddr("2001:db8::1"))) != 32 || len(a) != 32 {
|
|
t.Error("keys are not fixed width")
|
|
}
|
|
}
|
|
|
|
func TestKeyRangeMatchesCIDRMembers(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
cidr string
|
|
in []string
|
|
out []string
|
|
}{
|
|
{"10.1.2.0/24", []string{"10.1.2.0", "10.1.2.255", "10.1.2.77"}, []string{"10.1.1.255", "10.1.3.0"}},
|
|
{"2001:db8:1::/48", []string{"2001:db8:1::1", "2001:db8:1:ffff::1"}, []string{"2001:db8:2::1", "2001:db8::1"}},
|
|
{"192.0.2.1/32", []string{"192.0.2.1"}, []string{"192.0.2.2", "192.0.2.0"}},
|
|
} {
|
|
lo, hi := KeyRange(netip.MustParsePrefix(tc.cidr))
|
|
for _, s := range tc.in {
|
|
if k := IPKey(netip.MustParseAddr(s)); k < lo || k > hi {
|
|
t.Errorf("%s outside %s", s, tc.cidr)
|
|
}
|
|
}
|
|
for _, s := range tc.out {
|
|
if k := IPKey(netip.MustParseAddr(s)); k >= lo && k <= hi {
|
|
t.Errorf("%s inside %s", s, tc.cidr)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestClassifySortsEveryQueryShape(t *testing.T) {
|
|
cases := []struct {
|
|
in, kind string
|
|
}{
|
|
{"aabb.ccdd.eeff", QueryMAC}, {"AA-BB-CC-DD-EE-FF", QueryMAC}, {"aabbcc", QueryPrefix}, {"aa:bb:cc:d", QueryPrefix},
|
|
{"2001:db8::1", QueryIP}, {"192.0.2.7", QueryIP}, {"::1", QueryIP}, {"10.0.0.0/8", QueryCIDR}, {"2001:db8::/32", QueryCIDR},
|
|
{"vendor:Ubiquiti", QueryVendor}, {"VENDOR: cisco", QueryVendor},
|
|
}
|
|
for _, c := range cases {
|
|
q, err := Classify(c.in)
|
|
if err != nil || q.Kind != c.kind {
|
|
t.Errorf("Classify(%q) = %+v, %v; want %s", c.in, q, err, c.kind)
|
|
}
|
|
}
|
|
for _, bad := range []string{"", "hello", "aabb", "vendor:", "10.0.0.0/99", "12:34"} {
|
|
if q, err := Classify(bad); err == nil {
|
|
t.Errorf("Classify(%q) = %+v, want an error", bad, q)
|
|
}
|
|
}
|
|
if q, _ := Classify("VENDOR:Cisco"); q.Vendor != "cisco" {
|
|
t.Errorf("vendor not lower-cased: %q", q.Vendor)
|
|
}
|
|
}
|