diff --git a/internal/topology/builder.go b/internal/topology/builder.go index 6de1473..29d4d02 100644 --- a/internal/topology/builder.go +++ b/internal/topology/builder.go @@ -85,6 +85,16 @@ func (b *Builder) Build() models.TopologyGraph { device.IP, len(device.Neighbors), len(device.MACAddresses)) for _, neighbor := range device.Neighbors { + // 过滤虚拟机邻居:识别MAC地址前缀,只保留真实网络设备 + if neighbor.RemoteMAC != "" { + macPrefix := getMACPrefix(neighbor.RemoteMAC) + if isVirtualMAC(macPrefix) { + fmt.Printf(" ⊘ Skipping virtual machine neighbor on %s (MAC: %s, prefix: %s)\n", + neighbor.LocalInterface, neighbor.RemoteMAC, macPrefix) + continue + } + } + fmt.Printf(" Processing neighbor on %s: RemoteMAC=%s, RemoteDevice=%s, RemoteIP=%s\n", neighbor.LocalInterface, neighbor.RemoteMAC, neighbor.RemoteDevice, neighbor.RemoteIP) @@ -131,6 +141,39 @@ func (b *Builder) Build() models.TopologyGraph { } } + // 策略3b: 通过MAC前缀匹配(新增) + // 当精确MAC匹配失败时,尝试通过MAC前缀匹配(适用于同一设备的多个接口) + if targetIP == "" && neighbor.RemoteMAC != "" { + neighborMACPrefix := getMACPrefix(neighbor.RemoteMAC) + fmt.Printf(" Trying MAC prefix match: %s (prefix: %s)\n", neighbor.RemoteMAC, neighborMACPrefix) + + for _, d := range b.devices { + if d.IP == device.IP { + continue // 跳过自己 + } + + // 检查该设备的MAC地址是否有相同前缀 + matchingMACs := 0 + for _, mac := range d.MACAddresses { + if getMACPrefix(mac) == neighborMACPrefix { + matchingMACs++ + } + } + + // 如果该设备有多个MAC地址使用相同前缀,说明是同一台设备 + if matchingMACs >= 3 { // 至少3个MAC使用相同前缀 + // 进一步验证:检查是否在同一网段 + if getSubnet(d.IP) == getSubnet(device.IP) { + targetIP = d.IP + matchMethod = fmt.Sprintf("MAC-prefix(%s)", neighborMACPrefix) + fmt.Printf(" ✓ Matched by MAC prefix: %s (device has %d MACs with prefix %s, same subnet) -> %s\n", + neighbor.RemoteMAC, matchingMACs, neighborMACPrefix, d.IP) + break + } + } + } + } + // 策略4: 通过本地接口IP网段匹配(新增) if targetIP == "" { // 查找本地接口的IP地址 @@ -231,6 +274,9 @@ func (b *Builder) Build() models.TopologyGraph { } } + // 策略6: 通过邻居端口号反向匹配(已禁用 - 容易产生错误连接) + // 暂时禁用,因为端口号匹配不够精确,可能产生环路 + if targetIP == "" { fmt.Printf(" ✗ Could not match neighbor on %s (MAC: %s)\n", neighbor.LocalInterface, neighbor.RemoteMAC) continue @@ -239,15 +285,24 @@ func (b *Builder) Build() models.TopologyGraph { fmt.Printf(" ✓ Creating edge: %s (%s) -> %s via %s, matched by %s\n", device.IP, neighbor.LocalInterface, targetIP, neighbor.RemoteInterface, matchMethod) - // 创建唯一的边ID - edgeID := fmt.Sprintf("%s-%s-%s", device.IP, neighbor.LocalInterface, targetIP) - reverseEdgeID := fmt.Sprintf("%s-%s-%s", targetIP, neighbor.RemoteInterface, device.IP) - - // 避免重复边 - if edgeMap[edgeID] || edgeMap[reverseEdgeID] { + // 避免重复边:同一对设备之间只保留一条边(不区分接口) + // 如果这对设备已经有边了,跳过 + hasExistingEdge := false + for _, existingEdge := range graph.Edges { + if (existingEdge.Source == device.IP && existingEdge.Target == targetIP) || + (existingEdge.Source == targetIP && existingEdge.Target == device.IP) { + fmt.Printf(" ⚠ Skipping: edge between %s and %s already exists\n", device.IP, targetIP) + hasExistingEdge = true + break + } + } + if hasExistingEdge { continue } + // 创建唯一的边ID(包含接口信息用于调试) + edgeID := fmt.Sprintf("%s-%s-%s", device.IP, neighbor.LocalInterface, targetIP) + edge := models.TopologyEdge{ ID: edgeID, Source: device.IP, @@ -287,6 +342,42 @@ func normalizeMAC(mac string) string { return strings.ToLower(result) } +// getMACPrefix 获取MAC地址前缀(前4个字符) +func getMACPrefix(mac string) string { + normalized := normalizeMAC(mac) + if len(normalized) >= 4 { + return normalized[0:4] + } + return normalized +} + +// isVirtualMAC 判断是否是虚拟机MAC地址前缀 +func isVirtualMAC(prefix string) bool { + // 根据用户反馈,这些前缀都是虚拟机 + userVirtualPrefixes := []string{"9844", "9400", "cc05", "44a1"} + + for _, vp := range userVirtualPrefixes { + if prefix == vp { + return true + } + } + + // 常见虚拟机/虚拟网卡MAC前缀(VMware、Hyper-V、KVM等) + virtualPrefixes := []string{ + "0005", "000c", "0015", "0050", // VMware + "0017", "001d", "0025", "5254", // KVM/QEMU + "0015", "0017", "00ff", // Xen + } + + for _, vp := range virtualPrefixes { + if prefix == vp { + return true + } + } + + return false +} + // getSubnet 获取IP地址的C类网段(简化版,适用于/24网络) func getSubnet(ip string) string { parts := strings.Split(ip, ".")