-
-
Save smolleyes/a3dea6e5086dd4d064f6 to your computer and use it in GitHub Desktop.
simple upnp port mapping with nodejs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* node UPNP port forwarding PoC | |
This is a simple way to forward ports on NAT routers with UPNP. | |
This is a not-for-production hack that I found useful when testing apps | |
on my home network behind ny NAT router. | |
-satori / edited by smolleyes for freebox v6 | |
usage: (install/clone node-ip from https://github.com/indutny/node-ip) | |
================================================================================ | |
var upnp = require("./upnp"); | |
var myIp = require("node-ip").address(); | |
var timeout = 15000; //ms | |
upnp.searchGateway(timeout, function(err, gateway) { | |
if (err) throw err; | |
console.log("Found Gateway!"); | |
console.log("Fetching External IP ... "); | |
gateway.getExternalIP(function(err, ip) { | |
if (err) throw err; | |
console.log(ip); | |
console.log("Mapping port 8888->"+myIp+":8888 ... "); | |
gateway.AddPortMapping( | |
"TCP" // or "UDP" | |
, 8888 // External port | |
, 8888 // Internal Port | |
, myIp // Internal Host (ip address of your pc) | |
, "YOUR DESCRIPTION" // Description. | |
, function(err) { | |
if (err) throw err; | |
console.log("Success"); | |
console.log("Done."); | |
}); | |
}); | |
}); | |
================================================================================ | |
var url = require("url"); | |
var http = require("http"); | |
var dgram = require("dgram"); | |
var Buffer = require("buffer").Buffer; | |
// some const strings - dont change | |
const SSDP_PORT = 1900; | |
const bcast = "239.255.255.250"; | |
const ST = "urn:schemas-upnp-org:device:InternetGatewayDevice:1"; | |
const req = "M-SEARCH * HTTP/1.1\r\nHost:239.255.255.250:1900\r\n\ | |
ST:"+ST+"\r\nMan:\"ssdp:discover\"\r\nMX:3\r\n\r\n"; | |
const WANIP = "urn:schemas-upnp-org:service:WANIPConnection:1"; | |
const OK = "HTTP/1.1 200 OK"; | |
const SOAP_ENV_PRE = "<?xml version=\"1.0\"?>\n<s:Envelope \ | |
xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" \ | |
s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\"><s:Body>\n"; | |
const SOAP_ENV_POST = "</s:Body>\n</s:Envelope>\n"; | |
function searchGateway(timeout, callback) { | |
var self = this; | |
var reqbuf = new Buffer(req, "ascii"); | |
var socket = new dgram.Socket('udp4'); | |
var clients = {}; | |
var t; | |
if (timeout) { | |
t = setTimeout(function() { | |
onerror(new Error("searchGateway() timed out")); | |
}, timeout); | |
} | |
var onlistening = function() { | |
socket.setBroadcast(socket.fd, true); | |
// send a few packets just in case. | |
socket.send(reqbuf, 0, reqbuf.length, SSDP_PORT,bcast); | |
} | |
var onmessage = function(message, rinfo) { | |
message = message.toString(); | |
if (message.substr(0, OK.length) !== OK || | |
!message.indexOf(ST) || | |
!message.indexOf("location:")) return; | |
var l = url.parse(message.match(/location:(.+?)\r\n/i)[1].trim()); | |
if (clients[l.href]) return; | |
var client = clients[l.href] = http.createClient(l.port, l.hostname); | |
var request = client.request("GET", l.pathname, {"host": l.hostname}); | |
request.end(); | |
request.addListener('response', function (response) { | |
if (response.statusCode !== 200) return; | |
var resbuf = ""; | |
response.addListener('data', function (chunk) { resbuf += chunk }); | |
response.addListener("end", function() { | |
resbuf = resbuf.substr(resbuf.indexOf(WANIP) + WANIP.length); | |
var ipurl = resbuf.match(/<controlURL>(.+?)<\/controlURL>/i)[1].trim() | |
socket.close(); | |
clearTimeout(t); | |
callback(null, new Gateway(l.port, l.hostname, ipurl)); | |
}); | |
}); | |
} | |
var onerror = function(err) { | |
socket.close() ; | |
clearTimeout(t); | |
callback(err); | |
} | |
var onclose = function() { | |
socket.removeListener("listening", onlistening); | |
socket.removeListener("message", onmessage); | |
socket.removeListener("close", onclose); | |
socket.removeListener("error", onerror); | |
} | |
socket.addListener("listening", onlistening); | |
socket.addListener("message", onmessage); | |
socket.addListener("close", onclose); | |
socket.addListener("error", onerror); | |
socket.bind(SSDP_PORT); | |
} | |
exports.searchGateway = searchGateway; | |
function Gateway(port, host, path) { | |
this.port = port; | |
this.host = host; | |
this.path = path; | |
} | |
Gateway.prototype.getExternalIP = function(callback) { | |
var s = | |
"<u:GetExternalIPAddress xmlns:u=\"" + WANIP + "\">\ | |
</u:GetExternalIPAddress>\n"; | |
this._getSOAPResponse(s, "GetExternalIPAddress", function(err, xml) { | |
if (err) callback(err); | |
else callback(null, | |
xml.match(/<NewExternalIPAddress>(.+?)<\/NewExternalIPAddress>/i)[1]); | |
}); | |
} | |
Gateway.prototype.AddPortMapping = function(protocol | |
, extPort | |
, intPort | |
, host | |
, description | |
, callback) { | |
var s = | |
"<u:AddPortMapping \ | |
xmlns:u=\""+WANIP+"\">\ | |
<NewRemoteHost></NewRemoteHost>\ | |
<NewExternalPort>"+extPort+"</NewExternalPort>\ | |
<NewProtocol>"+protocol+"</NewProtocol>\ | |
<NewInternalPort>"+intPort+"</NewInternalPort>\ | |
<NewInternalClient>"+host+"</NewInternalClient>\ | |
<NewEnabled>1</NewEnabled>\ | |
<NewPortMappingDescription>"+description+"</NewPortMappingDescription>\ | |
<NewLeaseDuration>0</NewLeaseDuration>\ | |
</u:AddPortMapping>"; | |
this._getSOAPResponse(s, "AddPortMapping", callback); | |
} | |
Gateway.prototype._getSOAPResponse = function(soap, func, callback) { | |
var s = [SOAP_ENV_PRE, soap, SOAP_ENV_POST].join(""); | |
var client = http.createClient(this.port, this.host); | |
var hdrs = { "host" : this.host | |
, "SOAPACTION" : "\"" + WANIP + "#" + func + "\"" | |
, "content-type" : "text/xml" | |
, "content-length" : s.length }; | |
var request = client.request("POST", this.path, hdrs); | |
request.end(s); | |
request.addListener('response', function (response) { | |
if (response.statusCode !== 200) { | |
response.close(); | |
callback(new Error("Invalid SOAP action")); | |
return; | |
} | |
var buf = ""; | |
response.addListener('data', function (chunk) { buf += chunk }); | |
response.addListener('end', function () { callback(null, buf) }); | |
}); | |
} | |
*/ | |
socket.bind(SSDP_PORT);
should be changed to non 1900 port.(i cant receive any messages on this port(due to windows services already binded it?) )
on windows(at least) if client sending broadcast message to 239.255.255.250
then device answering (unicast) to the sender ip:port
tested on mikrotik upnp.
Microsoft Network Filter:
PC - 192.168.0.100 (binded 1901 port)
Router - 192.168.0.1
Request packet:
Ipv4: Src = 192.168.0.100, Dest = 239.255.255.250, Total IP Length = 122
Udp: SrcPort = 1901, DstPort = 1900, Length = 102
Answer packet:
Ipv4: Src = 192.168.0.1, Dest = 192.168.0.100, Total IP Length = 276
Udp: SrcPort = 55206, DstPort = 1901, Length = 256
Documentation:
To be found by a network search, a device MUST send a unicast UDP response to the source IP address and port that sent the request to the multicast address.
http://www.upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.1.pdf (page 33)
why is the whole thing commented out?
Anyone has an updated version?
No...
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
good work!