master
go 142 lines 3.51 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package l2topology
4
5 import (
6 "sort"
7 "strings"
8 )
9
10 type fdbCandidate struct {
11 mac string
12 bridgePort string
13 ifIndex int
14 statusRaw string
15 vlanID string
16 vlanName string
17 }
18
19 func buildFDBCandidates(entries []FDBObservation, bridgePortToIfIndex map[string]int) []fdbCandidate {
20 if len(entries) == 0 {
21 return nil
22 }
23
24 sorted := sortedFDBEntries(entries)
25 selfMACs := make(map[string]struct{}, len(sorted))
26 for _, entry := range sorted {
27 if canonicalFDBStatus(entry.Status) != fdbStatusSelf {
28 continue
29 }
30 mac := normalizeMAC(entry.MAC)
31 if mac == "" {
32 continue
33 }
34 selfMACs[mac] = struct{}{}
35 }
36
37 candidatesByEndpoint := make(map[string]fdbCandidate, len(sorted))
38 duplicates := make(map[string]struct{})
39 for _, entry := range sorted {
40 mac := normalizeMAC(entry.MAC)
41 if mac == "" {
42 continue
43 }
44 if _, isSelf := selfMACs[mac]; isSelf {
45 continue
46 }
47 if canonicalFDBStatus(entry.Status) != fdbStatusLearned {
48 continue
49 }
50
51 bridgePort := strings.TrimSpace(entry.BridgePort)
52 ifIndex := entry.IfIndex
53 if ifIndex <= 0 && bridgePort != "" {
54 if mappedIfIndex, ok := bridgePortToIfIndex[bridgePort]; ok {
55 ifIndex = mappedIfIndex
56 }
57 }
58
59 candidate := fdbCandidate{
60 mac: mac,
61 bridgePort: bridgePort,
62 ifIndex: ifIndex,
63 statusRaw: strings.TrimSpace(entry.Status),
64 vlanID: strings.TrimSpace(entry.VLANID),
65 vlanName: strings.TrimSpace(entry.VLANName),
66 }
67 candidateKey := opaqueCompositeKey(mac)
68 if candidate.vlanID != "" {
69 candidateKey = opaqueCompositeKey(mac, "vlan:"+strings.ToLower(candidate.vlanID))
70 }
71 if _, duplicated := duplicates[candidateKey]; duplicated {
72 continue
73 }
74
75 existing, exists := candidatesByEndpoint[candidateKey]
76 if !exists {
77 candidatesByEndpoint[candidateKey] = candidate
78 continue
79 }
80
81 if sameFDBDestination(existing, candidate) {
82 updated := existing
83 if candidate.statusRaw != "" {
84 updated.statusRaw = candidate.statusRaw
85 }
86 if updated.vlanName == "" && candidate.vlanName != "" {
87 updated.vlanName = candidate.vlanName
88 }
89 candidatesByEndpoint[candidateKey] = updated
90 continue
91 }
92
93 delete(candidatesByEndpoint, candidateKey)
94 duplicates[candidateKey] = struct{}{}
95 }
96
97 out := make([]fdbCandidate, 0, len(candidatesByEndpoint))
98 for _, candidate := range candidatesByEndpoint {
99 out = append(out, candidate)
100 }
101 sort.Slice(out, func(i, j int) bool {
102 if out[i].mac != out[j].mac {
103 return out[i].mac < out[j].mac
104 }
105 if out[i].vlanID != out[j].vlanID {
106 return out[i].vlanID < out[j].vlanID
107 }
108 if out[i].ifIndex != out[j].ifIndex {
109 return out[i].ifIndex < out[j].ifIndex
110 }
111 return out[i].bridgePort < out[j].bridgePort
112 })
113 return out
114 }
115
116 func canonicalFDBStatus(status string) string {
117 normalized := strings.ToLower(strings.TrimSpace(status))
118 switch normalized {
119 case "", "3", "learned", "dot1d_tp_fdb_status_learned", "dot1dtpfdbstatuslearned":
120 return fdbStatusLearned
121 case "4", "self", "dot1d_tp_fdb_status_self", "dot1dtpfdbstatusself":
122 return fdbStatusSelf
123 default:
124 if strings.Contains(normalized, "learned") {
125 return fdbStatusLearned
126 }
127 if strings.Contains(normalized, "self") {
128 return fdbStatusSelf
129 }
130 return fdbStatusIgnored
131 }
132 }
133
134 func sameFDBDestination(left, right fdbCandidate) bool {
135 if strings.TrimSpace(left.vlanID) != strings.TrimSpace(right.vlanID) {
136 return false
137 }
138 if left.ifIndex > 0 && right.ifIndex > 0 {
139 return left.ifIndex == right.ifIndex
140 }
141 return left.bridgePort != "" && left.bridgePort == right.bridgePort
142 }