fixed LLDP based topology discovery issues on some devices (mostly D-Link)
[public/netxms.git] / src / server / core / lldp.cpp
1 /*
2 ** NetXMS - Network Management System
3 ** Copyright (C) 2003-2016 Victor Kirhenshtein
4 **
5 ** This program is free software; you can redistribute it and/or modify
6 ** it under the terms of the GNU General Public License as published by
7 ** the Free Software Foundation; either version 2 of the License, or
8 ** (at your option) any later version.
9 **
10 ** This program is distributed in the hope that it will be useful,
11 ** but WITHOUT ANY WARRANTY; without even the implied warranty of
12 ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 ** GNU General Public License for more details.
14 **
15 ** You should have received a copy of the GNU General Public License
16 ** along with this program; if not, write to the Free Software
17 ** Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
18 **
19 ** File: lldp.cpp
20 **
21 **/
22
23 #include "nxcore.h"
24
25 /**
26 * Handler for walking local port table
27 */
28 static UINT32 PortLocalInfoHandler(UINT32 snmpVersion, SNMP_Variable *var, SNMP_Transport *transport, void *arg)
29 {
30 LLDP_LOCAL_PORT_INFO *port = new LLDP_LOCAL_PORT_INFO;
31 port->portNumber = var->getName()->getValue()[11];
32 port->localIdLen = var->getRawValue(port->localId, 256);
33
34 SNMP_ObjectId *oid = var->getName();
35 UINT32 newOid[128];
36 memcpy(newOid, oid->getValue(), oid->getLength() * sizeof(UINT32));
37 SNMP_PDU *pRqPDU = new SNMP_PDU(SNMP_GET_REQUEST, SnmpNewRequestId(), snmpVersion);
38
39 newOid[oid->getLength() - 2] = 4; // lldpLocPortDescr
40 pRqPDU->bindVariable(new SNMP_Variable(newOid, oid->getLength()));
41
42 newOid[oid->getLength() - 2] = 2; // lldpLocPortIdSubtype
43 pRqPDU->bindVariable(new SNMP_Variable(newOid, oid->getLength()));
44
45 SNMP_PDU *pRespPDU = NULL;
46 UINT32 rcc = transport->doRequest(pRqPDU, &pRespPDU, SnmpGetDefaultTimeout(), 3);
47 delete pRqPDU;
48 if (rcc == SNMP_ERR_SUCCESS)
49 {
50 if (pRespPDU->getNumVariables() >= 2)
51 {
52 pRespPDU->getVariable(0)->getValueAsString(port->ifDescr, 192);
53 port->localIdSubtype = pRespPDU->getVariable(1)->getValueAsUInt();
54 }
55 delete pRespPDU;
56 }
57 else
58 {
59 _tcscpy(port->ifDescr, _T("###error###"));
60 }
61
62 ((ObjectArray<LLDP_LOCAL_PORT_INFO> *)arg)->add(port);
63 return SNMP_ERR_SUCCESS;
64 }
65
66 /**
67 * Get information about LLDP local ports
68 */
69 ObjectArray<LLDP_LOCAL_PORT_INFO> *GetLLDPLocalPortInfo(SNMP_Transport *snmp)
70 {
71 ObjectArray<LLDP_LOCAL_PORT_INFO> *ports = new ObjectArray<LLDP_LOCAL_PORT_INFO>(64, 64, true);
72 if (SnmpWalk(snmp->getSnmpVersion(), snmp, _T(".1.0.8802.1.1.2.1.3.7.1.3"), PortLocalInfoHandler, ports, FALSE) != SNMP_ERR_SUCCESS)
73 {
74 delete ports;
75 return NULL;
76 }
77 return ports;
78 }
79
80 /**
81 * Find remote interface
82 *
83 * @param node remote node
84 * @param idType port ID type (value of lldpRemPortIdSubtype)
85 * @param id port ID
86 * @param idLen port ID length in bytes
87 */
88 static Interface *FindRemoteInterface(Node *node, UINT32 idType, BYTE *id, size_t idLen)
89 {
90 LLDP_LOCAL_PORT_INFO port;
91 TCHAR ifName[130], buffer[256];
92 Interface *ifc;
93
94 DbgPrintf(5, _T("LLDPTopoHandler(%s [%d]): FindRemoteInterface: idType=%d id=%s (%d)"), node->getName(), node->getId(), idType, BinToStr(id, idLen, buffer), (int)idLen);
95
96 // Try local LLDP port info first
97 if (node->getLldpLocalPortInfo(idType, id, idLen, &port))
98 {
99 DbgPrintf(5, _T("LLDPTopoHandler(%s [%d]): FindRemoteInterface: getLldpLocalPortInfo found port: %d \"%s\""), node->getName(), node->getId(), port.portNumber, port.ifDescr);
100 if (node->isBridge())
101 ifc = node->findBridgePort(port.portNumber);
102 else
103 ifc = node->findInterfaceByIndex(port.portNumber);
104 if (ifc == NULL) // unable to find interface by bridge port number or interface index, try description
105 ifc = node->findInterfaceByName(port.ifDescr); /* TODO: find by cached ifName value */
106 if (ifc != NULL)
107 return ifc;
108 }
109
110 DbgPrintf(5, _T("LLDPTopoHandler(%s [%d]): FindRemoteInterface: interface not found by getLldpLocalPortInfo"), node->getName(), node->getId());
111 switch(idType)
112 {
113 case 3: // MAC address
114 return node->findInterfaceByMAC(id);
115 case 4: // Network address
116 if (id[0] == 1) // IPv4
117 {
118 UINT32 ipAddr;
119 memcpy(&ipAddr, &id[1], sizeof(UINT32));
120 return node->findInterfaceByIP(ntohl(ipAddr));
121 }
122 return NULL;
123 case 5: // Interface name
124 #ifdef UNICODE
125 MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, (char *)id, (int)idLen, ifName, 128);
126 ifName[min(idLen, 127)] = 0;
127 #else
128 {
129 int len = min(idLen, 127);
130 memcpy(ifName, id, len);
131 ifName[len] = 0;
132 }
133 #endif
134 ifc = node->findInterfaceByName(ifName); /* TODO: find by cached ifName value */
135 if ((ifc == NULL) && !_tcsncmp(node->getObjectId(), _T(".1.3.6.1.4.1.1916.2"), 19))
136 {
137 // Hack for Extreme Networks switches
138 // Must be moved into driver
139 memmove(&ifName[2], ifName, (_tcslen(ifName) + 1) * sizeof(TCHAR));
140 ifName[0] = _T('1');
141 ifName[1] = _T(':');
142 ifc = node->findInterfaceByName(ifName);
143 }
144 return ifc;
145 case 7: // local identifier
146 return NULL; // already tried to find port using local info
147 default:
148 return NULL;
149 }
150 }
151
152 /**
153 * Get variable from cache
154 */
155 static SNMP_Variable *GetVariableFromCache(UINT32 *oid, size_t oidLen, StringObjectMap<SNMP_Variable> *cache)
156 {
157 TCHAR oidText[MAX_OID_LEN * 6];
158 SNMPConvertOIDToText(oidLen, oid, oidText, MAX_OID_LEN * 6);
159 return cache->get(oidText);
160 }
161
162 /**
163 * Process LLDP connection database entry
164 */
165 static void ProcessLLDPConnectionEntry(Node *node, StringObjectMap<SNMP_Variable> *connections, SNMP_Variable *var, LinkLayerNeighbors *nbs)
166 {
167 SNMP_ObjectId *oid = var->getName();
168
169 // Get additional info for current record
170 UINT32 newOid[128];
171 memcpy(newOid, oid->getValue(), oid->getLength() * sizeof(UINT32));
172
173 newOid[oid->getLength() - 4] = 4; // lldpRemChassisIdSubtype
174 SNMP_Variable *lldpRemChassisIdSubtype = GetVariableFromCache(newOid, oid->getLength(), connections);
175
176 newOid[oid->getLength() - 4] = 7; // lldpRemPortId
177 SNMP_Variable *lldpRemPortId = GetVariableFromCache(newOid, oid->getLength(), connections);
178
179 newOid[oid->getLength() - 4] = 6; // lldpRemPortIdSubtype
180 SNMP_Variable *lldpRemPortIdSubtype = GetVariableFromCache(newOid, oid->getLength(), connections);
181
182 newOid[oid->getLength() - 4] = 8; // lldpRemPortDesc
183 SNMP_Variable *lldpRemPortDesc = GetVariableFromCache(newOid, oid->getLength(), connections);
184
185 newOid[oid->getLength() - 4] = 9; // lldpRemSysName
186 SNMP_Variable *lldpRemSysName = GetVariableFromCache(newOid, oid->getLength(), connections);
187
188 if ((lldpRemChassisIdSubtype != NULL) && (lldpRemPortId != NULL) && (lldpRemPortIdSubtype != NULL) && (lldpRemPortDesc != NULL) && (lldpRemSysName != NULL))
189 {
190 // Build LLDP ID for remote system
191 TCHAR remoteId[256];
192 BuildLldpId(lldpRemChassisIdSubtype->getValueAsInt(), var->getValue(), (int)var->getValueLength(), remoteId, 256);
193 Node *remoteNode = FindNodeByLLDPId(remoteId);
194
195 // Try to find node by sysName as fallback
196 if (remoteNode == NULL)
197 {
198 TCHAR sysName[256] = _T("");
199 lldpRemSysName->getValueAsString(sysName, 256);
200 Trim(sysName);
201 DbgPrintf(5, _T("ProcessLLDPConnectionEntry(%s [%d]): remoteId=%s: FindNodeByLLDPId failed, fallback to sysName (\"%s\")"), node->getName(), node->getId(), remoteId, sysName);
202 remoteNode = FindNodeBySysName(sysName);
203 }
204
205 if (remoteNode != NULL)
206 {
207 DbgPrintf(5, _T("ProcessLLDPConnectionEntry(%s [%d]): remoteId=%s remoteNode=%s [%d]"), node->getName(), node->getId(), remoteId, remoteNode->getName(), remoteNode->getId());
208
209 BYTE remoteIfId[1024];
210 size_t remoteIfIdLen = lldpRemPortId->getRawValue(remoteIfId, 1024);
211 Interface *ifRemote = FindRemoteInterface(remoteNode, lldpRemPortIdSubtype->getValueAsUInt(), remoteIfId, remoteIfIdLen);
212 if (ifRemote == NULL)
213 {
214 // Try to find remote interface by description
215 TCHAR *ifDescr = lldpRemPortDesc->getValueAsString((TCHAR *)remoteIfId, 1024 / sizeof(TCHAR));
216 Trim(ifDescr);
217 DbgPrintf(5, _T("ProcessLLDPConnectionEntry(%s [%d]): FindRemoteInterface failed, lookup by description (\"%s\")"), node->getName(), node->getId(), CHECK_NULL(ifDescr));
218 if (ifDescr != NULL)
219 ifRemote = remoteNode->findInterfaceByName(ifDescr);
220 }
221
222 LL_NEIGHBOR_INFO info;
223
224 info.objectId = remoteNode->getId();
225 info.ifRemote = (ifRemote != NULL) ? ifRemote->getIfIndex() : 0;
226 info.isPtToPt = true;
227 info.protocol = LL_PROTO_LLDP;
228 info.isCached = false;
229
230 // Index to lldpRemTable is lldpRemTimeMark, lldpRemLocalPortNum, lldpRemIndex
231 UINT32 localPort = oid->getValue()[oid->getLength() - 2];
232
233 // Determine interface index from local port number. It can be
234 // either ifIndex or dot1dBasePort, as described in LLDP MIB:
235 // A port number has no mandatory relationship to an
236 // InterfaceIndex object (of the interfaces MIB, IETF RFC 2863).
237 // If the LLDP agent is a IEEE 802.1D, IEEE 802.1Q bridge, the
238 // LldpPortNumber will have the same value as the dot1dBasePort
239 // object (defined in IETF RFC 1493) associated corresponding
240 // bridge port. If the system hosting LLDP agent is not an
241 // IEEE 802.1D or an IEEE 802.1Q bridge, the LldpPortNumber
242 // will have the same value as the corresponding interface's
243 // InterfaceIndex object.
244 if (node->isBridge())
245 {
246 Interface *localIf = node->findBridgePort(localPort);
247 if (localIf != NULL)
248 info.ifLocal = localIf->getIfIndex();
249 DbgPrintf(5, _T("ProcessLLDPConnectionEntry(%s [%d]): lookup bridge port: localPort=%d iface=%s"), node->getName(), node->getId(), localPort, (localIf != NULL) ? localIf->getName() : _T("(null)"));
250 }
251 else
252 {
253 info.ifLocal = localPort;
254 }
255
256 nbs->addConnection(&info);
257 DbgPrintf(5, _T("ProcessLLDPConnectionEntry(%s [%d]): added connection: objectId=%d ifRemote=%d ifLocal=%d"), node->getName(), node->getId(), info.objectId, info.ifRemote, info.ifLocal);
258 }
259 else
260 {
261 DbgPrintf(5, _T("ProcessLLDPConnectionEntry(%s [%d]): remoteId=%s: remote node not found"), node->getName(), node->getId(), remoteId);
262 }
263 }
264 else
265 {
266 TCHAR remoteId[256];
267 BinToStr(var->getValue(), var->getValueLength(), remoteId);
268 DbgPrintf(5, _T("ProcessLLDPConnectionEntry(%s [%d]): SNMP get failed for remote ID %s"), node->getName(), node->getId(), remoteId);
269 }
270 }
271
272 /**
273 * Topology table walker's callback for LLDP topology table
274 */
275 static UINT32 LLDPTopoHandler(UINT32 snmpVersion, SNMP_Variable *var, SNMP_Transport *transport, void *arg)
276 {
277 ((StringObjectMap<SNMP_Variable> *)arg)->set(var->getName()->getValueAsText(), new SNMP_Variable(var));
278 return SNMP_ERR_SUCCESS;
279 }
280
281 /**
282 * Add LLDP-discovered neighbors
283 */
284 void AddLLDPNeighbors(Node *node, LinkLayerNeighbors *nbs)
285 {
286 if (!(node->getFlags() & NF_IS_LLDP))
287 return;
288
289 DbgPrintf(5, _T("LLDP: collecting topology information for node %s [%d]"), node->getName(), node->getId());
290
291 // Entire table should be cached before processing because some devices (D-Link for example)
292 // do not allow GET requests for table elements
293 StringObjectMap<SNMP_Variable> connections(true);
294 node->callSnmpEnumerate(_T(".1.0.8802.1.1.2.1.4.1.1"), LLDPTopoHandler, &connections);
295 if (connections.size() > 0)
296 {
297 DbgPrintf(5, _T("LLDP: %d entries in connection database for node %s [%d]"), connections.size(), node->getName(), node->getId());
298 StringList *oids = connections.keys();
299 for(int i = 0; i < oids->size(); i++)
300 {
301 const TCHAR *oid = oids->get(i);
302 if (_tcsncmp(oid, _T(".1.0.8802.1.1.2.1.4.1.1.5."), 26))
303 continue;
304 SNMP_Variable *var = connections.get(oid);
305 ProcessLLDPConnectionEntry(node, &connections, var, nbs);
306 }
307 delete oids;
308 }
309 else
310 {
311 DbgPrintf(5, _T("LLDP: connection database empty for node %s [%d]"), node->getName(), node->getId());
312 }
313 DbgPrintf(5, _T("LLDP: finished collecting topology information for node %s [%d]"), node->getName(), node->getId());
314 }
315
316 /**
317 * Parse MAC address. Could be without separators or with any separator char.
318 */
319 static bool ParseMACAddress(const char *text, int length, BYTE *mac, int *macLength)
320 {
321 bool withSeparator = false;
322 char separator = 0;
323 int p = 0;
324 bool hi = true;
325 for(int i = 0; (i < length) && (p < 64); i++)
326 {
327 char c = toupper(text[i]);
328 if ((i % 3 == 2) && withSeparator)
329 {
330 if (c != separator)
331 return false;
332 continue;
333 }
334 if (!isdigit(c) && ((c < 'A') || (c > 'F')))
335 {
336 if (i == 2)
337 {
338 withSeparator = true;
339 separator = c;
340 continue;
341 }
342 return false;
343 }
344 if (hi)
345 {
346 mac[p] = (isdigit(c) ? (c - '0') : (c - 'A' + 10)) << 4;
347 hi = false;
348 }
349 else
350 {
351 mac[p] |= (isdigit(c) ? (c - '0') : (c - 'A' + 10));
352 p++;
353 hi = true;
354 }
355 }
356 *macLength = p;
357 return true;
358 }
359
360 /**
361 * Build LLDP ID for node
362 */
363 void BuildLldpId(int type, const BYTE *data, int length, TCHAR *id, int idLen)
364 {
365 _sntprintf(id, idLen, _T("%d@"), type);
366 if (type == 4)
367 {
368 // Some D-Link switches returns MAC address for ID type 4 as formatted text instead of raw bytes
369 BYTE macAddr[64];
370 int macLength;
371 if ((length >= MAC_ADDR_LENGTH * 2) && ParseMACAddress((const char *)data, length, macAddr, &macLength))
372 {
373 BinToStr(macAddr, macLength, &id[_tcslen(id)]);
374 }
375 else
376 {
377 BinToStr(data, length, &id[_tcslen(id)]);
378 }
379 }
380 else
381 {
382 BinToStr(data, length, &id[_tcslen(id)]);
383 }
384 }