| 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 | } |