From 4594b4f6f6de17396a92287db3962854539bfe3a Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Fri, 9 Oct 2020 15:44:09 -0700 Subject: [PATCH] Many Intel AMT improvements. --- MeshCentralServer.njsproj | 1 - agents/meshcore.js | 37 +++--- agents/modules_meshcore/apfclient.js | 16 ++- amt/amt-redir-mesh.js | 5 +- amt/amt-wsman-comm.js | 18 +-- amtmanager.js | 170 +++++++++++++++++---------- amtscanner.js | 4 +- meshagent.js | 2 +- meshcentral.js | 5 +- meshuser.js | 30 ++++- mpsserver.js | 170 ++++++++++++++++++++------- webserver.js | 6 +- 12 files changed, 308 insertions(+), 156 deletions(-) diff --git a/MeshCentralServer.njsproj b/MeshCentralServer.njsproj index 81ea687c..4df7bd76 100644 --- a/MeshCentralServer.njsproj +++ b/MeshCentralServer.njsproj @@ -100,7 +100,6 @@ - diff --git a/agents/meshcore.js b/agents/meshcore.js index ae37219e..1d1381de 100644 --- a/agents/meshcore.js +++ b/agents/meshcore.js @@ -3507,15 +3507,20 @@ function createMeshCore(agent) { mpskeepalive: 60000, clientname: require('os').hostname(), clientaddress: '127.0.0.1', - clientuuid: meshCoreObj.intelamt.uuid + clientuuid: meshCoreObj.intelamt.uuid, + conntype: 2 // 0 = CIRA, 1 = Relay, 2 = LMS. The correct value is 2 since we are performing an LMS relay, other values for testing. }; - var tobj = { debug: false }; // - apftunnel = require('apfclient')(tobj, apfarg); - try { - apftunnel.connect(); - response += "..success"; - } catch (e) { - response += JSON.stringify(e); + if ((apfarg.clientuuid == null) || (apfarg.clientuuid.length != 36)) { + response = "Unable to get Intel AMT UUID: " + apfarg.clientuuid; + } else { + var tobj = { debug: false }; + apftunnel = require('apfclient')(tobj, apfarg); + try { + apftunnel.connect(); + response += "...success"; + } catch (e) { + response += JSON.stringify(e); + } } } else if (args['_'][0] == 'off') { response = "Stopping APF tunnel"; @@ -3640,18 +3645,10 @@ function createMeshCore(agent) { { switch(amt.lmsstate) { - case 0: - intelamt.microlms = 'DISABLED' - break; - case 1: - intelamt.microlms = 'CONNECTING' - break; - case 2: - intelamt.microlms = 'CONNECTED' - break; - default: - intelamt.microlms = 'unknown' - break; + case 0: intelamt.microlms = 'DISABLED'; break; + case 1: intelamt.microlms = 'CONNECTING'; break; + case 2: intelamt.microlms = 'CONNECTED'; break; + default: intelamt.microlms = 'unknown'; break; } } var p = false; diff --git a/agents/modules_meshcore/apfclient.js b/agents/modules_meshcore/apfclient.js index 1eacd2d7..de7a1f2f 100644 --- a/agents/modules_meshcore/apfclient.js +++ b/agents/modules_meshcore/apfclient.js @@ -7,6 +7,8 @@ */ function CreateAPFClient(parent, args) { + if ((args.clientuuid == null) || (args.clientuuid.length != 36)) return null; // Require a UUID if this exact length + var obj = {}; obj.parent = parent; obj.args = args; @@ -57,7 +59,7 @@ function CreateAPFClient(parent, args) { // Intel AMT forwarded port list for non-TLS mode //var pfwd_ports = [16992, 623, 16994, 5900]; - var pfwd_ports = [ 16992 ]; + var pfwd_ports = [ 16992, 16993 ]; // protocol definitions var APFProtocol = { @@ -81,7 +83,8 @@ function CreateAPFClient(parent, args) { KEEPALIVE_REQUEST: 208, KEEPALIVE_REPLY: 209, KEEPALIVE_OPTIONS_REQUEST: 210, - KEEPALIVE_OPTIONS_REPLY: 211 + KEEPALIVE_OPTIONS_REPLY: 211, + MESH_CONNECTION_TYPE: 250 // This is a Mesh specific command that instructs the server of the connection type: 1 = Relay, 2 = LMS. } var APFDisconnectCode = { @@ -160,13 +163,18 @@ function CreateAPFClient(parent, args) { }); obj.state = CIRASTATE.INITIAL; + if (typeof obj.args.conntype == 'number') { SendConnectionType(obj.forwardClient.ws, obj.args.conntype); } SendProtocolVersion(obj.forwardClient.ws, obj.args.clientuuid); SendServiceRequest(obj.forwardClient.ws, 'auth@amt.intel.com'); } + function SendConnectionType(socket, type) { + socket.write(String.fromCharCode(APFProtocol.MESH_CONNECTION_TYPE) + IntToStr(type)); + Debug("APF: Send connection type " + type); + } + function SendProtocolVersion(socket, uuid) { - var buuid = strToGuid(uuid); - var data = String.fromCharCode(APFProtocol.PROTOCOLVERSION) + '' + IntToStr(1) + IntToStr(0) + IntToStr(0) + hex2rstr(buuid) + binzerostring(64); + var data = String.fromCharCode(APFProtocol.PROTOCOLVERSION) + IntToStr(1) + IntToStr(0) + IntToStr(0) + hex2rstr(strToGuid(uuid)) + binzerostring(64); socket.write(data); Debug("APF: Send protocol version 1 0 " + uuid); obj.cirastate = CIRASTATE.PROTOCOL_VERSION_SENT; diff --git a/amt/amt-redir-mesh.js b/amt/amt-redir-mesh.js index 5f1baf5b..fdd2151f 100644 --- a/amt/amt-redir-mesh.js +++ b/amt/amt-redir-mesh.js @@ -113,11 +113,10 @@ module.exports.CreateAmtRedirect = function (module, domain, user, webserver, me */ // If Intel AMT CIRA connection is available, use it - if (((conn & 2) != 0) && (meshcentral.mpsserver.ciraConnections[nodeid] != null)) { + var ciraconn = meshcentral.mpsserver.GetConnectionToNode(nodeid, null, true); // Request an OOB connection + if (ciraconn != null) { Debug(1, 'Opening Intel AMT CIRA transport connection to ' + nodeid + '.'); - var ciraconn = meshcentral.mpsserver.ciraConnections[nodeid]; - // Compute target port, look at the CIRA port mappings, if non-TLS is allowed, use that, if not use TLS var port = 16995; if (ciraconn.tag.boundPorts.indexOf(16994) >= 0) port = 16994; // RELEASE: Always use non-TLS mode if available within CIRA diff --git a/amt/amt-wsman-comm.js b/amt/amt-wsman-comm.js index 7e50c320..aec168d7 100644 --- a/amt/amt-wsman-comm.js +++ b/amt/amt-wsman-comm.js @@ -5,7 +5,7 @@ */ // Construct a MeshServer object -var CreateWsmanComm = function (host, port, user, pass, tls, tlsoptions, transportServer) { +var CreateWsmanComm = function (host, port, user, pass, tls, tlsoptions, ciraConnection) { //console.log('CreateWsmanComm', host, port, user, pass, tls, tlsoptions); var obj = {}; @@ -38,7 +38,7 @@ var CreateWsmanComm = function (host, port, user, pass, tls, tlsoptions, transpo obj.pass = pass; obj.xtls = tls; obj.xtlsoptions = tlsoptions; - obj.transportServer = transportServer; // This can be a CIRA or APF server, if null, local sockets are used as transport. + obj.ciraConnection = ciraConnection; // This can be a CIRA or APF server, if null, local sockets are used as transport. obj.xtlsFingerprint; obj.xtlsCertificate = null; obj.xtlsCheck = 0; // 0 = No TLS, 1 = CA Checked, 2 = Pinned, 3 = Untrusted @@ -166,9 +166,9 @@ var CreateWsmanComm = function (host, port, user, pass, tls, tlsoptions, transpo obj.socketState = 1; obj.kerberosDone = 0; - if (obj.transportServer != null) { - // Setup a new channel using the transport server (CIRA or APF) - obj.socket = obj.transportServer.SetupChannelToNode(obj.host, obj.port); + if (obj.ciraConnection != null) { + // Setup a new channel using the CIRA/Relay/LMS connection + obj.socket = obj.ciraConnection.SetupChannel(obj.port); if (obj.socket == null) { try { obj.xxOnSocketClosed(); } catch (e) { } } else { @@ -229,7 +229,7 @@ var CreateWsmanComm = function (host, port, user, pass, tls, tlsoptions, transpo obj.xxOnSocketConnected = function () { if (obj.socket == null) return; // check TLS certificate for webrelay and direct only - if ((obj.transportServer == null) && (obj.xtls == 1)) { + if ((obj.ciraConnection == null) && (obj.xtls == 1)) { obj.xtlsCertificate = obj.socket.getPeerCertificate(); // ###BEGIN###{Certificates} @@ -348,7 +348,7 @@ var CreateWsmanComm = function (host, port, user, pass, tls, tlsoptions, transpo if (isNaN(s)) s = 500; if (s == 401 && ++(obj.authcounter) < 3) { obj.challengeParams = obj.parseDigest(header['www-authenticate']); // Set the digest parameters, after this, the socket will close and we will auto-retry - if (obj.transportServer == null) { obj.socket.end(); } else { obj.socket.close(); } + if (obj.ciraConnection == null) { obj.socket.end(); } else { obj.socket.close(); } } else { var r = obj.pendingAjaxCall.shift(); if (r == null || r.length < 1) { console.log("pendingAjaxCall error, " + r); return; } @@ -365,7 +365,7 @@ var CreateWsmanComm = function (host, port, user, pass, tls, tlsoptions, transpo //obj.Debug("xxOnSocketClosed"); obj.socketState = 0; if (obj.socket != null) { - if (obj.transportServer == null) { obj.socket.destroy(); } else { obj.socket.close(); } + if (obj.ciraConnection == null) { obj.socket.destroy(); } else { obj.socket.close(); } obj.socket = null; } if (obj.pendingAjaxCall.length > 0) { @@ -376,7 +376,7 @@ var CreateWsmanComm = function (host, port, user, pass, tls, tlsoptions, transpo obj.xxOnSocketTimeout = function () { if (obj.socket != null) { - if (obj.transportServer == null) { obj.socket.destroy(); } else { obj.socket.close(); } + if (obj.ciraConnection == null) { obj.socket.destroy(); } else { obj.socket.close(); } obj.socket = null; } } diff --git a/amtmanager.js b/amtmanager.js index 1acf0331..a9d2189f 100644 --- a/amtmanager.js +++ b/amtmanager.js @@ -41,67 +41,72 @@ module.exports.CreateAmtManager = function(parent) { // Handle server events // TODO: Only manage devices with connections to this server. In a multi-server setup, we don't want multiple managers talking to the same device. obj.HandleEvent = function (source, event, ids, id) { - // React to nodes connecting and disconnecting - if (event.action == 'nodeconnect') { - if ((event.conn & 14) != 0) { // connectType: Bitmask, 1 = MeshAgent, 2 = Intel AMT CIRA, 4 = Intel AMT local, 8 = Intel AMT Relay, 16 = MQTT - //if ((event.conn & 2) == 0) return // Debug: Only look at CIRA connections - - // We have an OOB connection to Intel AMT, update our information + switch (event.action) { + case 'nodeconnect': { // React to nodes connecting and disconnecting + // See if we have an existing device we manage var dev = obj.amtDevices[event.nodeid]; - if (dev == null) { obj.amtDevices[event.nodeid] = dev = { conn: event.conn }; fetchIntelAmtInformation(event.nodeid); } else { dev.conn = event.conn; } - /* - } else if (((event.conn & 1) != 0) && (parent.webserver != null)) { - // We have an agent connection without OOB, check if this agent supports Intel AMT - var agent = parent.webserver.wsagents[event.nodeid]; - if ((agent == null) || (agent.agentInfo == null) || (parent.meshAgentsArchitectureNumbers[agent.agentInfo.agentId].amt == false)) { removeDevice(event.nodeid); return; } - var dev = obj.amtDevices[event.nodeid]; - if (dev == null) { obj.amtDevices[event.nodeid] = dev = { conn: event.conn }; fetchIntelAmtInformation(event.nodeid); } else { dev.conn = event.conn; } - */ - } else { - removeDevice(event.nodeid); - } - } - // React to node being removed - if (event.action == 'removenode') { removeDevice(event.nodeid); } + // If the connection type we are using is not longer valid, remove our managed device. + if ((dev != null) && (dev.conntype != null) && ((dev.conntype & event.conn) == 0)) { removeDevice(event.nodeid); dev = null; } - // React to node wakeup command, perform Intel AMT wake if possible - if ((event.action == 'wakedevices') && (Array.isArray(event.nodeids))) { - for (var i in event.nodeids) { performPowerAction(event.nodeids[i], 2); } - } - - // React to changes in a node - if (event.action == 'changenode') { - if (event.amtchange === 1) { - // A change occured in the Intel AMT credentials, we need to reset the connection - removeDevice(event.nodeid); - - // Check if the agent is connected - var constate = parent.GetConnectivityState(event.nodeid); - if (constate == null) return; // No OOB connectivity, exit now. - - if ((constate & 14) != 0) { // connectType: Bitmask, 1 = MeshAgent, 2 = Intel AMT CIRA, 4 = Intel AMT local, 8 = Intel AMT Relay, 16 = MQTT + // Create or update a managed device + if ((event.conn & 14) != 0) { // connectType: Bitmask, 1 = MeshAgent, 2 = Intel AMT CIRA, 4 = Intel AMT local, 8 = Intel AMT Relay, 16 = MQTT // We have an OOB connection to Intel AMT, update our information - var dev = obj.amtDevices[event.nodeid]; - if (dev == null) { obj.amtDevices[event.nodeid] = dev = { conn: constate }; fetchIntelAmtInformation(event.nodeid); } else { dev.conn = constate; } + if (dev == null) { + obj.amtDevices[event.nodeid] = dev = { conn: event.conn }; fetchIntelAmtInformation(event.nodeid); + } else { + dev.conn = event.conn; + } } else if (((event.conn & 1) != 0) && (parent.webserver != null)) { - // We have an agent connection without OOB, check if this agent supports Intel AMT + // We have an agent connection without CIRA/Local/Relay, check if this agent supports Intel AMT var agent = parent.webserver.wsagents[event.nodeid]; - if ((agent == null) || (agent.agentInfo == null) || (parent.meshAgentsArchitectureNumbers[agent.agentInfo.agentId].amt == false)) { removeDevice(event.nodeid); return; } - var dev = obj.amtDevices[event.nodeid]; - if (dev == null) { obj.amtDevices[event.nodeid] = dev = { conn: constate }; fetchIntelAmtInformation(event.nodeid); } else { dev.conn = constate; } - } - } else { - var dev = obj.amtDevices[event.nodeid]; - if (dev != null) { - var amtchange = 0; - if (dev.name != event.node.name) { dev.name = event.node.name; } - if (dev.host != event.node.host) { - dev.host = event.node.host; - // The host has changed, if we are connected to this device locally, we need to reset. - if ((dev.conn & 4) != 0) { removeDevice(dev.nodeid); return; } // We are going to wait for the AMT scanned to find this device again. + if ((agent != null) && (agent.agentInfo != null) && (parent.meshAgentsArchitectureNumbers[agent.agentInfo.agentId].amt == true)) { + // We could turn on LMS relay at this point. } } + break; + } + case 'removenode': { // React to node being removed + removeDevice(event.nodeid); + break; + } + case 'wakedevices': { // React to node wakeup command, perform Intel AMT wake if possible + if (Array.isArray(event.nodeids)) { for (var i in event.nodeids) { performPowerAction(event.nodeids[i], 2); } } + break; + } + case 'changenode': { // React to changes in a node + if (event.amtchange === 1) { + // A change occured in the Intel AMT credentials, we need to reset the connection + removeDevice(event.nodeid); + + // Check if the agent is connected + var constate = parent.GetConnectivityState(event.nodeid); + if (constate == null) return; // No OOB connectivity, exit now. + + if ((constate & 14) != 0) { // connectType: Bitmask, 1 = MeshAgent, 2 = Intel AMT CIRA, 4 = Intel AMT local, 8 = Intel AMT Relay, 16 = MQTT + // We have an OOB connection to Intel AMT, update our information + var dev = obj.amtDevices[event.nodeid]; + if (dev == null) { obj.amtDevices[event.nodeid] = dev = { conn: constate }; fetchIntelAmtInformation(event.nodeid); } else { dev.conn = constate; } + } else if (((event.conn & 1) != 0) && (parent.webserver != null)) { + // We have an agent connection without OOB, check if this agent supports Intel AMT + var agent = parent.webserver.wsagents[event.nodeid]; + if ((agent == null) || (agent.agentInfo == null) || (parent.meshAgentsArchitectureNumbers[agent.agentInfo.agentId].amt == false)) { removeDevice(event.nodeid); return; } + var dev = obj.amtDevices[event.nodeid]; + if (dev == null) { obj.amtDevices[event.nodeid] = dev = { conn: constate }; fetchIntelAmtInformation(event.nodeid); } else { dev.conn = constate; } + } + } else { + var dev = obj.amtDevices[event.nodeid]; + if (dev != null) { + var amtchange = 0; + if (dev.name != event.node.name) { dev.name = event.node.name; } + if (dev.host != event.node.host) { + dev.host = event.node.host; + // The host has changed, if we are connected to this device locally, we need to reset. + if ((dev.conn & 4) != 0) { removeDevice(dev.nodeid); return; } // We are going to wait for the AMT scanned to find this device again. + } + } + } + break; } } } @@ -186,23 +191,24 @@ module.exports.CreateAmtManager = function(parent) { if (node.host) { dev.host = node.host.toLowerCase(); } dev.meshid = node.meshid; dev.intelamt = node.intelamt; - attemptInitialContact(nodeid, dev); + attemptInitialContact(dev); }); } // Attempt to perform initial contact with Intel AMT - function attemptInitialContact(nodeid, dev) { - if (dev == null) { dev = obj.amtDevices[nodeid]; } + function attemptInitialContact(dev) { if (dev == null) return; + //console.log('attemptInitialContact', dev.name); if ((dev.acctry == null) && ((typeof dev.intelamt.user != 'string') || (typeof dev.intelamt.pass != 'string'))) { if (obj.amtAdminAccounts.length > 0) { dev.acctry = 0; } else { return; } } // Handle the case where the Intel AMT CIRA is connected (conn & 2) + // In this connection type, we look at the port bindings to see if we need to do TLS or not. if ((dev.conn & 2) != 0) { // Check to see if CIRA is connected on this server. - var ciraconn = parent.mpsserver.ciraConnections[dev.nodeid]; + var ciraconn = parent.mpsserver.GetConnectionToNode(dev.nodeid, null, 0); // Select the CIRA connection if ((ciraconn == null) || (ciraconn.tag == null) || (ciraconn.tag.boundPorts == null)) { removeDevice(dev.nodeid); return; } // CIRA connection is not on this server, no need to deal with this device anymore. // See what user/pass to try. @@ -218,11 +224,12 @@ module.exports.CreateAmtManager = function(parent) { // Connect now //console.log('CIRA-Connect', (dotls == 1)?"TLS":"NoTLS", dev.name, dev.host, user, pass); var comm; + dotls = 0; // TODO: We don't support TLS with CIRA/Relay/LMS connections yet. Remove this when we do. if (dotls == 1) { - comm = CreateWsmanComm(dev.nodeid, 16993, user, pass, 1, null, parent.mpsserver); // Perform TLS + comm = CreateWsmanComm(dev.nodeid, 16993, user, pass, 1, null, ciraconn); // Perform TLS comm.xtlsFingerprint = 0; // Perform no certificate checking } else { - comm = CreateWsmanComm(dev.nodeid, 16992, user, pass, 0, null, parent.mpsserver); // No TLS + comm = CreateWsmanComm(dev.nodeid, 16992, user, pass, 0, null, ciraconn); // No TLS } var wsstack = WsmanStackCreateService(comm); dev.amtstack = AmtStackCreateService(wsstack); @@ -234,12 +241,45 @@ module.exports.CreateAmtManager = function(parent) { return; // If CIRA is connected, don't try any other methods. } + // Handle the case where the Intel AMT relay is connected (conn & 8) + if ((dev.conn & 8) != 0) { + // Check to see if CIRA is connected on this server. + var ciraconn = parent.mpsserver.GetConnectionToNode(dev.nodeid, null, 1); // Select a relay connection + if ((ciraconn == null) || (ciraconn.tag == null) || (ciraconn.tag.boundPorts == null)) { removeDevice(dev.nodeid); return; } // CIRA connection is not on this server, no need to deal with this device anymore. + + // See what user/pass to try. + var user = null, pass = null; + if (dev.acctry == null) { user = dev.intelamt.user; pass = dev.intelamt.pass; } else { user = obj.amtAdminAccounts[dev.acctry].user; pass = obj.amtAdminAccounts[dev.acctry].pass; } + + // Connect now + var comm; + dev.tlsfail = true; // TODO: We don't support TLS with CIRA/Relay/LMS connections yet. Remove this when we do. + if (dev.tlsfail !== true) { + //console.log('Relay-Connect', "TLS", dev.name, dev.host, user, pass); + comm = CreateWsmanComm(dev.nodeid, 16993, user, pass, 1, null, ciraconn); // Perform TLS + comm.xtlsFingerprint = 0; // Perform no certificate checking + } else { + //console.log('Relay-Connect', "NoTLS", dev.name, dev.host, user, pass); + comm = CreateWsmanComm(dev.nodeid, 16992, user, pass, 0, null, ciraconn); // No TLS + } + var wsstack = WsmanStackCreateService(comm); + dev.amtstack = AmtStackCreateService(wsstack); + dev.amtstack.dev = dev; + obj.activeLocalConnections[dev.host] = dev; + dev.amtstack.BatchEnum(null, ['*AMT_GeneralSettings', '*IPS_HostBasedSetupService'], attemptLocalConnectResponse); + dev.conntype = 8; // Relay + + return; // If relay is connected, don't try any other methods. + } + // Handle the case where the Intel AMT local scanner found the device (conn & 4) if (((dev.conn & 4) != 0) && (typeof dev.host == 'string')) { // Since we don't allow two or more connections to the same host, check if a pending connection is active. if (obj.activeLocalConnections[dev.host] != null) { // Active connection, hold and try later. - setTimeout(function () { attemptInitialContact(nodeid); }, 5000); + var tryAgainFunc = function tryAgainFunc() { if (obj.amtDevices[tryAgainFunc.dev.nodeid] != null) { attemptInitialContact(tryAgainFunc.dev); } } + tryAgainFunc.dev = dev; + setTimeout(tryAgainFunc, 5000); } else { // No active connections, see what user/pass to try. var user = null, pass = null; @@ -248,11 +288,11 @@ module.exports.CreateAmtManager = function(parent) { // Connect now var comm; if (dev.tlsfail !== true) { - //console.log('Connect', "TLS", dev.name, dev.host, user, pass); + //console.log('Direct-Connect', "TLS", dev.name, dev.host, user, pass); comm = CreateWsmanComm(dev.host, 16993, user, pass, 1); // Always try with TLS first comm.xtlsFingerprint = 0; // Perform no certificate checking } else { - //console.log('Connect', "NoTLS", dev.name, dev.host, user, pass); + //console.log('Direct-Connect', "NoTLS", dev.name, dev.host, user, pass); comm = CreateWsmanComm(dev.host, 16992, user, pass, 0); // Try without TLS } var wsstack = WsmanStackCreateService(comm); @@ -301,11 +341,11 @@ module.exports.CreateAmtManager = function(parent) { // We got a bad response if ((dev.conntype == 1) && (dev.tlsfail !== true) && (status == 408)) { // TLS error on a local connection, try again without TLS - dev.tlsfail = true; attemptInitialContact(dev.nodeid, dev); return; + dev.tlsfail = true; attemptInitialContact(dev); return; } else if (status == 401) { // Authentication error, see if we can use alternative credentials - if ((dev.acctry == null) && (obj.amtAdminAccounts.length > 0)) { dev.acctry = 0; attemptInitialContact(dev.nodeid, dev); return; } - if ((dev.acctry != null) && (obj.amtAdminAccounts.length > (dev.acctry + 1))) { dev.acctry++; attemptInitialContact(dev.nodeid, dev); return; } + if ((dev.acctry == null) && (obj.amtAdminAccounts.length > 0)) { dev.acctry = 0; attemptInitialContact(dev); return; } + if ((dev.acctry != null) && (obj.amtAdminAccounts.length > (dev.acctry + 1))) { dev.acctry++; attemptInitialContact(dev); return; } // We are unable to authenticate to this device, clear Intel AMT credentials. ClearDeviceCredentials(dev); diff --git a/amtscanner.js b/amtscanner.js index adf49171..3626308c 100644 --- a/amtscanner.js +++ b/amtscanner.js @@ -164,8 +164,8 @@ module.exports.CreateAmtScanner = function (parent) { if (err == null && docs.length > 0) { for (var i in docs) { var doc = docs[i], host = doc.host.toLowerCase(); - const ciraConnection = obj.parent.mpsserver ? obj.parent.mpsserver.ciraConnections[doc._id] : null; - if ((host != '127.0.0.1') && (host != '::1') && (host.toLowerCase() != 'localhost') && (ciraConnection == null)) { + const ciraConnections = obj.parent.mpsserver ? obj.parent.mpsserver.GetConnectionToNode(doc._id, null, true) : null; // See if any OOB connections are present + if ((host != '127.0.0.1') && (host != '::1') && (host.toLowerCase() != 'localhost') && (ciraConnections == null)) { var scaninfo = obj.scanTable[doc._id]; if (scaninfo == undefined) { var tag = obj.nextTag++; diff --git a/meshagent.js b/meshagent.js index 854b0803..13932fff 100644 --- a/meshagent.js +++ b/meshagent.js @@ -79,7 +79,7 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { const state = parent.parent.GetConnectivityState(obj.dbNodeKey); if ((state != null) && (state.connectivity != null)) { if ((state.connectivity & 1) != 0) { parent.wsagents[obj.dbNodeKey].close(); } // Disconnect mesh agent - if ((state.connectivity & 2) != 0) { parent.parent.mpsserver.close(parent.parent.mpsserver.ciraConnections[obj.dbNodeKey]); } // Disconnect CIRA connection + if ((state.connectivity & 2) != 0) { parent.parent.mpsserver.closeAllForNode(obj.dbNodeKey); } // Disconnect CIRA connection } } else { // Update the last connect time diff --git a/meshcentral.js b/meshcentral.js index 5b2d1d05..3478f713 100644 --- a/meshcentral.js +++ b/meshcentral.js @@ -1422,7 +1422,10 @@ function CreateMeshCentralServer(config, args) { rs: obj.webserver.relaySessionCount } }; - if (obj.mpsserver != null) { data.conn.am = Object.keys(obj.mpsserver.ciraConnections).length; } + if (obj.mpsserver != null) { + data.conn.am = 0; + for (var i in obj.mpsserver.ciraConnections) { data.conn.am += obj.mpsserver.ciraConnections[i].length; } + } if (obj.firstStats === true) { delete obj.firstStats; data.first = true; } obj.db.SetServerStats(data); // Save the stats to the database obj.DispatchEvent(['*'], obj, { action: 'servertimelinestats', data: data }); // Event the server stats diff --git a/meshuser.js b/meshuser.js index 6e6e5874..81c075ff 100644 --- a/meshuser.js +++ b/meshuser.js @@ -406,7 +406,10 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use RelayCount: Object.keys(parent.wsrelays).length }; if (parent.relaySessionErrorCount != 0) { serverStats.RelayErrors = parent.relaySessionErrorCount; } - if (parent.parent.mpsserver != null) { serverStats.ConnectedIntelAMT = Object.keys(parent.parent.mpsserver.ciraConnections).length; } + if (parent.parent.mpsserver != null) { + serverStats.ConnectedIntelAMT = 0; + for (var i in parent.parent.mpsserver.ciraConnections) { serverStats.ConnectedIntelAMT += parent.parent.mpsserver.ciraConnections[i].length; } + } // Take a look at agent errors var agentstats = parent.getAgentStats(); @@ -657,7 +660,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use docs[i].conn = state.connectivity; docs[i].pwr = state.powerState; if ((state.connectivity & 1) != 0) { var agent = parent.wsagents[docs[i]._id]; if (agent != null) { docs[i].agct = agent.connectTime; } } - if ((state.connectivity & 2) != 0) { var cira = parent.parent.mpsserver.ciraConnections[docs[i]._id]; if (cira != null) { docs[i].cict = cira.tag.connectTime; } } + + // Use the connection time of the CIRA/Relay connection + if ((state.connectivity & 2) != 0) { var cira = parent.parent.mpsserver.GetConnectionToNode(docs[i]._id, null, true); if (cira != null) { docs[i].cict = cira[0].tag.connectTime; } } } // Compress the meshid's @@ -1062,6 +1067,21 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } break; } + case 'mps': { // List all MPS connections and types. + if (parent.parent.mpsserver == null) { + r = 'MPS not enabled.'; + } else { + const connectionTypes = ['CIRA', 'Relay', 'LMS']; + for (var nodeid in parent.parent.mpsserver.ciraConnections) { + r += nodeid; + var connections = parent.parent.mpsserver.ciraConnections[nodeid]; + for (var i in connections) { r += ', ' + connectionTypes[connections[i].tag.connType]; } + r += '\r\n'; + } + if (r == '') { r = 'MPS has not connections.'; } + } + break; + } case 'dbstats': { parent.parent.db.getStats(function (stats) { var r2 = ''; @@ -3458,7 +3478,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use node.conn = state.connectivity; node.pwr = state.powerState; if ((state.connectivity & 1) != 0) { var agent = parent.wsagents[node._id]; if (agent != null) { node.agct = agent.connectTime; } } - if ((state.connectivity & 2) != 0) { var cira = parent.parent.mpsserver.ciraConnections[node._id]; if (cira != null) { node.cict = cira.tag.connectTime; } } + + // Uuse the connection time of the CIRA/Relay connection + if ((state.connectivity & 2) != 0) { var cira = parent.parent.mpsserver.GetConnectionToNode(node._id, null, true); if (cira != null) { node.cict = cira[0].tag.connectTime; } } } // Event the node change @@ -3527,7 +3549,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use var state = parent.parent.GetConnectivityState(nodeid); if ((state != null) && (state.connectivity != null)) { if ((state.connectivity & 1) != 0) { parent.wsagents[nodeid].close(); } // Disconnect mesh agent - if ((state.connectivity & 2) != 0) { parent.parent.mpsserver.close(parent.parent.mpsserver.ciraConnections[nodeid]); } // Disconnect CIRA connection + if ((state.connectivity & 2) != 0) { parent.parent.mpsserver.closeAllForNode(nodeid); } // Disconnect CIRA/Relay/LMS connections } }); } diff --git a/mpsserver.js b/mpsserver.js index 381c50b0..2660531e 100644 --- a/mpsserver.js +++ b/mpsserver.js @@ -22,7 +22,7 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) { obj.db = db; obj.args = args; obj.certificates = certificates; - obj.ciraConnections = {}; // NodeID --> Socket + obj.ciraConnections = {}; // NodeID --> [ Socket ] var tlsSessionStore = {}; // Store TLS session information for quick resume. var tlsSessionStoreCount = 0; // Number of cached TLS session information in store. const constants = (require('crypto').constants ? require('crypto').constants : require('constants')); // require('constants') is deprecated in Node 11.10, use require('crypto').constants instead. @@ -79,7 +79,8 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) { KEEPALIVE_REQUEST: 208, KEEPALIVE_REPLY: 209, KEEPALIVE_OPTIONS_REQUEST: 210, - KEEPALIVE_OPTIONS_REPLY: 211 + KEEPALIVE_OPTIONS_REPLY: 211, + MESH_CONNECTION_TYPE: 250 // This is a Mesh specific command that instructs the server of the connection type: 1 = Relay, 2 = LMS. }; /* @@ -142,22 +143,71 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) { var socketErrorCount = 0; var maxDomainDevicesReached = 0; - // Delay setting the connectivity state by 300ms to allow time for CIRA port mappings to be established - // Report power state as "present" (7) until Intel AMT manager starts polling for power state. - function delayedSetConnectivityState(meshid, nodeid, connectTime, connType) { - if (nodeid.startsWith('*')) return; // Don't set connectivity state for Intel AMT self agent relay - var f = function setConnFunc() { if (obj.ciraConnections[setConnFunc.nodeid] != null) { obj.parent.SetConnectivityState(setConnFunc.meshid, setConnFunc.nodeid, setConnFunc.connectTime, setConnFunc.connType, 7); } } - f.nodeid = nodeid; - f.meshid = meshid; - f.connectTime = connectTime; - f.connType = connType; + // Add a CIRA connection to the connection list + function addCiraConnection(socket) { + // Check if there is already a connection of the same type + var sameType = false, connections = obj.ciraConnections[socket.tag.nodeid]; + if (connections != null) { for (var i in connections) { var conn = connections[i]; if (conn.tag.connType === socket.tag.connType) { sameType = true; } } } + + // Add this connection to the connections list + if (connections == null) { obj.ciraConnections[socket.tag.nodeid] = [socket]; } else { obj.ciraConnections[socket.tag.nodeid].push(socket); } + if ((socket.tag.connType != 0) && (socket.tag.connType != 1)) return; // If not a CIRA or Relay connection, we don't indicate a connection state change + + // Update connectivity state + // Report the new state of a CIRA/Relay/LMS connection after a short delay. This is to wait for the connection to have the bounded ports setup before we advertise this new connection. + socket.xxStartHold = 1; + var f = function setConnFunc() { + delete setConnFunc.socket.xxStartHold; + const ciraArray = obj.ciraConnections[setConnFunc.socket.tag.nodeid]; + if ((ciraArray != null) && ((ciraArray.indexOf(setConnFunc.socket) >= 0))) { // Check if this connection is still present + if (setConnFunc.socket.tag.connType == 0) { + // Intel AMT CIRA connection. This connection indicates the remote device is present. + obj.parent.SetConnectivityState(setConnFunc.socket.tag.meshid, setConnFunc.socket.tag.nodeid, setConnFunc.socket.tag.connectTime, 2, 7); // 7 = Present + } else if (setConnFunc.socket.tag.connType == 1) { + // Intel AMT Relay connection. This connection does not give any information about the remote device's power state. + obj.parent.SetConnectivityState(setConnFunc.socket.tag.meshid, setConnFunc.socket.tag.nodeid, setConnFunc.socket.tag.connectTime, 8, 0); // 0 = Unknown + } else if (setConnFunc.socket.tag.connType == 2) { + // Intel AMT LMS connection. We don't notify of these connections except telling the Intel AMT manager about them. + // TODO: Notify AMT manager + } + } + } + f.socket = socket; setTimeout(f, 300); } + // Remove a CIRA connection from the connection list + function removeCiraConnection(socket) { + // Remove the connection from the list if present. + const ciraArray = obj.ciraConnections[socket.tag.nodeid]; + if (ciraArray == null) return; + var i = ciraArray.indexOf(socket); + if (i == -1) return; + ciraArray.splice(i, 1); + if (ciraArray.length == 0) { delete obj.ciraConnections[socket.tag.nodeid]; } else { obj.ciraConnections[socket.tag.nodeid] = ciraArray; } + + // If we are removing a connection during the hold period, don't clear any state since it was never set. + if (socket.xxStartHold == 1) return; + + // Check if there is already a connection of the same type + var sameType = false, connections = obj.ciraConnections[socket.tag.nodeid]; + if (connections != null) { for (var i in connections) { var conn = connections[i]; if (conn.tag.connType === socket.tag.connType) { sameType = true; } } } + if (sameType == true) return; // if there is a connection of the same type, don't change the connection state. + + // Update connectivity state + if (socket.tag.connType == 0) { + obj.parent.ClearConnectivityState(socket.tag.meshid, socket.tag.nodeid, 2); // CIRA + } else if (socket.tag.connType == 1) { + obj.parent.ClearConnectivityState(socket.tag.meshid, socket.tag.nodeid, 8); // Relay + } + } + // Return statistics about this MPS server obj.getStats = function () { + var ciraConnectionCount = 0; + for (var i in obj.ciraConnections) { ciraConnectionCount += obj.ciraConnections[i].length; } return { - ciraConnections: Object.keys(obj.ciraConnections).length, + ciraConnections: ciraConnectionCount, tlsSessionStore: Object.keys(tlsSessionStore).length, connectionCount: connectionCount, userAuthRequestCount: userAuthRequestCount, @@ -224,8 +274,11 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) { obj.onWebSocketConnection = function (socket) { connectionCount++; - // connType: 2 = CIRA, 8 = Relay - socket.tag = { first: true, connType: 2, clientCert: null, accumulator: '', activetunnels: 0, boundPorts: [], websocket: true, socket: socket, host: null, nextchannelid: 4, channels: {}, nextsourceport: 0 }; + // connType: 0 = CIRA, 1 = Relay, 2 = LMS + socket.tag = { first: true, connType: 0, clientCert: null, accumulator: '', activetunnels: 0, boundPorts: [], websocket: true, socket: socket, host: null, nextchannelid: 4, channels: {}, nextsourceport: 0 }; + socket.SetupChannel = function SetupChannel(targetport) { return SetupChannel.parent.SetupChannel(SetupChannel.conn, targetport); } + socket.SetupChannel.parent = obj; + socket.SetupChannel.conn = socket; socket.websocket = 1; parent.debug('mps', "New CIRA websocket connection"); @@ -245,8 +298,7 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) { socket.addListener('close', function () { socketClosedCount++; parent.debug('mps', "CIRA websocket closed", this.tag.meshid, this.tag.nodeid); - try { delete obj.ciraConnections[socket.tag.nodeid]; } catch (e) { } - if (!this.tag.nodeid.startsWith('*')) { obj.parent.ClearConnectivityState(this.tag.meshid, this.tag.nodeid, this.tag.connType); } + removeCiraConnection(socket); }); socket.addListener('error', function (e) { @@ -258,12 +310,15 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) { // Called when a new TLS/TCP connection is accepted function onConnection(socket) { connectionCount++; - // connType: 2 = CIRA, 8 = Relay + // connType: 0 = CIRA, 1 = Relay, 2 = LMS if (obj.args.mpstlsoffload) { - socket.tag = { first: true, connType: 2, clientCert: null, accumulator: '', activetunnels: 0, boundPorts: [], socket: socket, host: null, nextchannelid: 4, channels: {}, nextsourceport: 0 }; + socket.tag = { first: true, connType: 0, clientCert: null, accumulator: '', activetunnels: 0, boundPorts: [], socket: socket, host: null, nextchannelid: 4, channels: {}, nextsourceport: 0 }; } else { - socket.tag = { first: true, connType: 2, clientCert: socket.getPeerCertificate(true), accumulator: '', activetunnels: 0, boundPorts: [], socket: socket, host: null, nextchannelid: 4, channels: {}, nextsourceport: 0 }; + socket.tag = { first: true, connType: 0, clientCert: socket.getPeerCertificate(true), accumulator: '', activetunnels: 0, boundPorts: [], socket: socket, host: null, nextchannelid: 4, channels: {}, nextsourceport: 0 }; } + socket.SetupChannel = function SetupChannel(targetport) { return SetupChannel.parent.SetupChannel(SetupChannel.conn, targetport); } + socket.SetupChannel.parent = obj; + socket.SetupChannel.conn = socket; socket.setEncoding('binary'); parent.debug('mps', "New CIRA connection"); @@ -274,8 +329,7 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) { socket.addListener('close', function () { socketClosedCount++; parent.debug('mps', 'CIRA connection closed'); - try { delete obj.ciraConnections[socket.tag.nodeid]; } catch (e) { } - obj.parent.ClearConnectivityState(socket.tag.meshid, socket.tag.nodeid, socket.tag.connType); + removeCiraConnection(socket); }); socket.addListener('error', function (e) { @@ -382,8 +436,7 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) { obj.parent.DispatchEvent(['*', socket.tag.meshid], obj, { etype: 'node', action: 'addnode', node: parent.webserver.CloneSafeNode(device), msg: change, domain: domainid }); // Add the connection to the MPS connection list - obj.ciraConnections[socket.tag.nodeid] = socket; - delayedSetConnectivityState(socket.tag.meshid, socket.tag.nodeid, socket.tag.connectTime, socket.tag.connType); + addCiraConnection(socket); } }); return; @@ -412,8 +465,7 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) { } // Add the connection to the MPS connection list - obj.ciraConnections[socket.tag.nodeid] = socket; - delayedSetConnectivityState(socket.tag.meshid, socket.tag.nodeid, socket.tag.connectTime, socket.tag.connType); + addCiraConnection(socket); }); } else { // This node connected without certificate authentication, use password auth @@ -521,8 +573,7 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) { obj.parent.DispatchEvent(['*', socket.tag.meshid], obj, { etype: 'node', action: 'addnode', node: parent.webserver.CloneSafeNode(device), msg: change, domain: mesh.domain }); // Add the connection to the MPS connection list - obj.ciraConnections[socket.tag.nodeid] = socket; - delayedSetConnectivityState(socket.tag.meshid, socket.tag.nodeid, socket.tag.connectTime, socket.tag.connType); + addCiraConnection(socket); SendUserAuthSuccess(socket); // Notify the auth success on the CIRA connection } }); @@ -545,8 +596,7 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) { } // Add the connection to the MPS connection list - obj.ciraConnections[socket.tag.nodeid] = socket; - delayedSetConnectivityState(socket.tag.meshid, socket.tag.nodeid, socket.tag.connectTime, socket.tag.connType); + addCiraConnection(socket); SendUserAuthSuccess(socket); // Notify the auth success on the CIRA connection }); } else if (mesh.mtype == 2) { // If this is a agent mesh, search the mesh for this device UUID @@ -584,8 +634,7 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) { socket.tag.connectTime = Date.now(); // Add the connection to the MPS connection list - obj.ciraConnections[socket.tag.nodeid] = socket; - delayedSetConnectivityState(socket.tag.meshid, socket.tag.nodeid, socket.tag.connectTime, socket.tag.connType); + addCiraConnection(socket); SendUserAuthSuccess(socket); // Notify the auth success on the CIRA connection }); } else { // Unknown mesh type @@ -818,10 +867,17 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) { var ReasonCode = common.ReadInt(data, 1); disconnectCommandCount++; parent.debug('mpscmd', '--> DISCONNECT', ReasonCode); - try { delete obj.ciraConnections[socket.tag.nodeid]; } catch (e) { } - obj.parent.ClearConnectivityState(socket.tag.meshid, socket.tag.nodeid, socket.tag.connType); + removeCiraConnection(socket); return 7; } + case APFProtocol.MESH_CONNECTION_TYPE: // This is a Mesh specific command to indicate the connect type. + { + if (len < 5) return 0; + if ((socket.tag.connType == 0) && (socket.tag.SystemId == null)) { // Once set, the connection type can't be changed. + socket.tag.connType = common.ReadInt(data, 1); // 0 = CIRA, 1 = Relay, 2 = LMS + } + return 5; + } default: { parent.debug('mpscmd', '--> Unknown CIRA command: ' + cmd); @@ -833,8 +889,14 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) { // Disconnect CIRA tunnel obj.close = function (socket) { try { socket.end(); } catch (e) { } - try { delete obj.ciraConnections[socket.tag.nodeid]; } catch (e) { } - obj.parent.ClearConnectivityState(socket.tag.meshid, socket.tag.nodeid, socket.tag.connType); + removeCiraConnection(socket); + }; + + // Disconnect all CIRA tunnel for a given NodeId + obj.closeAllForNode = function (nodeid) { + var connections = obj.ciraConnections[nodeid]; + if (connections == null) return; + for (var i in connections) { obj.close(connections[i]); } }; function SendServiceAccept(socket, service) { @@ -926,18 +988,36 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) { } } - // Setup a new channel to a nodeid - obj.SetupChannelToNode = function (nodeid, targetport) { - var ciraconn = obj.ciraConnections[nodeid]; - if (ciraconn == null) return null; - return obj.SetupChannel(ciraconn, targetport); + // Returns a CIRA/Relay/LMS connection to a nodeid, use the best possible connection, CIRA first, Relay second, LMS third. + // if oob is set to true, don't allow an LMS connection. + obj.GetConnectionToNode = function (nodeid, targetport, oob) { + var connectionArray = obj.ciraConnections[nodeid]; + if (connectionArray == null) return null; + var selectConn = null; + // Select the best connection, which is the one with the lowest connType value. + for (var i in connectionArray) { + var conn = connectionArray[i]; + if ((oob === true) && (conn.tag.connType == 2)) continue; // If an OOB connection is required, don't allow LMS connections. + if ((typeof oob === 'number') && (conn.tag.connType !== oob)) continue; // if OOB specifies an exact connection type, filter on this type. + if ((targetport != null) && (conn.tag.boundPorts.indexOf(targetport) == -1)) continue; // This connection does not route to the target port. + if ((selectConn == null) || (conn.tag.connType < selectConn.tag.connType)) { selectConn = conn; } + } + return selectConn; + } + + // Setup a new channel to a nodeid, use the best possible connection, CIRA first, Relay second, LMS third. + // if oob is set to true, don't allow an LMS connection. + obj.SetupChannelToNode = function (nodeid, targetport, oob) { + var conn = obj.GetConnectionToNode(nodeid, targetport, oob); + if (conn == null) return null; + return obj.SetupChannel(conn, targetport); } // Setup a new channel obj.SetupChannel = function (socket, targetport) { var sourceport = (socket.tag.nextsourceport++ % 30000) + 1024; var cirachannel = { targetport: targetport, channelid: socket.tag.nextchannelid++, socket: socket, state: 1, sendcredits: 0, amtpendingcredits: 0, amtCiraWindow: 0, ciraWindow: 32768 }; - SendChannelOpen(socket, false, cirachannel.channelid, cirachannel.ciraWindow, socket.tag.host, targetport, "1.2.3.4", sourceport); + SendChannelOpen(socket, false, cirachannel.channelid, cirachannel.ciraWindow, socket.tag.host, targetport, '1.2.3.4', sourceport); // This function writes data to this CIRA channel cirachannel.write = function (data) { @@ -1015,8 +1095,12 @@ module.exports.CreateMpsServer = function (parent, db, args, certificates) { // Change a node to a new meshid, this is called when a node changes groups. obj.changeDeviceMesh = function (nodeid, newMeshId) { - var socket = obj.ciraConnections[nodeid]; - if ((socket != null) && (socket.tag != null)) { socket.tag.meshid = newMeshId; } + var connectionArray = obj.ciraConnections[nodeid]; + if (connectionArray == null) return; + for (var i in connectionArray) { + var socket = connectionArray[i]; + if ((socket != null) && (socket.tag != null)) { socket.tag.meshid = newMeshId; } + } } // Called when handling incoming HTTP data diff --git a/webserver.js b/webserver.js index eadd271e..8335d917 100644 --- a/webserver.js +++ b/webserver.js @@ -3375,11 +3375,11 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { } // If Intel AMT CIRA connection is available, use it - if (((conn & 2) != 0) && (parent.mpsserver.ciraConnections[req.query.host] != null)) { + var ciraconn = parent.mpsserver.GetConnectionToNode(req.query.host, null, false); + if (ciraconn != null) { parent.debug('web', 'Opening relay CIRA channel connection to ' + req.query.host + '.'); - var ciraconn = parent.mpsserver.ciraConnections[req.query.host]; - + // TODO: If ciraconn is a relay connection, we can't detect the TLS state like this. // Compute target port, look at the CIRA port mappings, if non-TLS is allowed, use that, if not use TLS var port = 16993; //if (node.intelamt.tls == 0) port = 16992; // DEBUG: Allow TLS flag to set TLS mode within CIRA