First working version with local device relay.
Ylian Saint-Hilaire committed
Apr 27, 2021 at 23:22 UTC
699f46c3192a0e336fa2803a363108688d5d26f1
4 files changed
+168
-7
meshrelay.js
+147
-1
@@ -938,4 +938,150 @@ a given size and timestamp. When looking at network traffic the flags are import
938
939
- If traffic has the first (0x0001) flag set, the data is binary otherwise it's a string.
940
- If the traffic has the second (0x0002) flag set, traffic is coming from the user's browser, if not, it's coming from the MeshAgent.
941
-*/
\ No newline at end of file
941
+*/
942
+
943
+
944
+
945
+
946
+module.exports.CreateLocalRelay = function (parent, ws, req, domain, user, cookie) {
947
+ CreateLocalRelayEx(parent, ws, req, domain, user, cookie);
948
+}
949
+
950
+function CreateLocalRelayEx(parent, ws, req, domain, user, cookie) {
951
+ const net = require('net');
952
+ var obj = {};
953
+ obj.id = Buffer.from(parent.crypto.randomBytes(9), 'binary').toString('base64');
954
+ obj.req = req;
955
+ obj.ws = ws;
956
+ obj.user = user;
957
+
958
+ // If there is no authentication, drop this connection
959
+ if (obj.user == null) { try { ws.close(); parent.parent.debug('relay', 'Relay: Connection with no authentication'); } catch (e) { console.log(e); } return; }
960
+
961
+ // Check for nodeid and tcpport
962
+ if ((req.query == null) || (req.query.nodeid == null) || (req.query.tcpport == null)) { try { ws.close(); parent.parent.debug('relay', 'Relay: Connection with invalid arguments'); } catch (e) { console.log(e); } return; }
963
+ const tcpport = parseInt(req.query.tcpport);
964
+ if ((typeof tcpport != 'number') || (tcpport < 1) || (tcpport > 65535)) { try { ws.close(); parent.parent.debug('relay', 'Relay: Connection with invalid arguments'); } catch (e) { console.log(e); } return; }
965
+ var nodeidsplit = req.query.nodeid.split('/');
966
+ if ((nodeidsplit.length != 3) || (nodeidsplit[0] != 'node') || (nodeidsplit[1] != domain.id) || (nodeidsplit[2].length < 10)) { try { ws.close(); parent.parent.debug('relay', 'Relay: Connection with invalid arguments'); } catch (e) { console.log(e); } return; }
967
+ obj.nodeid = req.query.nodeid;
968
+ obj.tcpport = tcpport;
969
+
970
+ // Relay session count (we may remove this in the future)
971
+ obj.relaySessionCounted = true;
972
+ parent.relaySessionCount++;
973
+
974
+ // Setup slow relay is requested. This will show down sending any data to this peer.
975
+ if ((req.query.slowrelay != null)) {
976
+ var sr = null;
977
+ try { sr = parseInt(req.query.slowrelay); } catch (ex) { }
978
+ if ((typeof sr == 'number') && (sr > 0) && (sr < 1000)) { obj.ws.slowRelay = sr; }
979
+ }
980
+
981
+ // Hold traffic until we connect to the target
982
+ ws._socket.pause();
983
+
984
+ // Mesh Rights
985
+ const MESHRIGHT_EDITMESH = 1;
986
+ const MESHRIGHT_MANAGEUSERS = 2;
987
+ const MESHRIGHT_MANAGECOMPUTERS = 4;
988
+ const MESHRIGHT_REMOTECONTROL = 8;
989
+ const MESHRIGHT_AGENTCONSOLE = 16;
990
+ const MESHRIGHT_SERVERFILES = 32;
991
+ const MESHRIGHT_WAKEDEVICE = 64;
992
+ const MESHRIGHT_SETNOTES = 128;
993
+ const MESHRIGHT_REMOTEVIEW = 256;
994
+
995
+ // Site rights
996
+ const SITERIGHT_SERVERBACKUP = 1;
997
+ const SITERIGHT_MANAGEUSERS = 2;
998
+ const SITERIGHT_SERVERRESTORE = 4;
999
+ const SITERIGHT_FILEACCESS = 8;
1000
+ const SITERIGHT_SERVERUPDATE = 16;
1001
+ const SITERIGHT_LOCKED = 32;
1002
+
1003
+ // Clean a IPv6 address that encodes a IPv4 address
1004
+ function cleanRemoteAddr(addr) { if (addr.startsWith('::ffff:')) { return addr.substring(7); } else { return addr; } }
1005
+
1006
+ // Disconnect
1007
+ obj.close = function (arg) {
1008
+ if ((arg == 1) || (arg == null)) { try { ws.close(); parent.parent.debug('relay', 'Relay: Soft disconnect'); } catch (e) { console.log(e); } } // Soft close, close the websocket
1009
+ if (arg == 2) { try { ws._socket._parent.end(); parent.parent.debug('relay', 'Relay: Hard disconnect'); } catch (e) { console.log(e); } } // Hard close, close the TCP socket
1010
+
1011
+ // Update the relay session count
1012
+ if (obj.relaySessionCounted) { parent.relaySessionCount--; delete obj.relaySessionCounted; }
1013
+
1014
+ // Log the disconnection
1015
+ if (obj.time) {
1016
+ var event = { etype: 'relay', action: 'relaylog', domain: domain.id, userid: obj.user._id, username: obj.user.name, msgid: 9, msgArgs: [obj.id, obj.req.clientIp, obj.host, Math.floor((Date.now() - obj.time) / 1000)], msg: 'Ended relay session \"' + obj.id + '\" from ' + obj.req.clientIp + ' to ' + obj.host + ', ' + Math.floor((Date.now() - obj.time) / 1000) + ' second(s)', nodeid: obj.req.query.nodeid };
1017
+ parent.parent.DispatchEvent(['*', user._id], obj, event);
1018
+ }
1019
+
1020
+ // Aggressive cleanup
1021
+ delete obj.ws;
1022
+ delete obj.req;
1023
+ delete obj.time;
1024
+ delete obj.nodeid;
1025
+ delete obj.meshid;
1026
+ delete obj.tcpport;
1027
+ delete obj.expireTimer;
1028
+ if (obj.client != null) { obj.client.destroy(); delete obj.client; }
1029
+ if (obj.pingtimer != null) { clearInterval(obj.pingtimer); delete obj.pingtimer; }
1030
+ if (obj.pongtimer != null) { clearInterval(obj.pongtimer); delete obj.pongtimer; }
1031
+
1032
+ // Unsubscribe
1033
+ if (obj.pid != null) { parent.parent.RemoveAllEventDispatch(obj); }
1034
+ };
1035
+
1036
+ // Send a PING/PONG message
1037
+ function sendPing() { try { obj.ws.send('{"ctrlChannel":"102938","type":"ping"}'); } catch (ex) { } }
1038
+ function sendPong() { try { obj.ws.send('{"ctrlChannel":"102938","type":"pong"}'); } catch (ex) { } }
1039
+
1040
+ function performRelay() {
1041
+ ws._socket.setKeepAlive(true, 240000); // Set TCP keep alive
1042
+
1043
+ // Setup the agent PING/PONG timers unless requested not to
1044
+ if (obj.req.query.noping != 1) {
1045
+ if ((typeof parent.parent.args.agentping == 'number') && (obj.pingtimer == null)) { obj.pingtimer = setInterval(sendPing, parent.parent.args.agentping * 1000); }
1046
+ else if ((typeof parent.parent.args.agentpong == 'number') && (obj.pongtimer == null)) { obj.pongtimer = setInterval(sendPong, parent.parent.args.agentpong * 1000); }
1047
+ }
1048
+
1049
+ parent.db.Get(obj.nodeid, function (err, docs) {
1050
+ if ((err != null) || (docs == null) || (docs.length != 1)) { try { obj.close(); } catch (e) { } return; } // Disconnect websocket
1051
+ const node = docs[0];
1052
+ obj.host = node.host;
1053
+ obj.meshid = node.meshid;
1054
+
1055
+ // Check if this user has permission to manage this computer
1056
+ if ((parent.GetNodeRights(obj.user, node.meshid, node._id) & MESHRIGHT_REMOTECONTROL) == 0) { console.log('ERR: Access denied (2)'); try { obj.close(); } catch (e) { } return; }
1057
+
1058
+ // Setup TCP client
1059
+ obj.client = new net.Socket();
1060
+ obj.client.connect(obj.tcpport, node.host, function () { ws.send('c'); ws._socket.resume(); });
1061
+ obj.client.on('data', function (data) { try { this.pause(); ws.send(data, this.clientResume); } catch (ex) { console.log(ex); } }); // Perform relay
1062
+ obj.client.on('close', function () { obj.close(); });
1063
+ obj.client.on('error', function (err) { obj.close(); });
1064
+ obj.client.clientResume = function () { try { obj.client.resume(); } catch (ex) { console.log(ex); } };
1065
+
1066
+ // Log the start of the connection
1067
+ obj.time = Date.now();
1068
+ var event = { etype: 'relay', action: 'relaylog', domain: domain.id, userid: obj.user._id, username: obj.user.name, msgid: 13, msgArgs: [obj.id, obj.req.clientIp, obj.host], msg: 'Started relay session \"' + obj.id + '\" from ' + obj.req.clientIp + ' to ' + obj.host, nodeid: req.query.nodeid };
1069
+ parent.parent.DispatchEvent(['*', obj.user._id, obj.meshid, obj.nodeid], obj, event);
1070
+ });
1071
+ }
1072
+
1073
+ ws.flushSink = function () { try { ws._socket.resume(); } catch (ex) { console.log(ex); } };
1074
+
1075
+ // When data is received from the mesh relay web socket
1076
+ ws.on('message', function (data) { if (typeof data != 'string') { try { ws._socket.pause(); obj.client.write(data, ws.flushSink); } catch (ex) { } } }); // Perform relay
1077
+
1078
+ // If error, close both sides of the relay.
1079
+ ws.on('error', function (err) { parent.relaySessionErrorCount++; obj.close(); });
1080
+
1081
+ // Relay web socket is closed
1082
+ ws.on('close', function (req) { obj.close(); });
1083
+
1084
+ // If this is not an authenticated session, or the session does not have routing instructions, just go ahead an connect to existing session.
1085
+ performRelay();
1086
+ return obj;
1087
+};
\ No newline at end of file
meshuser.js
+4
-2
@@ -3625,6 +3625,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
3625
if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) return; // Invalid domain, operation only valid for current domain
3626
if (common.validateString(command.devicename, 1, 256) == false) break; // Check device name
3627
if (common.validateString(command.hostname, 1, 256) == false) break; // Check hostname
3628
+ if (typeof command.type != 'number') break; // Type must be a number
3629
if ((command.type != 4) && (command.type != 6) && (command.type != 29)) break; // Check device type
3630
3631
// Get the mesh
@@ -3639,7 +3640,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
3640
parent.crypto.randomBytes(48, function (err, buf) {
3641
// Create the new node
3642
nodeid = 'node/' + domain.id + '/' + buf.toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
3642
- var device = { type: 'node', _id: nodeid, meshid: command.meshid, name: command.devicename, host: command.hostname, domain: domain.id, mtype: 3, agent: { id: command.type, caps: 0 } };
3643
+ var device = { type: 'node', _id: nodeid, meshid: command.meshid, mtype: 3, icon: 1, name: command.devicename, host: command.hostname, domain: domain.id, agent: { id: command.type, caps: 0 } };
3644
db.Set(device);
3645
3646
// Event the new node
@@ -3676,7 +3677,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
3677
parent.crypto.randomBytes(48, function (err, buf) {
3678
// Create the new node
3679
nodeid = 'node/' + domain.id + '/' + buf.toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
3679
- var device = { type: 'node', _id: nodeid, meshid: command.meshid, name: command.devicename, host: command.hostname, domain: domain.id, intelamt: { user: command.amtusername, pass: command.amtpassword, tls: command.amttls } };
3680
+ var device = { type: 'node', _id: nodeid, meshid: command.meshid, mtype: 1, icon: 1, name: command.devicename, host: command.hostname, domain: domain.id, intelamt: { user: command.amtusername, pass: command.amtpassword, tls: command.amttls } };
3681
db.Set(device);
3682
3683
// Event the new node
@@ -4316,6 +4317,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
4317
if (command.ip) { cookieContent.ip = command.ip; } // Indicates the browser want to agent to relay a TCP connection to a IP:port
4318
command.cookie = parent.parent.encodeCookie(cookieContent, parent.parent.loginCookieEncryptionKey);
4319
command.trustedCert = parent.isTrustedCert(domain);
4320
+ if (node.mtype == 3) { command.localRelay = true; }
4321
try { ws.send(JSON.stringify(command)); } catch (ex) { }
4322
});
4323
break;
views/default.handlebars
+5
-3
@@ -2463,11 +2463,12 @@
2463
var url = 'mcrouter://' + servername + portStr + domainUrl + 'control.ashx?c=' + authCookie + '&t=' + serverinfo.tlshash + '&l={{{lang}}}' + (urlargs.key?('&key=' + urlargs.key):'');
2464
if (message.nodeid != null) { url += ('&nodeid=' + message.nodeid); }
2465
if (message.tcpport != null) { url += ('&protocol=1&remoteport=' + message.tcpport); }
2466
+ if (message.localRelay) { url += '&local=1'; }
2467
if (message.ip != null) { url += ('&remoteip=' + message.ip); }
2468
url += ('&appid=' + message.protocol + '&autoexit=1'); // Protocol: 0 = Custom, 1 = HTTP, 2 = HTTPS, 3 = RDP, 4 = PuTTY, 5 = WinSCP
2469
downloadFile(url, '');
2470
} else if (message.tag == 'novnc') {
2470
- var vncurl = window.location.origin + domainUrl + 'novnc/vnc.html?ws=wss%3A%2F%2F' + window.location.host + encodeURIComponentEx(domainUrl) + 'meshrelay.ashx%3Fauth%3D' + message.cookie + '&show_dot=1' + (urlargs.key?('&key=' + urlargs.key):'') + '&l={{{lang}}}';
2471
+ var vncurl = window.location.origin + domainUrl + 'novnc/vnc.html?ws=wss%3A%2F%2F' + window.location.host + encodeURIComponentEx(domainUrl) + (message.localRelay?'local':'mesh') + 'relay.ashx%3Fauth%3D' + message.cookie + '&show_dot=1' + (urlargs.key?('&key=' + urlargs.key):'') + '&l={{{lang}}}';
2472
var node = getNodeFromId(message.nodeid);
2473
if (node != null) { vncurl += '&name=' + encodeURIComponentEx(node.name); }
2474
safeNewWindow(vncurl, 'mcnovnc/' + message.nodeid);
@@ -2475,6 +2476,7 @@
2476
var rdpurl = window.location.origin + domainUrl + 'mstsc.html?ws=' + message.cookie + (urlargs.key?('&key=' + urlargs.key):'');
2477
var node = getNodeFromId(message.nodeid);
2478
if (node != null) { rdpurl += '&name=' + encodeURIComponentEx(node.name); }
2479
+ if (message.localRelay) { url += '&local=1'; }
2480
safeNewWindow(rdpurl, 'mcmstsc/' + message.nodeid);
2481
}
2482
break;
@@ -4455,7 +4457,7 @@
4457
function addLocalDeviceToMeshEx(button, meshid) {
4458
var host = Q('dp1hostname').value;
4459
if (host == '') host = Q('dp1devicename').value;
4458
- meshserver.send({ action: 'addlocaldevice', meshid: meshid, devicename: Q('dp1devicename').value, hostname: host, type: Q('dp1type').value });
4460
+ meshserver.send({ action: 'addlocaldevice', meshid: meshid, devicename: Q('dp1devicename').value, hostname: host, type: parseInt(Q('dp1type').value) });
4461
}
4462
4463
function addDeviceToMesh(meshid) {
@@ -7154,7 +7156,7 @@
7156
function p10showChangeGroupDialog(nodeids) {
7157
if (xxdialogMode) return false;
7158
var targetMeshId = null;
7157
- if (nodeids.length == 1) { try { targetMeshId = meshes[getNodeFromId(nodeids[0])]._id; } catch (ex) { } }
7159
+ if (nodeids.length == 1) { try { targetMeshId = meshes[getNodeFromId(nodeids[0]).meshid]._id; } catch (ex) { } }
7160
7161
// List all available alternative groups
7162
var y = '<select id=p10newGroup style=width:236px>', count = 0, altGroups = [];
webserver.js
+12
-1
@@ -5355,7 +5355,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
5355
parent.debug('webrequest', '(' + req.clientIp + ') ' + req.url);
5356
5357
// Skip the rest is this is an agent connection
5358
- if ((req.url.indexOf('/meshrelay.ashx/.websocket') >= 0) || (req.url.indexOf('/agent.ashx/.websocket') >= 0)) { next(); return; }
5358
+ if ((req.url.indexOf('/meshrelay.ashx/.websocket') >= 0) || (req.url.indexOf('/agent.ashx/.websocket') >= 0) || (req.url.indexOf('/localrelay.ashx/.websocket') >= 0)) { next(); return; }
5359
5360
// If this domain has configured headers, use them.
5361
// Example headers: { 'Strict-Transport-Security': 'max-age=360000;includeSubDomains' };
@@ -5538,6 +5538,17 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
5538
}
5539
});
5540
});
5541
+ if (obj.args.wanonly != true) { // If the server is not in WAN mode, allow server relayed connections.
5542
+ obj.app.ws(url + 'localrelay.ashx', function (ws, req) {
5543
+ PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie) {
5544
+ if ((user == null) || (cookie == null)) {
5545
+ try { ws1.close(); } catch (ex) { }
5546
+ } else {
5547
+ obj.meshRelayHandler.CreateLocalRelay(obj, ws1, req1, domain, user, cookie); // Local relay
5548
+ }
5549
+ });
5550
+ });
5551
+ }
5552
if (domain.agentinvitecodes == true) {
5553
obj.app.get(url + 'invite', handleInviteRequest);
5554
obj.app.post(url + 'invite', handleInviteRequest);