master
go 246 lines 6.54 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package l2topology
4
5 import (
6 "net/netip"
7 "sort"
8 "strconv"
9 "strings"
10 )
11
12 type builtEndpointActors struct {
13 actors []Actor
14 count int
15 matchByEndpointID map[string]Match
16 labelsByEndpointID map[string]map[string]string
17 }
18
19 func buildEndpointActors(
20 attachments []Attachment,
21 enrichments []Enrichment,
22 ifaceByDeviceIndex map[string]Interface,
23 source string,
24 layer string,
25 actorIndex map[string]struct{},
26 actorMACIndex map[string]struct{},
27 ) builtEndpointActors {
28 accumulators := make(map[string]*endpointActorAccumulator)
29
30 for _, attachment := range attachments {
31 endpointID := strings.TrimSpace(attachment.EndpointID)
32 if endpointID == "" {
33 continue
34 }
35 acc := ensureEndpointActorAccumulator(accumulators, endpointID)
36 addEndpointIDIdentity(acc, endpointID)
37 if deviceID := strings.TrimSpace(attachment.DeviceID); deviceID != "" {
38 acc.deviceIDs[deviceID] = struct{}{}
39 }
40 if method := strings.TrimSpace(attachment.Method); method != "" {
41 acc.sources[strings.ToLower(method)] = struct{}{}
42 }
43 if attachment.IfIndex > 0 {
44 acc.ifIndexes[strconv.Itoa(attachment.IfIndex)] = struct{}{}
45 iface, ok := ifaceByDeviceIndex[deviceIfIndexKey(strings.TrimSpace(attachment.DeviceID), attachment.IfIndex)]
46 if ok {
47 if ifName := strings.TrimSpace(iface.IfName); ifName != "" {
48 acc.ifNames[ifName] = struct{}{}
49 }
50 }
51 }
52 if ifName := strings.TrimSpace(attachment.Labels["if_name"]); ifName != "" {
53 acc.ifNames[ifName] = struct{}{}
54 }
55 }
56
57 for _, enrichment := range enrichments {
58 endpointID := strings.TrimSpace(enrichment.EndpointID)
59 if endpointID == "" {
60 continue
61 }
62 acc := ensureEndpointActorAccumulator(accumulators, endpointID)
63 addEndpointIDIdentity(acc, endpointID)
64
65 if mac := normalizeMAC(enrichment.MAC); mac != "" {
66 acc.mac = mac
67 }
68 for _, ip := range enrichment.IPs {
69 if ip.IsValid() {
70 acc.ips[ip.String()] = ip.Unmap()
71 }
72 }
73 for _, sourceName := range csvToSet(enrichment.Labels["sources"]) {
74 acc.sources[sourceName] = struct{}{}
75 }
76 for _, deviceID := range csvToSet(enrichment.Labels["device_ids"]) {
77 deviceID = strings.TrimSpace(deviceID)
78 if deviceID == "" {
79 continue
80 }
81 acc.deviceIDs[deviceID] = struct{}{}
82 }
83 for _, ifIndex := range csvToSet(enrichment.Labels["if_indexes"]) {
84 acc.ifIndexes[ifIndex] = struct{}{}
85 }
86 for _, ifName := range csvToSet(enrichment.Labels["if_names"]) {
87 acc.ifNames[ifName] = struct{}{}
88 }
89 }
90
91 if len(accumulators) == 0 {
92 return builtEndpointActors{
93 matchByEndpointID: map[string]Match{},
94 labelsByEndpointID: map[string]map[string]string{},
95 }
96 }
97
98 keys := make([]string, 0, len(accumulators))
99 for endpointID := range accumulators {
100 keys = append(keys, endpointID)
101 }
102 sort.Strings(keys)
103
104 actors := make([]Actor, 0, len(keys))
105 endpointCount := 0
106 matchByEndpointID := make(map[string]Match, len(keys))
107 labelsByEndpointID := make(map[string]map[string]string, len(keys))
108 for _, endpointID := range keys {
109 acc := accumulators[endpointID]
110 if acc == nil {
111 continue
112 }
113
114 match := Match{}
115 if acc.mac != "" {
116 match.ChassisIDs = []string{acc.mac}
117 match.MacAddresses = []string{acc.mac}
118 }
119 match.IPAddresses = sortedEndpointIPs(acc.ips)
120 matchByEndpointID[endpointID] = match
121 labelsByEndpointID[endpointID] = map[string]string{
122 "learned_sources": strings.Join(sortedTopologySet(acc.sources), ","),
123 "learned_device_ids": strings.Join(sortedTopologySet(acc.deviceIDs), ","),
124 "learned_if_indexes": strings.Join(sortedTopologySet(acc.ifIndexes), ","),
125 "learned_if_names": strings.Join(sortedTopologySet(acc.ifNames), ","),
126 }
127
128 attrs := map[string]any{
129 "discovered": true,
130 "learned_sources": sortedTopologySet(acc.sources),
131 "learned_device_ids": sortedTopologySet(acc.deviceIDs),
132 "learned_if_indexes": sortedTopologySet(acc.ifIndexes),
133 "learned_if_names": sortedTopologySet(acc.ifNames),
134 }
135 derivedVendor, derivedPrefix := inferTopologyVendorFromMatch(match)
136 if derivedVendor != "" {
137 attrs["vendor"] = derivedVendor
138 attrs["vendor_source"] = "mac_oui"
139 attrs["vendor_confidence"] = "low"
140 attrs["vendor_match_prefix"] = derivedPrefix
141 attrs["vendor_derived"] = derivedVendor
142 attrs["vendor_derived_source"] = "mac_oui"
143 attrs["vendor_derived_confidence"] = "low"
144 attrs["vendor_derived_match_prefix"] = derivedPrefix
145 }
146 actor := Actor{
147 ActorType: "endpoint",
148 Layer: layer,
149 Source: source,
150 Match: match,
151 Attributes: pruneTopologyAttributes(attrs),
152 }
153
154 keys := topologyMatchIdentityKeys(actor.Match)
155 if len(keys) == 0 {
156 continue
157 }
158 macKeys := topologyMatchHardwareIdentityKeys(actor.Match)
159 if len(macKeys) > 0 {
160 if topologyIdentityIndexOverlaps(actorMACIndex, macKeys) {
161 continue
162 }
163 addTopologyIdentityKeys(actorMACIndex, macKeys)
164 } else if topologyIdentityIndexOverlaps(actorIndex, keys) {
165 continue
166 }
167 addTopologyIdentityKeys(actorIndex, keys)
168
169 actors = append(actors, actor)
170 endpointCount++
171 }
172
173 return builtEndpointActors{
174 actors: actors,
175 count: endpointCount,
176 matchByEndpointID: matchByEndpointID,
177 labelsByEndpointID: labelsByEndpointID,
178 }
179 }
180
181 func ensureEndpointActorAccumulator(accumulators map[string]*endpointActorAccumulator, endpointID string) *endpointActorAccumulator {
182 acc := accumulators[endpointID]
183 if acc != nil {
184 return acc
185 }
186 acc = &endpointActorAccumulator{
187 endpointID: endpointID,
188 ips: make(map[string]netip.Addr),
189 sources: make(map[string]struct{}),
190 deviceIDs: make(map[string]struct{}),
191 ifIndexes: make(map[string]struct{}),
192 ifNames: make(map[string]struct{}),
193 }
194 accumulators[endpointID] = acc
195 return acc
196 }
197
198 func addEndpointIDIdentity(acc *endpointActorAccumulator, endpointID string) {
199 if acc == nil {
200 return
201 }
202 kind, value, ok := strings.Cut(strings.TrimSpace(endpointID), ":")
203 if !ok {
204 return
205 }
206 switch strings.ToLower(strings.TrimSpace(kind)) {
207 case "mac":
208 if mac := normalizeMAC(value); mac != "" {
209 acc.mac = mac
210 }
211 case "ip":
212 if addr := parseAddr(value); addr.IsValid() {
213 acc.ips[addr.String()] = addr.Unmap()
214 }
215 }
216 }
217
218 func discoveredDeviceCount(devices []Device, localDeviceID string) int {
219 if len(devices) == 0 {
220 return 0
221 }
222
223 localDeviceID = strings.TrimSpace(localDeviceID)
224 if localDeviceID == "" {
225 return maxIntValue(len(devices)-1, 0)
226 }
227
228 count := 0
229 for _, dev := range devices {
230 if strings.TrimSpace(dev.ID) == "" {
231 continue
232 }
233 if dev.ID == localDeviceID {
234 continue
235 }
236 count++
237 }
238 return count
239 }
240
241 func maxIntValue(a, b int) int {
242 if a > b {
243 return a
244 }
245 return b
246 }