feat(arp): 自 go-hua 迁入 ARP 表查询与定时刷新缓存

- IP→MAC 查询(AutoRefresh 后台刷新/Search/Table), 新增依赖 mostlygeek/arp
- 附测试、examples/arp 例程与包 README
This commit is contained in:
2026-09-20 12:29:36 +08:00
parent 8a038d4df3
commit a4920a6a8e
10 changed files with 438 additions and 0 deletions
+37
View File
@@ -0,0 +1,37 @@
# arp
系统 ARP 表读取与 IP→MAC 查询,内置定时自动刷新缓存。
> 迁移自 go-hua/arp,代码保持原样。基于 `mostlygeek/arp` 读取系统 ARP 表,
> Windows/Unix/Linux 各有平台实现。
## 用法
```go
import (
"fmt"
"time"
"git.zeroonesoft.cn/golib/zogo/arp"
)
// 方式一:手动刷新 + 查询
arp.CacheUpdate()
mac := arp.Search("192.168.1.1") // 未命中会自动再刷一次
// 方式二:后台定时刷新(每 30s),进程退出前 Stop
arp.AutoRefresh(30 * time.Second)
defer arp.StopAutoRefresh()
// 全表
table := arp.Table() // map[ip]mac
fmt.Println("上次刷新:", arp.CacheLastUpdate(), "累计次数:", arp.CacheUpdateCount())
```
完整可运行例程:[examples/arp/main.go](../examples/arp/main.go)
## 注意
- 目标 IP 必须与本机同网段且有过通信,ARP 表才会有记录;查跨网段网关之外的
地址大概率 miss。
- `Search`/`Table` 读的是缓存;要实时数据先 `CacheUpdate` 或开启 `AutoRefresh`。
+57
View File
@@ -0,0 +1,57 @@
// Package arp 提供系统 ARP 表读取与 IP→MAC 查询,内置定时刷新缓存。
package arp
import (
"time"
)
// ArpTable ARP 表:IP → MAC 地址映射
type ArpTable map[string]string
var (
stop = make(chan struct{})
arpCache = &cache{
table: make(ArpTable),
}
)
// AutoRefresh 开启定时自动刷新 ARP 缓存
// t 为刷新间隔
func AutoRefresh(t time.Duration) {
go func() {
for {
select {
case <-time.After(t):
arpCache.Refresh()
case <-stop:
return
}
}
}()
}
// StopAutoRefresh 停止定时自动刷新
func StopAutoRefresh() {
stop <- struct{}{}
}
// CacheUpdate 手动刷新一次 ARP 缓存
func CacheUpdate() {
arpCache.Refresh()
}
// CacheLastUpdate 返回缓存最近一次刷新时间
func CacheLastUpdate() time.Time {
return arpCache.Updated
}
// CacheUpdateCount 返回缓存累计刷新次数
func CacheUpdateCount() int {
return arpCache.UpdatedCount
}
// Search looks up the MAC address for an IP address
// in the arp table
func Search(ip string) string {
return arpCache.Search(ip)
}
+43
View File
@@ -0,0 +1,43 @@
//go:build linux
// +build linux
package arp
import (
"bufio"
"os"
"strings"
)
const (
f_IPAddr int = iota
f_HWType
f_Flags
f_HWAddr
f_Mask
f_Device
)
// Table 读取 Linux 内核 ARP 表(/proc/net/arp)
func Table() ArpTable {
f, err := os.Open("/proc/net/arp")
if err != nil {
return nil
}
defer f.Close()
s := bufio.NewScanner(f)
s.Scan() // skip the field descriptions
var table = make(ArpTable)
for s.Scan() {
line := s.Text()
fields := strings.Fields(line)
table[fields[f_IPAddr]] = fields[f_HWAddr]
}
return table
}
+94
View File
@@ -0,0 +1,94 @@
package arp
import (
"fmt"
"testing"
"time"
)
var (
_ = fmt.Println
)
func TestTable(t *testing.T) {
table := Table()
if table == nil {
t.Errorf("Empty table")
}
}
func TestCacheInfo(t *testing.T) {
prevUpdated := CacheLastUpdate().UnixNano()
prevCount := CacheUpdateCount()
CacheUpdate()
if prevUpdated == CacheLastUpdate().UnixNano() {
t.Error()
}
if prevCount == CacheUpdateCount() {
t.Error()
}
}
func TestAutoRefresh(t *testing.T) {
if testing.Short() {
t.Skip("Skipping autorefresh test")
}
prevUpdated := CacheLastUpdate().UnixNano()
prevCount := CacheUpdateCount()
AutoRefresh(100 * time.Millisecond)
time.Sleep(200 * time.Millisecond)
StopAutoRefresh()
if prevUpdated == CacheLastUpdate().UnixNano() {
t.Error()
}
if prevCount == CacheUpdateCount() {
t.Error()
}
// test to make sure stop worked
prevUpdated = CacheLastUpdate().UnixNano()
prevCount = CacheUpdateCount()
time.Sleep(200 * time.Millisecond)
if prevUpdated != CacheLastUpdate().UnixNano() {
t.Error()
}
if prevCount != CacheUpdateCount() {
t.Error()
}
}
func TestSearch(t *testing.T) {
table := Table()
for ip, test := range table {
result := Search(ip)
if test != result {
t.Errorf("expected %s got %s", test, result)
}
}
}
func BenchmarkSearch(b *testing.B) {
table := Table()
if len(table) == 0 {
return
}
for ip, _ := range Table() {
for i := 0; i < b.N; i++ {
Search(ip)
}
// using the first key is enough
break
}
}
+40
View File
@@ -0,0 +1,40 @@
//go:build !linux && !windows
// +build !linux,!windows
// only tested on OSX
// decided to go with exec.Command after I couldn't figure
// out how to extract the arp cache out of the kernel with
// golang's syscall or Sysctl()
//
// ... Help appreciated :)
package arp
import (
"os/exec"
"strings"
)
// Table 通过 arp -an 命令读取 ARP 表(类 Unix 平台,仅在 OSX 测试过)
func Table() ArpTable {
data, err := exec.Command("arp", "-an").Output()
if err != nil {
return nil
}
var table = make(ArpTable)
for _, line := range strings.Split(string(data), "\n") {
fields := strings.Fields(line)
if len(fields) < 3 {
continue
}
// strip brackets around IP
ip := strings.Replace(fields[1], "(", "", -1)
ip = strings.Replace(ip, ")", "", -1)
table[ip] = fields[3]
}
return table
}
+57
View File
@@ -0,0 +1,57 @@
//go:build windows
// +build windows
package arp
// Windows arp table reader added by Claudio Matsuoka.
// Tested only in Windows 8.1, hopefully the arp command output format
// is the same in other Windows versions.
import (
"os/exec"
"strings"
"syscall"
)
// Table 读取 Windows 系统 ARP 表(执行 arp -a 命令解析)
// MAC 地址统一转换为冒号分隔格式
func Table() ArpTable {
cmd := exec.Command("arp", "-a")
// 隐藏窗口
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
data, err := cmd.Output()
// data, err := exec.Command("arp", "-a").Output()
if err != nil {
return nil
}
var table = make(ArpTable)
skipNext := false
for _, line := range strings.Split(string(data), "\n") {
// skip empty lines
if len(line) <= 0 {
continue
}
// skip Interface: lines
if line[0] != ' ' {
skipNext = true
continue
}
// skip column headers
if skipNext {
skipNext = false
continue
}
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
ip := fields[0]
// Normalize MAC address to colon-separated format
table[ip] = strings.Replace(fields[1], "-", ":", -1)
}
return table
}
+42
View File
@@ -0,0 +1,42 @@
package arp
import (
"sync"
"time"
)
// cache ARP 表缓存,读写加锁保证并发安全
type cache struct {
sync.RWMutex
table ArpTable
Updated time.Time
UpdatedCount int
}
// Refresh 重新读取系统 ARP 表,并记录刷新时间与累计次数
func (c *cache) Refresh() {
c.Lock()
defer c.Unlock()
c.table = Table()
c.Updated = time.Now()
c.UpdatedCount += 1
}
// Search 按 IP 查询 MAC 地址,缓存未命中时自动刷新一次后重查
func (c *cache) Search(ip string) string {
c.RLock()
defer c.RUnlock()
mac, ok := c.table[ip]
if !ok {
c.RUnlock()
c.Refresh()
c.RLock()
mac = c.table[ip]
}
return mac
}