Files
membank/auto-check/pkg/discovery/scanner.go

142 lines
3.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package discovery
import (
"context"
"fmt"
"net"
"os/exec"
"strings"
"sync"
"time"
"auto-check/pkg/model"
)
// Scanner 网段扫描器(并发 ping 探测)
type Scanner struct {
CIDR string
Timeout time.Duration // 单 IP 探测超时
Concurrency int // 并发数,默认 30
}
// NewScanner 创建扫描器
func NewScanner(cidr string, timeout time.Duration, concurrency int) *Scanner {
if concurrency <= 0 {
concurrency = 10
}
return &Scanner{
CIDR: cidr,
Timeout: timeout,
Concurrency: concurrency,
}
}
// Scan 并发 ping 扫描网段,返回存活设备列表(仅含 IPMAC 留空待 SSH 阶段回填)
func (s *Scanner) Scan() []model.Device {
ips, ok := s.candidateIPs()
if !ok {
return nil
}
fmt.Printf("[扫描] 网段 %s共 %d 个IP%d 并发 ping 探测...\n", s.CIDR, len(ips), s.Concurrency)
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: ""}
}
}
}()
}
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() ([]string, bool) {
_, ipnet, err := net.ParseCIDR(s.CIDR)
if err != nil {
fmt.Printf("[扫描] 网段解析失败: %v\n", err)
return nil, false
}
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]
}
return ips, true
}
// 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
}
return parsePingReply(string(out))
}
// 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
}
}
return false
}
// inc IP 递增(原地修改,调用方负责复制)
func inc(ip net.IP) {
for j := len(ip) - 1; j >= 0; j-- {
ip[j]++
if ip[j] > 0 {
break
}
}
}