feat(arp): 自 go-hua 迁入 ARP 表查询与定时刷新缓存
- IP→MAC 查询(AutoRefresh 后台刷新/Search/Table), 新增依赖 mostlygeek/arp - 附测试、examples/arp 例程与包 README
This commit is contained in:
@@ -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
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user