refactor(discovery): use ping for discovery and fill MAC via SSH

This commit is contained in:
12600k-rog-d4
2026-08-12 21:45:51 +08:00
parent 906bb54aaa
commit 175c5e8c6c
21 changed files with 542 additions and 310 deletions

View File

@@ -1,77 +0,0 @@
package discovery
import (
"os"
"os/exec"
"runtime"
"strings"
)
// lookupMAC 从 ARP 表获取 IP 对应的 MAC 地址(跨平台)
// 返回空字符串表示无法获取
func lookupMAC(ip string) string {
switch runtime.GOOS {
case "linux":
return macFromProcNetArp(ip)
case "windows":
return macFromWindowsArp(ip)
default:
return macFromUnixArp(ip)
}
}
// macFromProcNetArp 解析 Linux /proc/net/arp
func macFromProcNetArp(ip string) string {
data, err := os.ReadFile("/proc/net/arp")
if err != nil {
return ""
}
for _, line := range strings.Split(string(data), "\n")[1:] {
fields := strings.Fields(line)
if len(fields) >= 4 && fields[0] == ip {
mac := fields[3]
if mac != "" && mac != "00:00:00:00:00:00" {
return mac
}
}
}
return ""
}
// macFromWindowsArp 解析 Windows `arp -a` 输出
func macFromWindowsArp(ip string) string {
out, err := exec.Command("arp", "-a").Output()
if err != nil {
return ""
}
for _, line := range strings.Split(string(out), "\n") {
fields := strings.Fields(line)
if len(fields) >= 2 && fields[0] == ip {
mac := strings.ReplaceAll(fields[1], "-", ":")
if mac != "" && mac != "00:00:00:00:00:00" {
return strings.ToLower(mac)
}
}
}
return ""
}
// macFromUnixArp 解析 macOS/Linux `arp -n` 输出
func macFromUnixArp(ip string) string {
out, err := exec.Command("arp", "-n", ip).Output()
if err != nil {
return ""
}
for _, line := range strings.Split(string(out), "\n") {
fields := strings.Fields(line)
for i, f := range fields {
if f == "("+ip+")" && i+2 < len(fields) {
mac := fields[i+2]
if mac != "" && mac != "ff:ff:ff:ff:ff:ff" {
return strings.ToLower(mac)
}
}
}
}
return ""
}

View File

@@ -1,23 +1,18 @@
package discovery
import (
"fmt"
"auto-check/pkg/model"
)
// Devices 设备表IP → 设备),每轮 Discover() 后更新,包级公开
var Devices = make(map[string]model.Device)
// Discover 扫描并更新包级 Devices map(过滤无 MAC 设备,保留上次状态延续)
// Discover 扫描并更新包级 Devices map
func (s *Scanner) Discover() {
scanned := s.Scan()
Devices = make(map[string]model.Device, len(scanned))
devices := make(map[string]model.Device, len(scanned))
for _, dev := range scanned {
if dev.MAC == "" {
fmt.Printf(" [!] %s 无 MACARP 未解析),下轮重试\n", dev.IP)
continue
}
Devices[dev.IP] = dev
devices[dev.IP] = dev
}
Devices = devices
}

View File

