468 Zeilen
14 KiB
Go
468 Zeilen
14 KiB
Go
package topology
|
||
|
||
import (
|
||
"fmt"
|
||
"network-topology-discovery/pkg/models"
|
||
"strings"
|
||
)
|
||
|
||
// Builder 拓扑构建器
|
||
type Builder struct {
|
||
devices []models.Device
|
||
}
|
||
|
||
// NewBuilder 创建拓扑构建器
|
||
func NewBuilder() *Builder {
|
||
return &Builder{}
|
||
}
|
||
|
||
// AddDevice 添加设备(按IP去重)
|
||
func (b *Builder) AddDevice(device models.Device) {
|
||
for i, existing := range b.devices {
|
||
if existing.IP == device.IP {
|
||
b.devices[i] = device // 覆盖更新
|
||
return
|
||
}
|
||
}
|
||
b.devices = append(b.devices, device)
|
||
}
|
||
|
||
// AddDevices 批量添加设备
|
||
func (b *Builder) AddDevices(devices []models.Device) {
|
||
b.devices = append(b.devices, devices...)
|
||
}
|
||
|
||
// RemoveDevice 根据ID或IP移除设备
|
||
func (b *Builder) RemoveDevice(id string) {
|
||
for i, d := range b.devices {
|
||
if d.ID == id || d.IP == id {
|
||
b.devices = append(b.devices[:i], b.devices[i+1:]...)
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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前缀匹配(使用12字符前缀 - 更精确)
|
||
// 同品牌设备虽然4字符前缀相同,但12字符前缀通常不同
|
||
if targetIP == "" && neighbor.RemoteMAC != "" {
|
||
// 使用12字符前缀(例如:642f-c7e0-03)
|
||
macPrefix := ""
|
||
if len(neighbor.RemoteMAC) >= 12 {
|
||
macPrefix = neighbor.RemoteMAC[:12]
|
||
} else {
|
||
macPrefix = neighbor.RemoteMAC
|
||
}
|
||
|
||
fmt.Printf(" Trying MAC prefix match (12 chars): %s (prefix: %s)\n", neighbor.RemoteMAC, macPrefix)
|
||
|
||
var matchedDevices []string
|
||
|
||
for _, d := range b.devices {
|
||
if d.IP == device.IP {
|
||
continue // 跳过自己
|
||
}
|
||
|
||
// 检查设备d的MAC地址是否有匹配的前缀
|
||
matchingMACs := 0
|
||
for _, mac := range d.MACAddresses {
|
||
if len(mac) >= 12 && mac[:12] == macPrefix {
|
||
matchingMACs++
|
||
}
|
||
}
|
||
|
||
if matchingMACs >= 3 {
|
||
matchedDevices = append(matchedDevices, d.IP)
|
||
}
|
||
}
|
||
|
||
// 只有唯一匹配时才建立连接
|
||
if len(matchedDevices) == 1 {
|
||
targetIP = matchedDevices[0]
|
||
matchMethod = fmt.Sprintf("MAC-prefix-12(%s)", macPrefix)
|
||
fmt.Printf(" ✓ Matched by MAC prefix-12 (unique): %s -> %s\n", neighbor.RemoteMAC, targetIP)
|
||
} else if len(matchedDevices) > 1 {
|
||
fmt.Printf(" ✗ Ambiguous MAC prefix-12 match: %s matched %v\n", macPrefix, matchedDevices)
|
||
} else {
|
||
fmt.Printf(" ✗ No device matches MAC prefix-12 %s\n", macPrefix)
|
||
}
|
||
}
|
||
|
||
// 策略3c: LLDP接口匹配(改进 - 单向但要求唯一性)
|
||
// 如果设备A的LLDP信息显示它连接到接口Y
|
||
// 而设备B有接口Y,则A连接到B
|
||
// 但如果有多个设备都有接口Y,则不匹配(避免歧义)
|
||
if targetIP == "" && neighbor.RemoteMAC != "" && neighbor.RemoteInterface != "" {
|
||
fmt.Printf(" Trying LLDP interface match: looking for device with interface %s\n", neighbor.RemoteInterface)
|
||
|
||
var matchedDevices []string
|
||
|
||
for _, d := range b.devices {
|
||
if d.IP == device.IP {
|
||
continue // 跳过自己
|
||
}
|
||
|
||
// 检查设备d是否有这个接口
|
||
for _, iface := range d.Interfaces {
|
||
if iface.Name == neighbor.RemoteInterface {
|
||
matchedDevices = append(matchedDevices, d.IP)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
// 只有唯一匹配时才建立连接
|
||
if len(matchedDevices) == 1 {
|
||
targetIP = matchedDevices[0]
|
||
matchMethod = fmt.Sprintf("LLDP-interface(%s->%s)", neighbor.LocalInterface, neighbor.RemoteInterface)
|
||
fmt.Printf(" ✓ Matched by LLDP interface (unique): %s %s -> %s %s\n",
|
||
device.IP, neighbor.LocalInterface, targetIP, neighbor.RemoteInterface)
|
||
} else if len(matchedDevices) > 1 {
|
||
fmt.Printf(" ✗ Ambiguous: multiple devices have interface %s: %v\n",
|
||
neighbor.RemoteInterface, matchedDevices)
|
||
} else {
|
||
fmt.Printf(" ✗ No device found with interface %s\n", neighbor.RemoteInterface)
|
||
}
|
||
}
|
||
|
||
// 策略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 (risky): 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 (risky): %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 (risky): %s (%s) in %s\n",
|
||
d.IP, iface.Name, localSubnet)
|
||
break
|
||
}
|
||
}
|
||
if targetIP != "" {
|
||
break
|
||
}
|
||
}
|
||
} else {
|
||
// 策略4b: 本地接口没有IP,尝试使用设备管理IP进行子网匹配(高风险策略 - 已禁用)
|
||
// 此策略容易产生错误连接,暂时禁用
|
||
fmt.Printf(" ✗ Skipping subnet match with management IP (disabled - too risky)\n")
|
||
}
|
||
}
|
||
|
||
// 策略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)
|
||
}
|