Skip to content

Commit e0047eb

Browse files
committed
Merge pull request #1630 from uProxy/kennysong-pmp-upnp-probing
Added NAT-PMP, PCP, UPnP probing functionality
2 parents df927e0 + 60df8be commit e0047eb

File tree

4 files changed

+417
-8
lines changed

4 files changed

+417
-8
lines changed

src/chrome/app/manifest.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
}
1515
},
1616
"permissions": [
17-
"storage"
17+
"storage",
18+
"http://*/"
1819
],
1920
"sockets": {
2021
"tcp": { "connect" : "" },

src/generic_core/diagnose-nat.ts

Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22
/// <reference path='../../../third_party/freedom-typings/udp-socket.d.ts' />
33
/// <reference path='../../../third_party/sha1/sha1.d.ts' />
44
/// <reference path='../../../third_party/typings/lodash/lodash.d.ts' />
5+
/// <reference path='../../../third_party/typings/generic/url.d.ts' />
6+
/// <reference path='../../../third_party/ipaddrjs/ipaddrjs.d.ts' />
7+
58

69
import arraybuffers = require('../../../third_party/uproxy-lib/arraybuffers/arraybuffers');
710
import logging = require('../../../third_party/uproxy-lib/logging/logging');
811
import _ = require('lodash');
912
import globals = require('./globals');
1013
import sha1 = require('crypto/sha1');
14+
import ipaddr = require('ipaddr.js');
1115

1216
// Both Ping and NAT type detection need help from a server. The following
1317
// ip/port is the instance we run on EC2.
@@ -854,3 +858,332 @@ export function doNatProvoking() :Promise<string> {
854858
});
855859
});
856860
}
861+
862+
// Closes the OS-level sockets and discards its Freedom object
863+
function closeSocket(socket:freedom_UdpSocket.Socket) {
864+
socket.destroy().then(() => {
865+
freedom['core.udpsocket'].close(socket);
866+
});
867+
}
868+
869+
// Test if NAT-PMP is supported by the router, returns a boolean
870+
export function probePmpSupport(routerIp:string, privateIp:string) :Promise<boolean> {
871+
var socket :freedom_UdpSocket.Socket;
872+
var _probePmpSupport = new Promise((F, R) => {
873+
socket = freedom['core.udpsocket']();
874+
875+
// Fulfill when we get any reply (failure is on timeout in wrapper function)
876+
socket.on('onData', (pmpResponse:freedom_UdpSocket.RecvFromInfo) => {
877+
closeSocket(socket);
878+
F(true);
879+
});
880+
881+
// Bind a UDP port and send a NAT-PMP request
882+
socket.bind(privateIp, 0).
883+
then((result:number) => {
884+
if (result != 0) {
885+
R(new Error('Failed to bind to a port: Err= ' + result));
886+
}
887+
888+
// Construct the NAT-PMP map request as an ArrayBuffer
889+
// Map internal port 55555 to external port 55555 w/ 120 sec lifetime
890+
var pmpBuffer = new ArrayBuffer(12);
891+
var pmpView = new DataView(pmpBuffer);
892+
// Version and OP fields (1 byte each)
893+
pmpView.setInt8(0, 0);
894+
pmpView.setInt8(1, 1);
895+
// Reserved, internal port, external port fields (2 bytes each)
896+
pmpView.setInt16(2, 0, false);
897+
pmpView.setInt16(4, 55555, false);
898+
pmpView.setInt16(6, 55555, false);
899+
// Mapping lifetime field (4 bytes)
900+
pmpView.setInt32(8, 120, false);
901+
902+
socket.sendTo(pmpBuffer, routerIp, 5351);
903+
});
904+
});
905+
906+
// Give _probePmpSupport 2 seconds before timing out
907+
return Promise.race([
908+
countdownFulfill(2000, false, () => { closeSocket(socket); }),
909+
_probePmpSupport
910+
]);
911+
}
912+
913+
// Test if PCP is supported by the router, returns a boolean
914+
export function probePcpSupport(routerIp:string, privateIp:string) :Promise<boolean> {
915+
var socket :freedom_UdpSocket.Socket;
916+
var _probePcpSupport = new Promise((F, R) => {
917+
socket = freedom['core.udpsocket']();
918+
919+
// Fulfill when we get any reply (failure is on timeout in wrapper function)
920+
socket.on('onData', (pcpResponse:freedom_UdpSocket.RecvFromInfo) => {
921+
closeSocket(socket);
922+
F(true);
923+
});
924+
925+
// Bind a UDP port and send a PCP request
926+
socket.bind(privateIp, 0).
927+
then((result:number) => {
928+
if (result != 0) {
929+
R(new Error('Failed to bind to a port: Err= ' + result));
930+
}
931+
932+
// Create the PCP MAP request as an ArrayBuffer
933+
// Map internal port 55556 to external port 55556 w/ 120 sec lifetime
934+
var pcpBuffer = new ArrayBuffer(60);
935+
var pcpView = new DataView(pcpBuffer);
936+
// Version field (1 byte)
937+
pcpView.setInt8(0, 0b00000010);
938+
// R and Opcode fields (1 bit + 7 bits)
939+
pcpView.setInt8(1, 0b00000001);
940+
// Reserved field (2 bytes)
941+
pcpView.setInt16(2, 0, false);
942+
// Requested lifetime (4 bytes)
943+
pcpView.setInt32(4, 120, false);
944+
// Client IP address (128 bytes; we use the IPv4 -> IPv6 mapping)
945+
pcpView.setInt32(8, 0, false);
946+
pcpView.setInt32(12, 0, false);
947+
pcpView.setInt16(16, 0, false);
948+
pcpView.setInt16(18, 0xffff, false);
949+
// Start of IPv4 octets of the client's private IP
950+
var ipOctets = ipaddr.IPv4.parse(privateIp).octets;
951+
pcpView.setInt8(20, ipOctets[0]);
952+
pcpView.setInt8(21, ipOctets[1]);
953+
pcpView.setInt8(22, ipOctets[2]);
954+
pcpView.setInt8(23, ipOctets[3]);
955+
// Mapping Nonce (12 bytes)
956+
pcpView.setInt32(24, randInt(0, 0xffffffff), false);
957+
pcpView.setInt32(28, randInt(0, 0xffffffff), false);
958+
pcpView.setInt32(32, randInt(0, 0xffffffff), false);
959+
// Protocol (1 byte)
960+
pcpView.setInt8(36, 17);
961+
// Reserved (3 bytes)
962+
pcpView.setInt16(37, 0, false);
963+
pcpView.setInt8(39, 0);
964+
// Internal and external ports
965+
pcpView.setInt16(40, 55556, false);
966+
pcpView.setInt16(42, 55556, false);
967+
// External IP address (128 bytes; we use the all-zero IPv4 -> IPv6 mapping)
968+
pcpView.setFloat64(44, 0, false);
969+
pcpView.setInt16(52, 0, false);
970+
pcpView.setInt16(54, 0xffff, false);
971+
pcpView.setInt32(56, 0, false);
972+
973+
socket.sendTo(pcpBuffer, routerIp, 5351);
974+
});
975+
});
976+
977+
// Give _probePcpSupport 2 seconds before timing out
978+
return Promise.race([
979+
countdownFulfill(2000, false, () => { closeSocket(socket); }),
980+
_probePcpSupport
981+
]);
982+
}
983+
984+
// Send if UPnP AddPortMapping is supported by the router
985+
// Returns a 'true' boolean if UPnP is supported
986+
// Errors with a message if something breaks or times out (not supported)
987+
export function probeUpnpSupport(privateIp:string) :Promise<boolean> {
988+
return new Promise((F, R) => {
989+
sendSsdpRequest(privateIp).
990+
then(fetchControlUrl).
991+
then((controlUrl:string) => sendAddPortMapping(controlUrl, privateIp)).
992+
then((result:boolean) => F(result)).
993+
catch((err:Error) => R(err));
994+
});
995+
}
996+
997+
// Send a UPnP SSDP request to discover UPnP devices on the network
998+
function sendSsdpRequest(privateIp:string) :Promise<ArrayBuffer> {
999+
var socket :freedom_UdpSocket.Socket;
1000+
var _sendSsdpRequest = new Promise((F, R) => {
1001+
socket = freedom['core.udpsocket']();
1002+
1003+
// Fulfill when we get any reply (failure is on timeout or invalid parsing)
1004+
socket.on('onData', (ssdpResponse:freedom_UdpSocket.RecvFromInfo) => {
1005+
closeSocket(socket);
1006+
F(ssdpResponse.data);
1007+
});
1008+
1009+
// Bind a socket and send the SSDP request
1010+
socket.bind(privateIp, 0).
1011+
then((result:number) => {
1012+
if (result != 0) {
1013+
R(new Error('Failed to bind to a port: Err= ' + result));
1014+
}
1015+
1016+
// Construct and send a UPnP SSDP message
1017+
var ssdpStr = 'M-SEARCH * HTTP/1.1\r\n' +
1018+
'HOST: 239.255.255.250:1900\r\n' +
1019+
'MAN: ssdp:discover\r\n' +
1020+
'MX: 10\r\n' +
1021+
'ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1';
1022+
var ssdpBuffer = arraybuffers.stringToArrayBuffer(ssdpStr);
1023+
socket.sendTo(ssdpBuffer, '239.255.255.250', 1900);
1024+
});
1025+
});
1026+
1027+
// Give _sendSsdpRequest 1 second before timing out
1028+
return Promise.race([
1029+
countdownReject(1000, '(SSDP timed out)', () => { closeSocket(socket); }),
1030+
_sendSsdpRequest
1031+
]);
1032+
}
1033+
1034+
// Get the UPnP control URL from SSDP response
1035+
function fetchControlUrl(ssdpResponse:ArrayBuffer) :Promise<string> {
1036+
var _fetchControlUrl = new Promise((F, R) => {
1037+
// Get UPnP device profile URL from the LOCATION header
1038+
var ssdpStr = arraybuffers.arrayBufferToString(ssdpResponse);
1039+
var startIndex = ssdpStr.indexOf('LOCATION: ') + 10;
1040+
var endIndex = ssdpStr.indexOf('\n', startIndex);
1041+
var locationUrl = ssdpStr.substring(startIndex, endIndex);
1042+
1043+
// Reject if there is no LOCATION header
1044+
if (startIndex === -1) {
1045+
R(new Error('(No LOCATION header for UPnP device)'));
1046+
}
1047+
1048+
// Get the XML device description at location URL
1049+
var xhr = new XMLHttpRequest();
1050+
xhr.open('GET', locationUrl, true);
1051+
xhr.onreadystatechange = () => {
1052+
if (xhr.readyState === 4) {
1053+
// Get control URL from XML file
1054+
// Ideally we would parse and traverse the XML tree for this,
1055+
// but DOMParser is not available here
1056+
var xmlDoc = xhr.responseText;
1057+
var preIndex = xmlDoc.indexOf('WANIPConnection');
1058+
var startIndex = xmlDoc.indexOf('<controlUrl>', preIndex) + 13;
1059+
var endIndex = xmlDoc.indexOf('</controlUrl>', startIndex);
1060+
1061+
// Reject if there is no controlUrl
1062+
if (preIndex === -1 || startIndex === -1) {
1063+
R(new Error('(Could not parse control URL)'));
1064+
}
1065+
1066+
// Combine the controlUrl path with the locationUrl
1067+
var controlUrlPath = xmlDoc.substring(startIndex, endIndex);
1068+
var locationUrlParser = new URL(locationUrl);
1069+
var controlUrl = 'http://' + locationUrlParser.host +
1070+
'/' + controlUrlPath;
1071+
1072+
F(controlUrl);
1073+
}
1074+
}
1075+
xhr.send();
1076+
});
1077+
1078+
// Give _fetchControlUrl 1 second before timing out
1079+
return Promise.race([
1080+
countdownReject(1000, '(Timed out when retrieving description XML)'),
1081+
_fetchControlUrl
1082+
]);
1083+
}
1084+
1085+
// Send a UPnP AddPortMapping request
1086+
function sendAddPortMapping(controlUrl:string, privateIp:string) :Promise<boolean> {
1087+
var _sendAddPortMapping = new Promise((F, R) => {
1088+
var internalPort = 55557;
1089+
var externalPort = 55557;
1090+
var leaseDuration = 120; // Note: Some routers may not support a non-zero duration
1091+
1092+
// Create the AddPortMapping SOAP request string
1093+
var apm = '<?xml version="1.0"?>' +
1094+
'<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">' +
1095+
'<s:Body>' +
1096+
'<u:AddPortMapping xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">' +
1097+
'<NewExternalPort>' + externalPort + '</NewExternalPort>' +
1098+
'<NewProtocol>UDP</NewProtocol>' +
1099+
'<NewInternalPort>' + internalPort + '</NewInternalPort>' +
1100+
'<NewInternalClient>' + privateIp + '</NewInternalClient>' +
1101+
'<NewEnabled>1</NewEnabled>' +
1102+
'<NewPortMappingDescription>uProxy UPnP probe</NewPortMappingDescription>' +
1103+
'<NewLeaseDuration>' + leaseDuration + '</NewLeaseDuration>' +
1104+
'</u:AddPortMapping>' +
1105+
'</s:Body>' +
1106+
'</s:Envelope>';
1107+
1108+
// Create an XMLHttpRequest that encapsulates the SOAP string
1109+
var xhr = new XMLHttpRequest();
1110+
xhr.open('POST', controlUrl, true);
1111+
xhr.setRequestHeader('Content-Type', 'text/xml');
1112+
xhr.setRequestHeader('SOAPAction', '"urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping"');
1113+
1114+
// Send the AddPortMapping request
1115+
xhr.onreadystatechange = () => {
1116+
if (xhr.readyState === 4) { F(true); }
1117+
}
1118+
xhr.send(apm);
1119+
});
1120+
1121+
// Give _sendAddPortMapping 1 second to run before timing out
1122+
return Promise.race([
1123+
countdownReject(1000, '(AddPortMapping timed out)'),
1124+
_sendAddPortMapping
1125+
]);
1126+
}
1127+
1128+
// Returns a promise for the internal IP address of the computer
1129+
export function getInternalIp() :Promise<string> {
1130+
var _getInternalIp = new Promise((F, R) => {
1131+
var pc = freedom['core.rtcpeerconnection']({
1132+
iceServers: globals.settings.stunServers
1133+
});
1134+
1135+
// One of the ICE candidates is the internal host IP; return it
1136+
pc.on('onicecandidate',
1137+
(candidate?:freedom_RTCPeerConnection.OnIceCandidateEvent) => {
1138+
if (candidate.candidate) {
1139+
var cand = candidate.candidate.candidate.split(' ');
1140+
if (cand[7] === 'host') {
1141+
var internalIp = cand[4];
1142+
if (ipaddr.IPv4.isValid(internalIp)) {
1143+
F(internalIp);
1144+
}
1145+
}
1146+
}
1147+
});
1148+
1149+
// Set up the PeerConnection to start generating ICE candidates
1150+
pc.createDataChannel('dummy data channel').
1151+
then(pc.createOffer).
1152+
then(pc.setLocalDescription);
1153+
});
1154+
1155+
// Give _getInternalIp 2 seconds to run before timing out
1156+
return Promise.race([
1157+
countdownReject(2000, 'getInternalIp() failed'),
1158+
_getInternalIp
1159+
]);
1160+
}
1161+
1162+
// Generate a random integer between min and max
1163+
function randInt(min:number, max:number) :number {
1164+
return Math.floor(Math.random() * (max - min + 1)) + min;
1165+
}
1166+
1167+
// Return a promise that fulfills in a given time with a boolean
1168+
// Can run a callback function before fulfilling
1169+
function countdownFulfill(time:number, bool:boolean,
1170+
callback?:Function) :Promise<boolean> {
1171+
return new Promise<boolean>((F, R) => {
1172+
setTimeout(() => {
1173+
if (callback !== undefined) { callback(); }
1174+
F(bool);
1175+
}, time);
1176+
});
1177+
}
1178+
1179+
// Return a promise that rejects in a given time with an Error message
1180+
// Can call a callback function before rejecting
1181+
function countdownReject(time:number, msg:string,
1182+
callback?:Function) :Promise<any> {
1183+
return new Promise<any>((F, R) => {
1184+
setTimeout(() => {
1185+
if (callback !== undefined) { callback(); }
1186+
R(new Error(msg));
1187+
}, time);
1188+
});
1189+
}

0 commit comments

Comments
 (0)