| 1 | /** |
| 2 | * @description MeshCentral Mesh Agent Local Scanner |
| 3 | * @author Ylian Saint-Hilaire |
| 4 | * @copyright Intel Corporation 2018-2022 |
| 5 | * @license Apache-2.0 |
| 6 | * @version v0.0.1 |
| 7 | */ |
| 8 | |
| 9 | /*jslint node: true */ |
| 10 | /*jshint node: true */ |
| 11 | /*jshint strict:false */ |
| 12 | /*jshint -W097 */ |
| 13 | /*jshint esversion: 6 */ |
| 14 | "use strict"; |
| 15 | |
| 16 | // Construct a Mesh Scanner object |
| 17 | // TODO: We need once "server4" and "server6" per interface, or change the default multicast interface as we send. |
| 18 | module.exports.CreateMeshScanner = function (parent) { |
| 19 | var obj = {}; |
| 20 | obj.parent = parent; |
| 21 | obj.dgram = require('dgram'); |
| 22 | obj.common = require('./common.js'); |
| 23 | obj.servers4 = {}; |
| 24 | obj.servers6 = {}; |
| 25 | obj.mainTimer = null; |
| 26 | const periodicScanTime = (60000 * 20); // Interval between scans, 20 minutes. |
| 27 | const membershipIPv4 = '239.255.255.235'; |
| 28 | const membershipIPv6 = 'FF02:0:0:0:0:0:0:FE'; |
| 29 | obj.agentCertificateHashHex = parent.certificateOperations.forge.pki.getPublicKeyFingerprint(parent.certificateOperations.forge.pki.certificateFromPem(parent.certificates.agent.cert).publicKey, { md: parent.certificateOperations.forge.md.sha384.create(), encoding: 'hex' }).toUpperCase(); |
| 30 | obj.error = 0; |
| 31 | obj.pendingOutboundPackets = []; |
| 32 | obj.pendingOutboundTimer = null; |
| 33 | |
| 34 | // Setup the multicast key if present |
| 35 | if ((typeof obj.parent.args.localdiscovery == 'object') && (typeof obj.parent.args.localdiscovery.key == 'string') && (obj.parent.args.localdiscovery.key.length > 0)) { |
| 36 | obj.multicastKey = parent.crypto.createHash('sha384').update(obj.parent.args.localdiscovery.key).digest('raw').slice(0, 32); |
| 37 | } |
| 38 | |
| 39 | // Encrypt UDP packet |
| 40 | function encryptPacket(plainPacket) { |
| 41 | if (obj.multicastKey == null) { return plainPacket; } |
| 42 | const iv = parent.crypto.randomBytes(16), aes = parent.crypto.createCipheriv('aes-256-cbc', obj.multicastKey, iv); |
| 43 | var ciphertext = aes.update(plainPacket); |
| 44 | return Buffer.concat([iv, ciphertext, aes.final()]); |
| 45 | } |
| 46 | |
| 47 | // Decrypt UDP packet |
| 48 | function decryptPacket(packet) { |
| 49 | if (obj.multicastKey == null) { return packet; } |
| 50 | if (packet.length < 17) { return null; } |
| 51 | try { |
| 52 | const iv = packet.slice(0, 16), data = packet.slice(16); |
| 53 | const aes = parent.crypto.createDecipheriv('aes-256-cbc', obj.multicastKey, iv); |
| 54 | var plaintextBytes = Buffer.from(aes.update(data)); |
| 55 | return Buffer.concat([plaintextBytes, aes.final()]); |
| 56 | } catch (ex) { return null; } |
| 57 | } |
| 58 | |
| 59 | // Get a list of IPv4 and IPv6 interface addresses |
| 60 | function getInterfaceList() { |
| 61 | var i; |
| 62 | var ipv4 = ['*'], ipv6 = ['*']; // Bind to IN_ADDR_ANY always |
| 63 | var interfaces = require('os').networkInterfaces(); |
| 64 | for (i in interfaces) { |
| 65 | var xinterface = interfaces[i]; |
| 66 | for (var j in xinterface) { |
| 67 | var interface2 = xinterface[j]; |
| 68 | if ((interface2.mac != '00:00:00:00:00:00') && (interface2.internal == false)) { |
| 69 | if (interface2.family == 'IPv4') { ipv4.push(interface2.address); } |
| 70 | if (interface2.family == 'IPv6') { ipv6.push(interface2.address + '%' + i); } |
| 71 | } |
| 72 | } |
| 73 | } |
| 74 | return { ipv4: ipv4, ipv6: ipv6 }; |
| 75 | } |
| 76 | |
| 77 | // Setup all IPv4 and IPv6 servers |
| 78 | function setupServers() { |
| 79 | var addresses = getInterfaceList(), i, localAddress, bindOptions; |
| 80 | for (i in obj.servers4) { obj.servers4[i].xxclear = true; } |
| 81 | for (i in obj.servers6) { obj.servers6[i].xxclear = true; } |
| 82 | for (i in addresses.ipv4) { |
| 83 | localAddress = addresses.ipv4[i]; |
| 84 | if (obj.servers4[localAddress] != null) { |
| 85 | // Server already exists |
| 86 | obj.servers4[localAddress].xxclear = false; |
| 87 | } else { |
| 88 | // Create a new IPv4 server |
| 89 | try { |
| 90 | var server4 = obj.dgram.createSocket({ type: 'udp4', reuseAddr: true }); |
| 91 | server4.xxclear = false; |
| 92 | server4.xxtype = 4; |
| 93 | server4.xxlocal = localAddress; |
| 94 | server4.on('error', function (err) { /*if (this.xxlocal == '*') { console.log("ERROR: Server port 16989 not available, check if server is running twice."); } this.close(); delete obj.servers6[this.xxlocal];*/ }); |
| 95 | bindOptions = { port: 16989, exclusive: true }; |
| 96 | if (server4.xxlocal != '*') { bindOptions.address = server4.xxlocal; } |
| 97 | server4.bind(bindOptions, function () { |
| 98 | try { |
| 99 | var doscan = true; |
| 100 | try { this.setBroadcast(true); this.setMulticastTTL(128); this.addMembership(membershipIPv4, this.xxlocal); } catch (e) { doscan = false; } |
| 101 | this.on('error', function (error) { /*console.log('Error: ' + error);*/ }); |
| 102 | this.on('message', function (msg, info) { onUdpPacket(msg, info, this); }); |
| 103 | if (doscan == true) { obj.performScan(this); obj.performScan(this); } |
| 104 | } catch (e) { console.log(e); } |
| 105 | }); |
| 106 | obj.servers4[localAddress] = server4; |
| 107 | } catch (e) { |
| 108 | console.log(e); |
| 109 | } |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | for (i in addresses.ipv6) { |
| 114 | localAddress = addresses.ipv6[i]; |
| 115 | if (obj.servers6[localAddress] != null) { |
| 116 | // Server already exists |
| 117 | obj.servers6[localAddress].xxclear = false; |
| 118 | } else { |
| 119 | // Create a new IPv6 server |
| 120 | try { |
| 121 | var server6 = obj.dgram.createSocket({ type: 'udp6', reuseAddr: true }); |
| 122 | server6.xxclear = false; |
| 123 | server6.xxtype = 6; |
| 124 | server6.xxlocal = localAddress; |
| 125 | server6.on('error', function (err) { /*this.close(); delete obj.servers6[this.xxlocal];*/ }); |
| 126 | bindOptions = { port: 16989, exclusive: true }; |
| 127 | if (server6.xxlocal != '*') { bindOptions.address = server6.xxlocal; } |
| 128 | server6.bind(bindOptions, function () { |
| 129 | try { |
| 130 | var doscan = true; |
| 131 | try { this.setBroadcast(true); this.setMulticastTTL(128); this.addMembership(membershipIPv6, this.xxlocal); } catch (e) { doscan = false; } |
| 132 | this.on('error', function (error) { console.log('Error: ' + error); }); |
| 133 | this.on('message', function (msg, info) { onUdpPacket(msg, info, this); }); |
| 134 | if (doscan == true) { obj.performScan(this); obj.performScan(this); } |
| 135 | } catch (e) { console.log(e); } |
| 136 | }); |
| 137 | obj.servers6[localAddress] = server6; |
| 138 | } catch (e) { |
| 139 | console.log(e); |
| 140 | } |
| 141 | } |
| 142 | } |
| 143 | |
| 144 | for (i in obj.servers4) { if (obj.servers4[i].xxclear == true) { obj.servers4[i].close(); delete obj.servers4[i]; } } |
| 145 | for (i in obj.servers6) { if (obj.servers6[i].xxclear == true) { obj.servers6[i].close(); delete obj.servers6[i]; } } |
| 146 | } |
| 147 | |
| 148 | // Clear all IPv4 and IPv6 servers |
| 149 | function clearServers() { |
| 150 | var i; |
| 151 | for (i in obj.servers4) { obj.servers4[i].close(); delete obj.servers4[i]; } |
| 152 | for (i in obj.servers6) { obj.servers6[i].close(); delete obj.servers6[i]; } |
| 153 | } |
| 154 | |
| 155 | // Start scanning for local network Mesh Agents |
| 156 | obj.start = function () { |
| 157 | if (obj.server4 != null) return; |
| 158 | |
| 159 | // Setup the local discovery values |
| 160 | var name = 'MeshCentral'; |
| 161 | var info = ''; |
| 162 | try { |
| 163 | if ((typeof obj.parent.config.domains[''].title == 'string') && (obj.parent.config.domains[''].title.length > 0)) { |
| 164 | name = obj.parent.config.domains[''].title; info = ''; |
| 165 | try { |
| 166 | if ((typeof obj.parent.config.domains[''].title2 == 'string') && (obj.parent.config.domains[''].title2.length > 0)) { |
| 167 | info = obj.common.replacePlaceholders(obj.parent.config.domains[''].title2, { |
| 168 | 'serverversion': obj.parent.currentVer, |
| 169 | 'servername': obj.getWebServerName(domain, req), |
| 170 | 'agentsessions': Object.keys(parent.webserver.wsagents).length, |
| 171 | 'connectedusers': Object.keys(parent.webserver.wssessions).length, |
| 172 | 'userssessions': Object.keys(parent.webserver.wssessions2).length, |
| 173 | 'relaysessions': parent.webserver.relaySessionCount, |
| 174 | 'relaycount': Object.keys(parent.webserver.wsrelays).length |
| 175 | }); |
| 176 | } |
| 177 | } catch (ex) { } |
| 178 | } |
| 179 | } catch (ex) { } |
| 180 | try { |
| 181 | if ((typeof obj.parent.args.localdiscovery.name == 'string') && (obj.parent.args.localdiscovery.name.length > 0)) { |
| 182 | name = obj.parent.args.localdiscovery.name; info = ''; |
| 183 | try { if ((typeof obj.parent.args.localdiscovery.info == 'string') && (obj.parent.args.localdiscovery.info.length > 0)) { info = obj.parent.args.localdiscovery.info; } } catch (ex) { } |
| 184 | } |
| 185 | } catch (ex) { } |
| 186 | if (info == '') { info = parent.certificates.CommonName; } |
| 187 | |
| 188 | // Figure out the correct websocket port |
| 189 | var port = (parent.args.aliasport)?parent.args.aliasport:parent.args.port; |
| 190 | |
| 191 | // Build the IPv4 response |
| 192 | var url = 'wss://%s:' + port + '/agent.ashx'; |
| 193 | obj.multicastPacket4 = Buffer.from("MeshCentral2|" + obj.agentCertificateHashHex + '|' + url, 'ascii'); |
| 194 | if (parent.certificates.CommonName.indexOf('.') != -1) { url = 'wss://' + parent.certificates.CommonName + ':' + port + '/agent.ashx'; } |
| 195 | obj.multicastPacket4x = Buffer.from("MeshCentral2|" + obj.agentCertificateHashHex + '|' + url + '|' + name + '|' + info, 'ascii'); |
| 196 | |
| 197 | // Build the IPv6 response |
| 198 | url = 'wss://[%s]:' + port + '/agent.ashx'; |
| 199 | obj.multicastPacket6 = Buffer.from("MeshCentral2|" + obj.agentCertificateHashHex + '|' + url, 'ascii'); |
| 200 | if (parent.certificates.CommonName.indexOf('.') != -1) { url = 'wss://' + parent.certificates.CommonName + ':' + port + '/agent.ashx'; } |
| 201 | obj.multicastPacket6x = Buffer.from("MeshCentral2|" + obj.agentCertificateHashHex + '|' + url + '|' + name + '|' + info, 'ascii'); |
| 202 | |
| 203 | setupServers(); |
| 204 | obj.mainTimer = setInterval(obj.performScan, periodicScanTime); |
| 205 | return obj; |
| 206 | }; |
| 207 | |
| 208 | // Stop scanning for local network Mesh Agents |
| 209 | obj.stop = function () { |
| 210 | if (obj.mainTimer != null) { clearInterval(obj.mainTimer); obj.mainTimer = null; } |
| 211 | clearServers(); |
| 212 | }; |
| 213 | |
| 214 | // Look for all Mesh Agents that may be locally reachable, indicating the presense of this server. |
| 215 | obj.performScan = function (server) { |
| 216 | var i; |
| 217 | if (server != null) { |
| 218 | if (server.xxtype == 4) { var p = encryptPacket(obj.multicastPacket4); try { server.send(p, 0, p.length, 16990, membershipIPv4); } catch (e) { } } |
| 219 | if (server.xxtype == 6) { var p = encryptPacket(obj.multicastPacket6); try { server.send(p, 0, p.length, 16990, membershipIPv6); } catch (e) { } } |
| 220 | if ((server.xxtype == 4) && (server.xxlocal == '*')) { var p = encryptPacket(obj.multicastPacket4); try { server.send(p, 0, p.length, 16990, '127.0.0.1'); } catch (e) { } try { server.send(p, 0, p.length, 16990, '255.255.255.255'); } catch (e) { } } |
| 221 | if ((server.xxtype == 6) && (server.xxlocal == '*')) { var p = encryptPacket(obj.multicastPacket6); try { server.send(p, 0, p.length, 16990, '::1'); } catch (e) { } } |
| 222 | } else { |
| 223 | for (i in obj.servers4) { var p = encryptPacket(obj.multicastPacket4); try { obj.servers4[i].send(p, 0, p.length, 16990, membershipIPv4); } catch (e) { } } |
| 224 | for (i in obj.servers6) { var p = encryptPacket(obj.multicastPacket6); try { obj.servers6[i].send(p, 0, p.length, 16990, membershipIPv6); } catch (e) { } } |
| 225 | setupServers(); // Check if any network interfaces where added or removed |
| 226 | } |
| 227 | }; |
| 228 | |
| 229 | // Called when a UDP packet is received from an agent. |
| 230 | function onUdpPacket(msg, info, server) { |
| 231 | // Decrypt the packet if needed |
| 232 | if ((msg = decryptPacket(msg)) == null) return; |
| 233 | |
| 234 | //console.log('Received ' + msg.length + ' bytes from ' + info.address + ':' + info.port + ', on interface: ' + server.xxlocal + '.'); |
| 235 | if ((msg.length == 96) && (msg.toString('ascii') == obj.agentCertificateHashHex)) { |
| 236 | if (server.xxtype == 4) { var p = encryptPacket(obj.multicastPacket4); try { server.send(p, 0, p.length, info.port, info.address); } catch (e) { } } |
| 237 | if (server.xxtype == 6) { var p = encryptPacket(obj.multicastPacket6); try { server.send(p, 0, p.length, info.port, info.address); } catch (e) { } } |
| 238 | } else if (msg.toString('ascii') == 'MeshServerScan') { |
| 239 | if (server.xxtype == 4) { var p = encryptPacket(obj.multicastPacket4x); try { server.send(p, 0, p.length, info.port, info.address); } catch (e) { } } |
| 240 | if (server.xxtype == 6) { var p = encryptPacket(obj.multicastPacket6x); try { server.send(p, 0, p.length, info.port, info.address); } catch (e) { } } |
| 241 | } |
| 242 | } |
| 243 | |
| 244 | // Send the next packet in the pending list, stop the timer if we are done. |
| 245 | function sendPendingPacket() { |
| 246 | if (obj.pendingOutboundPackets.length == 0) { if (obj.pendingOutboundTimer != null) { clearInterval(obj.pendingOutboundTimer); obj.pendingOutboundTimer = null; } return; } |
| 247 | var packet = obj.pendingOutboundPackets.shift(); |
| 248 | if (packet != null) { packet[0].send(packet[1], 0, packet[1].length, packet[2], packet[3]); } |
| 249 | } |
| 250 | |
| 251 | // As a side job, we also send server wake-on-lan packets |
| 252 | obj.wakeOnLan = function (macs, host) { |
| 253 | var i, j, futureTime = 0; |
| 254 | for (i in macs) { |
| 255 | var mac = macs[i].split(':').join(''); |
| 256 | var hexpacket = 'FFFFFFFFFFFF'; |
| 257 | for (j = 0; j < 16; j++) { hexpacket += mac; } |
| 258 | var wakepacket = Buffer.from(hexpacket, 'hex'); |
| 259 | |
| 260 | // Add all wake packets to the pending list |
| 261 | for (var k = 0; k < 5; k++) { |
| 262 | for (j in obj.servers4) { |
| 263 | obj.pendingOutboundPackets.push([obj.servers4[j], wakepacket, 7, '255.255.255.255']); // IPv4 Broadcast |
| 264 | obj.pendingOutboundPackets.push([obj.servers4[j], wakepacket, 16990, membershipIPv4]); // IPv4 Multicast |
| 265 | if (host != null) { obj.pendingOutboundPackets.push([obj.servers4[j], wakepacket, 7, host]); } // IPv4 Directed |
| 266 | } |
| 267 | for (j in obj.servers6) { |
| 268 | obj.pendingOutboundPackets.push([obj.servers6[j], wakepacket, 16990, membershipIPv6]); // IPv6 Multicast |
| 269 | } |
| 270 | } |
| 271 | |
| 272 | // Send each packet at 10ms interval |
| 273 | // This packet spacing is absolutly required, otherwise the outbound buffer gets filled up and packets get lost which often causes the machine not to wake. |
| 274 | if (obj.pendingOutboundTimer == null) { obj.pendingOutboundTimer = setInterval(sendPendingPacket, 10); } |
| 275 | } |
| 276 | }; |
| 277 | |
| 278 | return obj; |
| 279 | }; |