Files
network-topology-discovery/internal/topology/builder.go
T
Your Name e2f804ac52 feat: 增强拓扑匹配算法,支持多重匹配策略
- 添加5重匹配策略:IP直连、主机名、MAC地址、子网匹配、接口描述
- 实现智能子网匹配:仅当网段只有2台设备时才使用(避免歧义)
- 修复JSON配置文件尾随逗号问题
- 修复devices.json解析错误
- 优化邻居匹配逻辑,提升拓扑连接准确性

修复问题:
- 解决LLDP邻居MAC无法匹配设备的问题
- 解决ARP表解析返回0条记录的问题
- 解决设备之间无法建立拓扑连接的问题

当前成果:
- 5个设备节点正确显示
- 3条拓扑边正确建立(172.16.8.6↔172.16.8.1, 172.16.8.8↔172.16.8.6, 172.16.8.9↔172.16.8.6)
2026-04-26 05:45:00 +08:00

329 baris
9.7 KiB
Go
Mentah Salahkan Riwayat

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 {
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
}
}
}
// 策略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
}
}
}
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)
// 创建唯一的边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] {
continue
}
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)
}
// 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)
}