@@ -1,61 +1,88 @@
package discovery
import (
"context"
"fmt"
"net"
"os/exec"
"runtime"
"strings"
"sync"
"time"
"auto-check/pkg/model"
)
// Scanner 网段扫描器(串行,按顺序逐个探测)
// Scanner 网段扫描器(并发 ping 探测)
type Scanner struct {
CIDR string
Timeout time.Duration
CIDR string
Timeout time.Duration // 单 IP 探测超时
Concurrency int // 并发数,默认 30
}
// NewScanner 创建扫描器
func NewScanner(cidr string, timeout time.Duration) *Scanner {
func NewScanner(cidr string, timeout time.Duration, concurrency int) *Scanner {
if concurrency <= 0 {
concurrency = 10
}
return &Scanner{
CIDR: cidr,
Timeout: timeout,
CIDR: cidr,
Timeout: timeout,
Concurrency: concurrency,
}
}
// Scan 扫描网段,返回存活设备列表(含 MAC 地址
// 串行逐个探测,每个 IP 有超时保护
// Scan 并发 ping 扫描网段,返回存活设备列表(IPMAC 留空待 SSH 阶段回填
func (s *Scanner) Scan() []model.Device {
ips, ok := s.candidateIPs()
if !ok {
return nil
}
fmt.Printf("[扫描] 网段 %s共 %d 个IP开始探测...\n", s.CIDR, len(ips))
fmt.Printf("[扫描] 网段 %s共 %d 个IP%d 并发 ping 探测...\n", s.CIDR, len(ips), s.Concurrency)
var results []model.Device
for _, ip := range ips {
if dev, ok := s.probe(ip); ok {
results = append(results, dev)
}
jobs := make(chan string, len(ips))
results := make(chan model.Device, len(ips))
var wg sync.WaitGroup
for i := 0; i < s.Concurrency; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for ip := range jobs {
if s.ping(ip) {
results <- model.Device{IP: ip, MAC: ""}
}
}
}()
}
fmt.Printf("[扫描] 完成,发现 %d 台存活设备\n", len(results))
return results
for _, ip := range ips {
jobs <- ip
}
close(jobs)
wg.Wait()
close(results)
var devices []model.Device
for dev := range results {
devices = append(devices, dev)
}
fmt.Printf("[扫描] 完成,发现 %d 台存活设备\n", len(devices))
return devices
}
// candidateIPs 解析网段并生成候选 IP 列表(排除网络地址和广播地址)
func (s *Scanner) candidateIPs() ([]net.IP, bool) {
ip, ipnet, err := net.ParseCIDR(s.CIDR)
func (s *Scanner) candidateIPs() ([]string, bool) {
_, ipnet, err := net.ParseCIDR(s.CIDR)
if err != nil {
fmt.Printf("[扫描] 网段解析失败: %v\n", err)
return nil, false
}
var ips []net.IP
for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) {
dst := make(net.IP, len(ip))
copy(dst, ip)
ips = append(ips, dst)
ip := ipnet.IP.Mask(ipnet.Mask)
var ips []string
for ; ipnet.Contains(ip); inc(ip) {
ips = append(ips, ip.String())
}
if len(ips) > 2 {
ips = ips[1 : len(ips)-1]
@@ -63,39 +90,47 @@ func (s *Scanner) candidateIPs() ([]net.IP, bool) {
return ips, true
}
// probe 探测单个 IP存活则返回设备信息MAC 通过 ARP 表查询)
func (s *Scanner) probe(ip net.IP) (model.Device, bool) {
if !s.isAlive(ip) {
return model.Device{}, false
// ping 探测单个 IP 是否存活ICMP经系统 ping 命令)。
// 仅凭 exit code 不可靠Windows 在某些代答/虚拟网卡场景会返回 0
// 因此解析输出,要求至少收到 1 个回包。
func (s *Scanner) ping(ip string) bool {
// ping 命令的等待超时(毫秒):固定 1s 足够局域网探测
const pingWait = 1000
// context 超时设为 ping 等待的 2 倍,留足 ping 自然退出时间,
// 避免 context 先于 ping -w 杀进程导致拿不到输出。
ctx, cancel := context.WithTimeout(context.Background(), 2*pingWait*time.Millisecond)
defer cancel()
cmd := exec.CommandContext(ctx, "ping", "-n", "1", "-w", fmt.Sprintf("%d", pingWait), ip)
out, err := cmd.Output()
if err != nil {
return false
}
mac := lookupMAC(ip.String())
desc := "无MAC"
if mac != "" {
desc = mac
}
fmt.Printf(" [+] %s 存活 (MAC: %s)\n", ip, desc)
return model.Device{IP: ip.String(), MAC: mac}, true
return parsePingReply(string(out))
}
// isAlive 用 ping 探测单个 IP 是否存活(内置超时保护)
func (s *Scanner) isAlive(ip net.IP) bool {
waitMs := s.Timeout.Milliseconds()
if waitMs < 1000 {
waitMs = 1000
// parsePingReply 判断 ping 输出是否表示存活
func parsePingReply(out string) bool {
for _, line := range strings.Split(out, "\n") {
l := strings.ToLower(strings.TrimSpace(line))
// Windows: "Packets: Sent = 1, Received = 1, Lost = 0"
if strings.Contains(l, "received =") {
i := strings.Index(l, "received =")
n := strings.TrimSpace(l[i+len("received ="):])
// 取数字前缀
cnt := 0
for _, c := range n {
if c < '0' || c > '9' {
break
}
cnt = cnt*10 + int(c-'0')
}
return cnt >= 1
}
}
var args []string
switch runtime.GOOS {
case "windows":
args = []string{"-n", "1", "-w", fmt.Sprintf("%d", waitMs), ip.String()}
case "darwin":
args = []string{"-c", "1", "-W", fmt.Sprintf("%d", waitMs), ip.String()}
default:
args = []string{"-c", "1", "-W", fmt.Sprintf("%d", waitMs/1000), ip.String()}
}
return exec.Command("ping", args...).Run() == nil
return false
}
// inc IP 递增
// inc IP 递增(原地修改,调用方负责复制)
func inc(ip net.IP) {
for j := len(ip) - 1; j >= 0; j-- {
ip[j]++