Files
Your Name 6e1b010c17 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)
- 星型拓扑,无环路
2026-04-26 06:16:15 +08:00

420 regels
13 KiB
Go
Ruw Permalink Blame Geschiedenis

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 topology
import (
"fmt"
"network-topology-discovery/pkg/models"
"strings"
)
// Builder 拓扑构建器
type Builder struct {
devices []models.Device
}
// NewBuilder 创建拓扑构建器
func NewBuilder() *Builder {
return &Builder{}
}
// AddDevice 添加设备
func (b *Builder) AddDevice(device models.Device) {
b.devices = append(b.devices, device)
}
// AddDevices 批量添加设备
func (b *Builder) AddDevices(devices []models.Device) {
b.devices = append(b.devices, devices...)
}
// Build 构建拓扑图
func (b *Builder) Build() models.TopologyGraph {
graph := models.TopologyGraph{
Nodes: make([]models.TopologyNode, 0),
Edges: make([]models.TopologyEdge, 0),
}
// 构建节点
nodeMap := make(map[string]models.TopologyNode)
for _, device := range b.devices {
// 优先使用主机名,如果没有则使用IP作为显示名
hostname := device.Hostname
if hostname == "" {
hostname = device.IP
}
node := models.TopologyNode{
ID: device.IP,
IP: device.IP,
Hostname: hostname,
Type: string(device.Type),
Icon: getDeviceIcon(device.Type),
}
nodeMap[device.IP] = node
graph.Nodes = append(graph.Nodes, node)
}
fmt.Printf("\n========== TOPOLOGY BUILD ===========\n")
fmt.Printf("Total devices: %d\n", len(b.devices))
fmt.Printf("Total nodes created: %d\n", len(graph.Nodes))
for _, node := range graph.Nodes {
fmt.Printf(" Node: %s (%s) - Type: %s\n", node.ID, node.Hostname, node.Type)
}
// 构建边(基于邻居信息)
edgeMap := make(map[string]bool) // 用于去重
// 打印所有设备的MAC地址,用于调试
fmt.Println("\n========== MAC Address Database ==========")
for _, d := range b.devices {
if len(d.MACAddresses) > 0 {
fmt.Printf("Device %s (%s): %d MAC addresses\n", d.Hostname, d.IP, len(d.MACAddresses))
for i, mac := range d.MACAddresses {
if i < 5 { // 只打印前5个
fmt.Printf(" MAC[%d]: %s\n", i, mac)
}
}
if len(d.MACAddresses) > 5 {
fmt.Printf(" ... and %d more\n", len(d.MACAddresses)-5)
}
}
}
fmt.Println("==========================================\n")
for _, device := range b.devices {
fmt.Printf("Building edges for device %s: %d neighbors, %d MAC addresses\n",
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)
// 尝试匹配邻居设备 - 多重策略
targetIP := neighbor.RemoteIP
matchMethod := "IP"
// 策略1: 如果已有IP,直接使用
if targetIP != "" {
fmt.Printf(" ✓ Using direct IP: %s\n", targetIP)
} else if neighbor.RemoteDevice != "" {
// 策略2: 尝试通过主机名匹配
fmt.Printf(" Trying hostname match: %s\n", neighbor.RemoteDevice)
for _, d := range b.devices {
if d.Hostname == neighbor.RemoteDevice {
targetIP = d.IP
matchMethod = "hostname"
fmt.Printf(" ✓ Matched by hostname: %s -> %s\n", d.Hostname, d.IP)
break
}
}
}
// 策略3: 通过MAC地址匹配
if targetIP == "" && neighbor.RemoteMAC != "" {
fmt.Printf(" Trying MAC match: %s\n", neighbor.RemoteMAC)
for _, d := range b.devices {
for _, mac := range d.MACAddresses {
// 标准化MAC地址进行比较(去除分隔符,转小写)
normalizedNeighborMAC := normalizeMAC(neighbor.RemoteMAC)
normalizedDeviceMAC := normalizeMAC(mac)
if normalizedNeighborMAC == normalizedDeviceMAC {
targetIP = d.IP
matchMethod = "MAC"
fmt.Printf(" ✓ Matched by MAC: %s (device) == %s (neighbor) -> %s\n",
mac, neighbor.RemoteMAC, d.IP)
break
}
}
if targetIP != "" {
break
}
}
}
// 策略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地址
localInterfaceIP := ""
for _, iface := range device.Interfaces {
if iface.Name == neighbor.LocalInterface && iface.IP != "" {
localInterfaceIP = iface.IP
break
}
}
if localInterfaceIP != "" {
fmt.Printf(" Trying subnet match: local interface %s has IP %s\n",
neighbor.LocalInterface, localInterfaceIP)
// 计算本地接口的网段
localSubnet := getSubnet(localInterfaceIP)
// 查找其他设备中是否有接口在同一网段
for _, d := range b.devices {
if d.IP == device.IP {
continue // 跳过自己
}
// 检查设备的管理IP是否在同一网段
if getSubnet(d.IP) == localSubnet {
targetIP = d.IP
matchMethod = "subnet(management IP)"
fmt.Printf(" ✓ Matched by subnet: %s in %s\n", d.IP, localSubnet)
break
}
// 检查设备的所有接口是否在同一网段
for _, iface := range d.Interfaces {
if iface.IP != "" && getSubnet(iface.IP) == localSubnet {
targetIP = d.IP
matchMethod = fmt.Sprintf("subnet(interface %s)", iface.Name)
fmt.Printf(" ✓ Matched by subnet: %s (%s) in %s\n",
d.IP, iface.Name, localSubnet)
break
}
}
if targetIP != "" {
break
}
}
} else {
// 策略4b: 本地接口没有IP,尝试使用设备管理IP进行子网匹配(新增)
// 注意:只有当该网段只有2台设备时才使用此策略(点对点连接)
fmt.Printf(" Trying subnet match with management IP: %s\n", device.IP)
localSubnet := getSubnet(device.IP)
// 统计在该网段的设备数量
var devicesInSubnet []string
for _, d := range b.devices {
if d.IP != device.IP && getSubnet(d.IP) == localSubnet {
devicesInSubnet = append(devicesInSubnet, d.IP)
}
}
// 只有当网段中恰好有1台其他设备时才匹配(点对点)
if len(devicesInSubnet) == 1 {
targetIP = devicesInSubnet[0]
matchMethod = "subnet(management IP both sides)"
fmt.Printf(" ✓ Matched by management subnet: %s in %s (only device in subnet)\n", targetIP, localSubnet)
} else if len(devicesInSubnet) > 1 {
fmt.Printf(" ✗ Skipping subnet match: %d devices in subnet %s (ambiguous)\n", len(devicesInSubnet)+1, localSubnet)
}
}
}
// 策略5: 通过接口描述匹配(新增)
if targetIP == "" {
// 查找本地接口的描述,看是否包含其他设备的IP或主机名
for _, iface := range device.Interfaces {
if iface.Name == neighbor.LocalInterface && iface.Description != "" {
desc := strings.ToLower(iface.Description)
fmt.Printf(" Trying description match: %s\n", iface.Description)
for _, d := range b.devices {
if d.IP == device.IP {
continue
}
// 检查描述中是否包含目标设备的IP或主机名
if strings.Contains(desc, strings.ToLower(d.IP)) ||
(d.Hostname != "" && strings.Contains(desc, strings.ToLower(d.Hostname))) {
targetIP = d.IP
matchMethod = "description"
fmt.Printf(" ✓ Matched by description: %s\n", iface.Description)
break
}
}
}
if targetIP != "" {
break
}
}
}
// 策略6: 通过邻居端口号反向匹配(已禁用 - 容易产生错误连接)
// 暂时禁用,因为端口号匹配不够精确,可能产生环路
if targetIP == "" {
fmt.Printf(" ✗ Could not match neighbor on %s (MAC: %s)\n", neighbor.LocalInterface, neighbor.RemoteMAC)
continue
}
fmt.Printf(" ✓ Creating edge: %s (%s) -> %s via %s, matched by %s\n",
device.IP, neighbor.LocalInterface, targetIP, neighbor.RemoteInterface, matchMethod)
// 避免重复边:同一对设备之间只保留一条边(不区分接口)
// 如果这对设备已经有边了,跳过
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,
Target: targetIP,
SourceInterface: neighbor.LocalInterface,
TargetInterface: neighbor.RemoteInterface,
Protocol: neighbor.Protocol,
}
graph.Edges = append(graph.Edges, edge)
edgeMap[edgeID] = true
}
}
fmt.Printf("\nTotal edges created: %d\n", len(graph.Edges))
for _, edge := range graph.Edges {
fmt.Printf(" Edge: %s (%s) -> %s (%s) [%s]\n",
edge.Source, edge.SourceInterface,
edge.Target, edge.TargetInterface,
edge.Protocol)
}
fmt.Println("========================================\n")
return graph
}
// normalizeMAC 标准化MAC地址(去除分隔符,转小写)
func normalizeMAC(mac string) string {
// 去除所有分隔符(-、:、.
result := ""
for _, c := range mac {
if c != '-' && c != ':' && c != '.' {
result += string(c)
}
}
// 转小写
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, ".")
if len(parts) == 4 {
// 返回前3段作为网段,例如: 172.16.8.0/24 -> "172.16.8"
return fmt.Sprintf("%s.%s.%s", parts[0], parts[1], parts[2])
}
return ip
}
// getDeviceIcon 获取设备图标
func getDeviceIcon(deviceType models.DeviceType) string {
switch deviceType {
case models.DeviceTypeCisco:
return "router"
case models.DeviceTypeHuawei:
return "router"
case models.DeviceTypeH3C:
return "switch"
case models.DeviceTypeASA:
return "firewall"
case models.DeviceTypeLinux:
return "server"
case models.DeviceTypeWindows:
return "server"
default:
return "device"
}
}
// GetDevices 获取所有设备
func (b *Builder) GetDevices() []models.Device {
return b.devices
}
// Clear 清空拓扑
func (b *Builder) Clear() {
b.devices = make([]models.Device, 0)
}