From fb653292612eddca5b088ed88466c546d1597107 Mon Sep 17 00:00:00 2001 From: Elizabeth Alexander Hunt Date: Sat, 20 Jun 2026 09:40:07 -0700 Subject: Initial commit --- adapters/arp/arp.go | 63 ++++++++++++++++++++++++++++++++++++++++++++++++ adapters/arp/arp_test.go | 51 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 adapters/arp/arp.go create mode 100644 adapters/arp/arp_test.go (limited to 'adapters/arp') diff --git a/adapters/arp/arp.go b/adapters/arp/arp.go new file mode 100644 index 0000000..19d7632 --- /dev/null +++ b/adapters/arp/arp.go @@ -0,0 +1,63 @@ +package arp + +import ( + "os/exec" + "regexp" + "strings" + + "penguins.lan/utils" +) + +type ArpTable struct { + AddrToMac map[utils.INet4]utils.Mac +} + +// Lookup returns the canonical MAC for an IPv4 string, false if it isn't in the +// table or isn't parseable IPv4 (e.g. an IPv6 peer). +func (t *ArpTable) Lookup(ip string) (string, bool) { + addr, err := utils.ParseINet4(ip) + if err != nil { + return "", false + } + mac, ok := t.AddrToMac[*addr] + if !ok { + return "", false + } + return mac.Format(), true +} + +type TableProvider interface { + Fetch() (*ArpTable, error) +} + +var arpLine = regexp.MustCompile(`\((\d{1,3}(?:\.\d{1,3}){3})\) at ([0-9a-fA-F]{1,2}(?::[0-9a-fA-F]{1,2}){5})`) + +type SystemProvider struct{} + +func (SystemProvider) Fetch() (*ArpTable, error) { + out, err := exec.Command("arp", "-an").Output() + if err != nil { + return nil, err + } + return parseTable(string(out)), nil +} + +func parseTable(out string) *ArpTable { + table := &ArpTable{AddrToMac: make(map[utils.INet4]utils.Mac)} + for _, line := range strings.Split(out, "\n") { + m := arpLine.FindStringSubmatch(line) + if m == nil { + continue + } + addr, err := utils.ParseINet4(m[1]) + if err != nil { + continue + } + mac, err := utils.ParseMac(m[2]) + if err != nil { + continue + } + table.AddrToMac[*addr] = *mac + } + return table +} diff --git a/adapters/arp/arp_test.go b/adapters/arp/arp_test.go new file mode 100644 index 0000000..9a215e0 --- /dev/null +++ b/adapters/arp/arp_test.go @@ -0,0 +1,51 @@ +package arp + +import "testing" + +const sample = `Neighbor Linklayer Address +? (192.168.1.5) at 8:0:27:ab:cd:ef on en0 ifscope [ethernet] +? (192.168.1.9) at AA:BB:CC:11:22:33 [ether] on br-lan +? (10.0.0.2) at de:ad:be:ef:00:01 on eth0 +? (192.168.1.250) at (incomplete) on en0 [ethernet] + +` + +func TestParseTable(t *testing.T) { + table := parseTable(sample) + + got := make(map[string]string, len(table.AddrToMac)) + for ip, mac := range table.AddrToMac { + got[ip.Format()] = mac.Format() + } + + want := map[string]string{ + "192.168.1.5": "08:00:27:ab:cd:ef", + "192.168.1.9": "aa:bb:cc:11:22:33", + "10.0.0.2": "de:ad:be:ef:00:01", + } + + if len(got) != len(want) { + t.Fatalf("parsed %d entries %v, want %d (header/incomplete lines should be skipped)", len(got), got, len(want)) + } + for ip, mac := range want { + if got[ip] != mac { + t.Errorf("entry %q = %q, want %q", ip, got[ip], mac) + } + } +} + +func TestTableLookup(t *testing.T) { + table := parseTable(sample) + + if mac, ok := table.Lookup("192.168.1.5"); !ok || mac != "08:00:27:ab:cd:ef" { + t.Errorf("Lookup(192.168.1.5) = %q,%v", mac, ok) + } + if _, ok := table.Lookup("1.2.3.4"); ok { + t.Error("Lookup of a miss should be false") + } + if _, ok := table.Lookup("::1"); ok { + t.Error("Lookup of non-IPv4 should be false") + } +} + +var _ TableProvider = SystemProvider{} -- cgit v1.3