diff --git a/meshrelay.js b/meshrelay.js index 562313bf..6aec4784 100644 --- a/meshrelay.js +++ b/meshrelay.js @@ -16,16 +16,11 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie) { var obj = {}; obj.ws = ws; - obj.req = req; - obj.peer = null; - obj.user = user; - obj.cookie = cookie; - obj.parent = parent; obj.id = req.query.id; - obj.remoteaddr = obj.ws._socket.remoteAddress; - obj.domain = domain; - if (obj.remoteaddr.startsWith('::ffff:')) { obj.remoteaddr = obj.remoteaddr.substring(7); } - obj.parent.relaySessionCount++; + + // Relay session count (we may remove this in the future) + obj.relaySessionCounted = true; + parent.relaySessionCount++; // Mesh Rights const MESHRIGHT_EDITMESH = 1; @@ -46,23 +41,31 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie const SITERIGHT_SERVERUPDATE = 16; const SITERIGHT_LOCKED = 32; + // Clean a IPv6 address that encodes a IPv4 address + function cleanRemoteAddr(addr) { if (addr.startsWith('::ffff:')) { return addr.substring(7); } else { return addr; } } + // Disconnect this agent obj.close = function (arg) { - if ((arg == 1) || (arg == null)) { try { obj.ws.close(); obj.parent.parent.debug(1, 'Relay: Soft disconnect (' + obj.remoteaddr + ')'); } catch (e) { console.log(e); } } // Soft close, close the websocket - if (arg == 2) { try { obj.ws._socket._parent.end(); obj.parent.parent.debug(1, 'Relay: Hard disconnect (' + obj.remoteaddr + ')'); } catch (e) { console.log(e); } } // Hard close, close the TCP socket + if ((arg == 1) || (arg == null)) { try { ws.close(); parent.parent.debug(1, 'Relay: Soft disconnect (' + cleanRemoteAddr(ws._socket.remoteAddress) + ')'); } catch (e) { console.log(e); } } // Soft close, close the websocket + if (arg == 2) { try { ws._socket._parent.end(); parent.parent.debug(1, 'Relay: Hard disconnect (' + cleanRemoteAddr(ws._socket.remoteAddress) + ')'); } catch (e) { console.log(e); } } // Hard close, close the TCP socket + + // Aggressive cleanup + delete obj.id; + delete obj.ws; + delete obj.peer; }; obj.sendAgentMessage = function (command, userid, domainid) { var rights, mesh; if (command.nodeid == null) return false; - var user = obj.parent.users[userid]; + var user = parent.users[userid]; if (user == null) return false; var splitnodeid = command.nodeid.split('/'); // Check that we are in the same domain and the user has rights over this node. if ((splitnodeid[0] == 'node') && (splitnodeid[1] == domainid)) { // Get the user object // See if the node is connected - var agent = obj.parent.wsagents[command.nodeid]; + var agent = parent.wsagents[command.nodeid]; if (agent != null) { // Check if we have permission to send a message to that node rights = user.links[agent.dbMeshKey]; @@ -79,7 +82,7 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie } } else { // Check if a peer server is connected to this agent - var routing = obj.parent.parent.GetRoutingServerId(command.nodeid, 1); // 1 = MeshAgent routing type + var routing = parent.parent.GetRoutingServerId(command.nodeid, 1); // 1 = MeshAgent routing type if (routing != null) { // Check if we have permission to send a message to that node rights = user.links[routing.meshid]; @@ -90,7 +93,7 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie command.consent = mesh.consent; // Add user consent if (typeof domain.userconsentflags == 'number') { command.consent |= domain.userconsentflags; } // Add server required consent flags command.username = user.name; // Add user name - obj.parent.parent.multiServer.DispatchMessageSingleServer(command, routing.serverid); + parent.parent.multiServer.DispatchMessageSingleServer(command, routing.serverid); return true; } } @@ -105,12 +108,12 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie // If this is a MeshMessenger session, the ID is the two userid's and authentication must match one of them. if (obj.id.startsWith('meshmessenger/')) { - if ((obj.id.startsWith('meshmessenger/user/') == true) && (obj.user == null)) { try { obj.close(); } catch (e) { } return null; } // If user-to-user, both sides need to be authenticated. + if ((obj.id.startsWith('meshmessenger/user/') == true) && (user == null)) { try { obj.close(); } catch (e) { } return null; } // If user-to-user, both sides need to be authenticated. var x = obj.id.split('/'), user1 = x[1] + '/' + x[2] + '/' + x[3], user2 = x[4] + '/' + x[5] + '/' + x[6]; if ((x[1] != 'user') && (x[4] != 'user')) { try { obj.close(); } catch (e) { } return null; } // MeshMessenger session must have at least one authenticated user if ((x[1] == 'user') && (x[4] == 'user')) { // If this is a user-to-user session, you must be authenticated to join. - if ((obj.user._id != user1) && (obj.user._id != user2)) { try { obj.close(); } catch (e) { } return null; } + if ((user._id != user1) && (user._id != user2)) { try { obj.close(); } catch (e) { } return null; } } else { // If only one side of the session is a user // !!!!! TODO: Need to make sure that one of the two sides is the correct user. !!!!! @@ -123,9 +126,9 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie if (!parent.args.notls) { // Check the identifier, if running without TLS, skip this. var ids = obj.id.split(':'); - if (ids.length != 3) { obj.ws.close(); obj.id = null; return null; } // Invalid ID, drop this. - if (parent.crypto.createHmac('SHA384', parent.relayRandom).update(ids[0] + ':' + ids[1]).digest('hex') != ids[2]) { obj.ws.close(); obj.id = null; return null; } // Invalid HMAC, drop this. - if ((Date.now() - parseInt(ids[1])) > 120000) { obj.ws.close(); obj.id = null; return null; } // Expired time, drop this. + if (ids.length != 3) { ws.close(); delete obj.id; return null; } // Invalid ID, drop this. + if (parent.crypto.createHmac('SHA384', parent.relayRandom).update(ids[0] + ':' + ids[1]).digest('hex') != ids[2]) { ws.close(); delete obj.id; return null; } // Invalid HMAC, drop this. + if ((Date.now() - parseInt(ids[1])) > 120000) { ws.close(); delete obj.id; return null; } // Expired time, drop this. obj.id = ids[0]; } */ @@ -137,9 +140,11 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie if (relayinfo.state == 1) { // Check that at least one connection is authenticated if ((obj.authenticated != true) && (relayinfo.peer1.authenticated != true)) { - obj.id = null; - obj.ws.close(); - obj.parent.parent.debug(1, 'Relay without-auth: ' + obj.id + ' (' + obj.remoteaddr + ')'); + ws.close(); + parent.parent.debug(1, 'Relay without-auth: ' + obj.id + ' (' + cleanRemoteAddr(ws._socket.remoteAddress) + ')'); + delete obj.id; + delete obj.ws; + delete obj.peer; return null; } @@ -148,7 +153,7 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie obj.peer.peer = obj; relayinfo.peer2 = obj; relayinfo.state = 2; - obj.ws.send('c'); // Send connect to both peers + ws.send('c'); // Send connect to both peers relayinfo.peer1.ws.send('c'); relayinfo.peer1.ws._socket.resume(); // Release the traffic relayinfo.peer2.ws._socket.resume(); // Release the traffic @@ -156,24 +161,26 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie relayinfo.peer1.ws.peer = relayinfo.peer2.ws; relayinfo.peer2.ws.peer = relayinfo.peer1.ws; - obj.parent.parent.debug(1, 'Relay connected: ' + obj.id + ' (' + obj.remoteaddr + ' --> ' + obj.peer.remoteaddr + ')'); + parent.parent.debug(1, 'Relay connected: ' + obj.id + ' (' + cleanRemoteAddr(ws._socket.remoteAddress) + ' --> ' + cleanRemoteAddr(obj.peer.ws._socket.remoteAddress) + ')'); } else { // Connected already, drop (TODO: maybe we should re-connect?) - obj.id = null; - obj.ws.close(); - obj.parent.parent.debug(1, 'Relay duplicate: ' + obj.id + ' (' + obj.remoteaddr + ')'); + ws.close(); + parent.parent.debug(1, 'Relay duplicate: ' + obj.id + ' (' + cleanRemoteAddr(ws._socket.remoteAddress) + ')'); + delete obj.id; + delete obj.ws; + delete obj.peer; return null; } } else { // Wait for other relay connection ws._socket.pause(); // Hold traffic until the other connection parent.wsrelays[obj.id] = { peer1: obj, state: 1 }; - obj.parent.parent.debug(1, 'Relay holding: ' + obj.id + ' (' + obj.remoteaddr + ') ' + (obj.authenticated ? 'Authenticated' : '')); + parent.parent.debug(1, 'Relay holding: ' + obj.id + ' (' + cleanRemoteAddr(ws._socket.remoteAddress) + ') ' + (obj.authenticated ? 'Authenticated' : '')); // Check if a peer server has this connection if (parent.parent.multiServer != null) { - var rsession = obj.parent.wsPeerRelays[obj.id]; - if ((rsession != null) && (rsession.serverId > obj.parent.parent.serverId)) { + var rsession = parent.wsPeerRelays[obj.id]; + if ((rsession != null) && (rsession.serverId > parent.parent.serverId)) { // We must initiate the connection to the peer parent.parent.multiServer.createPeerRelay(ws, req, rsession.serverId, req.session.userid); delete parent.wsrelays[obj.id]; @@ -202,14 +209,15 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie // If error, close both sides of the relay. ws.on('error', function (err) { - obj.parent.relaySessionErrorCount++; - console.log('Relay error from ' + obj.remoteaddr + ', ' + err.toString().split('\r')[0] + '.'); + parent.relaySessionErrorCount++; + if (obj.relaySessionCounted) { parent.relaySessionCount--; delete obj.relaySessionCounted; } + console.log('Relay error from ' + cleanRemoteAddr(ws._socket.remoteAddress) + ', ' + err.toString().split('\r')[0] + '.'); closeBothSides(); }); // If the relay web socket is closed, close both sides. ws.on('close', function (req) { - obj.parent.relaySessionCount--; + if (obj.relaySessionCounted) { parent.relaySessionCount--; delete obj.relaySessionCounted; } closeBothSides(); }); @@ -221,58 +229,66 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain, user, cookie if (relayinfo.state == 2) { // Disconnect the peer var peer = (relayinfo.peer1 == obj) ? relayinfo.peer2 : relayinfo.peer1; - obj.parent.parent.debug(1, 'Relay disconnect: ' + obj.id + ' (' + obj.remoteaddr + ' --> ' + peer.remoteaddr + ')'); - peer.id = null; + try { if (peer.relaySessionCounted) { parent.relaySessionCount--; delete peer.relaySessionCounted; } } catch (ex) { console.log(ex); } + parent.parent.debug(1, 'Relay disconnect: ' + obj.id + ' (' + cleanRemoteAddr(ws._socket.remoteAddress) + ' --> ' + cleanRemoteAddr(peer.ws._socket.remoteAddress) + ')'); try { peer.ws.close(); } catch (e) { } // Soft disconnect try { peer.ws._socket._parent.end(); } catch (e) { } // Hard disconnect + + // Aggressive peer cleanup + delete peer.id; + delete peer.ws; + delete peer.peer; } else { - obj.parent.parent.debug(1, 'Relay disconnect: ' + obj.id + ' (' + obj.remoteaddr + ')'); + parent.parent.debug(1, 'Relay disconnect: ' + obj.id + ' (' + cleanRemoteAddr(ws._socket.remoteAddress) + ')'); } delete parent.wsrelays[obj.id]; } - obj.peer = null; - obj.id = null; } + + // Aggressive cleanup + delete obj.id; + delete obj.ws; + delete obj.peer; } // Mark this relay session as authenticated if this is the user end. - obj.authenticated = (obj.user != null); + obj.authenticated = (user != null); if (obj.authenticated) { // Kick off the routing, if we have agent routing instructions, process them here. // Routing instructions can only be given by a authenticated user - if ((obj.cookie != null) && (obj.cookie.nodeid != null) && (obj.cookie.tcpport != null) && (obj.cookie.domainid != null)) { + if ((cookie != null) && (cookie.nodeid != null) && (cookie.tcpport != null) && (cookie.domainid != null)) { // We have routing instructions in the cookie, but first, check user access for this node. - obj.parent.db.Get(obj.cookie.nodeid, function (err, docs) { + parent.db.Get(cookie.nodeid, function (err, docs) { if (docs.length == 0) { console.log('ERR: Node not found'); try { obj.close(); } catch (e) { } return; } // Disconnect websocket var node = docs[0]; // Check if this user has permission to manage this computer - var meshlinks = obj.user.links[node.meshid]; + var meshlinks = user.links[node.meshid]; if ((!meshlinks) || (!meshlinks.rights) || ((meshlinks.rights & MESHRIGHT_REMOTECONTROL) == 0)) { console.log('ERR: Access denied (2)'); try { obj.close(); } catch (e) { } return; } // Send connection request to agent if (obj.id == undefined) { obj.id = ('' + Math.random()).substring(2); } // If there is no connection id, generate one. - var command = { nodeid: obj.cookie.nodeid, action: 'msg', type: 'tunnel', value: '*/meshrelay.ashx?id=' + obj.id, tcpport: obj.cookie.tcpport, tcpaddr: obj.cookie.tcpaddr }; - obj.parent.parent.debug(1, 'Relay: Sending agent tunnel command: ' + JSON.stringify(command)); - if (obj.sendAgentMessage(command, obj.user._id, obj.cookie.domainid) == false) { obj.id = null; obj.parent.parent.debug(1, 'Relay: Unable to contact this agent (' + obj.remoteaddr + ')'); } + var command = { nodeid: cookie.nodeid, action: 'msg', type: 'tunnel', value: '*/meshrelay.ashx?id=' + obj.id, tcpport: cookie.tcpport, tcpaddr: cookie.tcpaddr }; + parent.parent.debug(1, 'Relay: Sending agent tunnel command: ' + JSON.stringify(command)); + if (obj.sendAgentMessage(command, user._id, cookie.domainid) == false) { delete obj.id; parent.parent.debug(1, 'Relay: Unable to contact this agent (' + cleanRemoteAddr(ws._socket.remoteAddress) + ')'); } performRelay(); }); return obj; } else if ((req.query.nodeid != null) && (req.query.tcpport != null)) { // We have routing instructions in the URL arguments, but first, check user access for this node. - obj.parent.db.Get(req.query.nodeid, function (err, docs) { + parent.db.Get(req.query.nodeid, function (err, docs) { if (docs.length == 0) { console.log('ERR: Node not found'); try { obj.close(); } catch (e) { } return; } // Disconnect websocket var node = docs[0]; // Check if this user has permission to manage this computer - var meshlinks = obj.user.links[node.meshid]; + var meshlinks = user.links[node.meshid]; if ((!meshlinks) || (!meshlinks.rights) || ((meshlinks.rights & MESHRIGHT_REMOTECONTROL) == 0)) { console.log('ERR: Access denied (2)'); try { obj.close(); } catch (e) { } return; } // Send connection request to agent if (obj.id == null) { obj.id = ('' + Math.random()).substring(2); } // If there is no connection id, generate one. var command = { nodeid: req.query.nodeid, action: 'msg', type: 'tunnel', value: '*/meshrelay.ashx?id=' + obj.id, tcpport: req.query.tcpport, tcpaddr: ((req.query.tcpaddr == null) ? '127.0.0.1' : req.query.tcpaddr) }; - obj.parent.parent.debug(1, 'Relay: Sending agent tunnel command: ' + JSON.stringify(command)); - if (obj.sendAgentMessage(command, obj.user._id, obj.domain.id) == false) { obj.id = null; obj.parent.parent.debug(1, 'Relay: Unable to contact this agent (' + obj.remoteaddr + ')'); } + parent.parent.debug(1, 'Relay: Sending agent tunnel command: ' + JSON.stringify(command)); + if (obj.sendAgentMessage(command, user._id, domain.id) == false) { delete obj.id; parent.parent.debug(1, 'Relay: Unable to contact this agent (' + cleanRemoteAddr(ws._socket.remoteAddress) + ')'); } performRelay(); }); return obj; diff --git a/meshuser.js b/meshuser.js index 6e3bb463..3e0b0c39 100644 --- a/meshuser.js +++ b/meshuser.js @@ -238,6 +238,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use "Connected Users": Object.keys(parent.wssessions).length, "Users Sessions": Object.keys(parent.wssessions2).length, "Relay Sessions": parent.relaySessionCount, + "Relay Count": Object.keys(parent.wsrelays).length }; if (parent.relaySessionErrorCount != 0) { serverStats['Relay Errors'] = parent.relaySessionErrorCount; } if (parent.parent.mpsserver != null) { serverStats['Connected Intel® AMT'] = Object.keys(parent.parent.mpsserver.ciraConnections).length; } @@ -519,7 +520,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use switch (cmd) { case 'help': { r = 'Available commands: help, info, versions, args, resetserver, showconfig, usersessions, tasklimiter, setmaxtasks, cores,\r\n' - r += 'migrationagents, swarmstats, nodeconfig, heapdump.'; + r += 'migrationagents, swarmstats, nodeconfig, heapdump, relays.'; break; } case 'info': { @@ -645,6 +646,16 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } break; } + case 'relays': { + for (var i in parent.wsrelays) { + r += 'id: ' + i + ', state: ' + parent.wsrelays[i].state; + if (parent.wsrelays[i].peer1 != null) { r += ', peer1: ' + cleanRemoteAddr(parent.wsrelays[i].peer1.ws._socket.remoteAddress); } + if (parent.wsrelays[i].peer2 != null) { r += ', peer2: ' + cleanRemoteAddr(parent.wsrelays[i].peer2.ws._socket.remoteAddress); } + r += '
'; + } + if (r == '') { r = 'No relays.'; } + break; + } default: { // This is an unknown command, return an error message r = 'Unknown command \"' + cmd + '\", type \"help\" for list of avaialble commands.'; break; @@ -2469,5 +2480,8 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use // Return true if at least one element of arr2 is in arr1 function findOne(arr1, arr2) { if ((arr1 == null) || (arr2 == null)) return false; return arr2.some(function (v) { return arr1.indexOf(v) >= 0; }); }; + // Clean a IPv6 address that encodes a IPv4 address + function cleanRemoteAddr(addr) { if (addr.startsWith('::ffff:')) { return addr.substring(7); } else { return addr; } } + return obj; }; \ No newline at end of file diff --git a/package.json b/package.json index db28ce6d..e38c9b63 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "meshcentral", - "version": "0.3.3-g", + "version": "0.3.3-h", "keywords": [ "Remote Management", "Intel AMT",