feat: 优化拓扑匹配策略,修复环路问题

- 禁用端口号匹配策略(策略6),避免产生错误连接和环路
- 保留精确匹配策略:MAC精确匹配、MAC前缀匹配、子网匹配
- 优化虚拟机邻居过滤(过滤9844、9400等虚拟机MAC前缀)
- 实现设备间去重逻辑(同一对设备只保留一条边)

最终拓扑结构:
- 5个节点,4条边
- 防火墙(172.16.8.1)  核心交换机(172.16.8.6)
- 核心交换机(172.16.8.6)  接入交换机(172.16.8.8/9/10)
- 星型拓扑,无环路
This commit is contained in:
Your Name
2026-04-26 06:16:15 +08:00
parent e2f804ac52
commit 6e1b010c17
+97 -6
View File
@@ -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, ".")