├── README.md ├── anygram.js ├── index.js ├── irc.js ├── package.json ├── punch.js └── stun.js /README.md: -------------------------------------------------------------------------------- 1 | AnyGram 2 | ------ 3 | 4 | Datagram forwarding behind NAT 5 | 6 | Anygram uses STUN to get the mapped address, IRC to signal peers. 7 | 8 | 9 | ### Install 10 | ```bash 11 | npm -g install anygram@latest 12 | ``` 13 | 14 | 15 | ### CLI 16 | 17 | #### Server 18 | ```bash 19 | anygram -n [serverNick] -m server -P [serverPort] 20 | ``` 21 | 22 | #### Client 23 | ```bash 24 | anygram -n [clientNick] -m client -s [serverNick] -p [clientPort] -P [serverPort] 25 | ``` 26 | 27 | The commands above will forward packets to 127.0.0.1:`clientPort`@client 28 | to 127.0.0.1:`serverPort`@server. For more options, see `anygram --help` 29 | 30 | To run the commands forever, consider using 31 | [forever](https://github.com/foreverjs/forever) 32 | 33 | 34 | ### API 35 | 36 | ```js 37 | var anygram = require('anygram')(config); 38 | ``` 39 | 40 | `config` uses the same command line options. 41 | 42 | In AnyGram, all sockets are UDP sockets. 43 | 44 | #### anygram.punch(socket) 45 | 46 | Returns a promise of socket. `socket.rinfo` may change if the remote NAT 47 | is symmetric. The punching process usually succeeds if not both NATs are 48 | symmetric. 49 | 50 | `socket` is a UDP socket plus two attributes `linfo` and `rinfo`, 51 | obtained by calling `anygram.stun` 52 | 53 | The `rinfo.punchTime` attribute also indicates when to start punching. 54 | This option is to make sure both sides start punching at the same time 55 | in spite of 56 | the (sometimes huge) IRC time lag. 57 | 58 | #### anygram.stun(stunServer) 59 | 60 | Returns a promise of socket. `socket.linfo` will include the mapped 61 | `port`, `address` and the NAT `type`. 62 | 63 | `stunServer` is the hostname of a stun server listening at 3478 64 | 65 | #### anygram.irc(config) 66 | 67 | Returns an IRC client. 68 | 69 | The config should specify the `name` and `pass`(if any) of your IRC 70 | account. The IRC server's `host` and `port` are optional. The client 71 | will send PING packets at the `keepalive` interval. 72 | 73 | #### anygram.connect(irc, to, rinfo) 74 | 75 | Returns a promise of socket. 76 | 77 | `irc` is the IRC **client** 78 | 79 | `to` is the name of the peer you are connecting to 80 | 81 | `rinfo`(optional) is specified if you already got peer's rinfo 82 | 83 | #### anygram.createServer(irc, onconn, onerr) 84 | 85 | Start listening on the `irc` **client** for incoming connections 86 | 87 | `onconn` is called when connected with a peer successfully 88 | 89 | `onerr` is called on error 90 | 91 | #### anygram.send(socket, msg, lport, rport) 92 | 93 | Sends `msg` with 4 bytes header (`lport` and `rport`) 94 | 95 | #### anygram.onrecv(socket, cb) 96 | 97 | Parses received messages to cb(`msg`, `lport`, `rport`) 98 | 99 | Notice that the `rport`@remote will become `lport`@local and vice versa. 100 | 101 | 102 | ### Credits 103 | 104 | * [commander](https://github.com/tj/commander.js) 105 | * [node-stun](https://github.com/enobufs/stun) 106 | * [slate-irc](https://github.com/slate/slate-irc) 107 | -------------------------------------------------------------------------------- /anygram.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | var cmd = require('commander'); 4 | 5 | cmd 6 | .version(require('./package').version) 7 | .option('--host ', 'IRC Host (default: irc.freenode.net)') 8 | .option('--port ', 'IRC Port (default: 6667)') 9 | .option('-n, --name ', 'IRC Nickname') 10 | .option('--pass ', 'IRC Password (if any)') 11 | .option('--keepalive ', 'IRC KeepAlive interval in ms (default: 15000)') 12 | .option('--stunServer ', 'default: stun.ucsb.edu') 13 | .option('--timeLag ', 'max IRC time lag in ms (default: 10000, server only)') 14 | .option('-m, --mode ', 'client or server') 15 | .option('-s, --server ', 'nickname of server (client only)') 16 | .option('-p, --clientPort ', 'the port client listening on (client only)') 17 | .option('-P, --serverPort ', 'the port server forwarding to') 18 | .option('--postup ', 'postup script') 19 | .parse(process.argv); 20 | 21 | if (!cmd.name || !cmd.mode || !cmd.serverPort) 22 | cmd.help(); 23 | 24 | var cp = require('child_process'); 25 | var onexit = require('exit-hook'); 26 | var run = (script) => { 27 | if (!script) return; 28 | script = cp.exec(script); 29 | onexit(() => { 30 | script.kill(); 31 | }); 32 | }; 33 | 34 | var irc = require('./irc')(cmd); 35 | irc.on('error', e=>{throw Error(e)}); 36 | 37 | var anygram = require('.')(cmd); 38 | var dgram = require('dgram'); 39 | 40 | if (cmd.mode === 'client') { 41 | if (!cmd.server || !cmd.clientPort) cmd.help(); 42 | anygram.connect(irc, cmd.server).then(socket => { 43 | 44 | console.log('connected'); 45 | irc.removeAllListeners('error'); 46 | irc.on('error', (e) => {}); 47 | irc.quit('thank you!'); 48 | 49 | var server = dgram.createSocket('udp4'); 50 | server.bind(cmd.clientPort, '127.0.0.1'); 51 | 52 | server.on('message', (msg, l) => anygram.send(socket, msg, l.port, cmd.serverPort)); 53 | anygram.onrecv(socket, (msg, lport) => server.send(msg, lport, '127.0.0.1')); 54 | 55 | run(cmd.postup); 56 | 57 | }).catch(e=>{throw Error(e)}); 58 | } 59 | else if (cmd.mode === 'server') { 60 | anygram.createServer(irc, (socket) => { 61 | 62 | var pool = {}; 63 | anygram.onrecv(socket, (msg, lport, rport) => { 64 | if (!pool[rport]) { 65 | pool[rport] = dgram.createSocket('udp4'); 66 | pool[rport].on('message', (msg) => anygram.send(socket, msg, cmd.serverPort, rport)); 67 | } 68 | pool[rport].send(msg, cmd.serverPort, '127.0.0.1'); 69 | }); 70 | 71 | }); 72 | 73 | run(cmd.postup); 74 | } 75 | else { 76 | cmd.help(); 77 | } 78 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var punch = require('./punch'); 2 | var stun = require('./stun'); 3 | 4 | module.exports = (config) => { 5 | 6 | config.stunServer = config.stunServer || 'stun.ucsb.edu'; 7 | config.timeLag = config.timeLag || 10000; 8 | 9 | var exports = { 10 | punch: punch, 11 | stun: stun, 12 | irc: require('./irc') 13 | }; 14 | 15 | exports.connect = (irc, to, rinfo) => new Promise((res, rej) => 16 | stun(config.stunServer).then(socket => { 17 | var onrinfo = (rinfo) => { 18 | socket.rinfo = rinfo; 19 | setTimeout( 20 | () => punch(socket).then(res).catch(rej), 21 | rinfo.punchTime - Date.now() 22 | ); 23 | }; 24 | var onmsg = (e) => { 25 | if (e.from === to) { 26 | irc.removeListener('message', onmsg); 27 | onrinfo(JSON.parse(e.message)); 28 | } 29 | }; 30 | if (rinfo) { 31 | socket.linfo.punchTime = rinfo.punchTime = Date.now() + config.timeLag; 32 | onrinfo(rinfo); 33 | } else { 34 | irc.on('message', onmsg); 35 | } 36 | irc.send(to, JSON.stringify(socket.linfo)); 37 | }).catch(rej)); 38 | 39 | exports.createServer = (irc, onconn, onerr) => { 40 | onerr = onerr || console.error; 41 | irc.on('message', e => { 42 | try { 43 | var rinfo = JSON.parse(e.message); 44 | exports.connect(irc, e.from, rinfo).then(onconn).catch(onerr); 45 | } catch (e) {} 46 | }); 47 | }; 48 | 49 | exports.send = (socket, msg, lport, rport) => { 50 | var buf = new Buffer(4); 51 | buf.writeUInt16BE(lport, 0); 52 | buf.writeUInt16BE(rport, 2); 53 | socket.send(Buffer.concat([buf, msg]), socket.rinfo.port, socket.rinfo.address); 54 | }; 55 | 56 | exports.onrecv = (socket, cb) => { 57 | socket.on('message', (msg) => { 58 | if (msg.length <= 4) return ; 59 | var lport = msg.readUInt16BE(2); 60 | var rport = msg.readUInt16BE(0); 61 | cb(msg.slice(4), lport, rport); 62 | }); 63 | }; 64 | 65 | return exports; 66 | }; 67 | -------------------------------------------------------------------------------- /irc.js: -------------------------------------------------------------------------------- 1 | var irc = require('slate-irc'); 2 | var net = require('net'); 3 | 4 | module.exports = (e) => { 5 | e.host = e.host || 'irc.freenode.net'; 6 | e.port = e.port || 6667; 7 | e.pass = e.pass || '*'; 8 | 9 | var client = irc(net.connect(e)); 10 | client.pass(e.pass); 11 | client.nick(e.name); 12 | client.user(e.name, e.name); 13 | 14 | client.on('data', (msg) => { 15 | if ('PING' == msg.command) 16 | client.write('PONG :' + msg.trailing); 17 | }); 18 | client.timer = setInterval(() => { 19 | client.stream.write('PING :KeepAlive\n'); 20 | }, e.keepalive || 15000); 21 | 22 | client.stream.on('end', () => client.emit('error', 'IRC connection ended')); 23 | client.stream.on('error', (err) => client.emit('error', err)); 24 | 25 | client.config = e; 26 | return client; 27 | }; 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "anygram", 3 | "version": "0.2.2", 4 | "description": "Datagram forwarding behind NAT", 5 | "main": "index.js", 6 | "bin": { 7 | "anygram": "anygram.js" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "isofew@gmail.com", 13 | "license": "ISC", 14 | "dependencies": { 15 | "commander": "^2.9.0", 16 | "exit-hook": "^1.1.1", 17 | "node-stun": "^0.1.0", 18 | "slate-irc": "^0.9.0" 19 | }, 20 | "devDependencies": {}, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/isofew/anygram.git" 24 | }, 25 | "keywords": [ 26 | "udp", 27 | "nat", 28 | "traversal" 29 | ], 30 | "bugs": { 31 | "url": "https://github.com/isofew/anygram/issues" 32 | }, 33 | "homepage": "https://github.com/isofew/anygram#readme" 34 | } 35 | -------------------------------------------------------------------------------- /punch.js: -------------------------------------------------------------------------------- 1 | var code = ':P'; 2 | 3 | var send = (socket, port) => { 4 | port = port || socket.rinfo.port; 5 | if (port < 1 || port > 65535) return; 6 | socket.send(code, port, socket.rinfo.address); 7 | }; 8 | 9 | module.exports = (socket) => new Promise((res, rej) => { 10 | console.log('start punching'); 11 | console.log(socket.linfo); 12 | console.log(socket.rinfo); 13 | 14 | if (socket.linfo.type === 'Symmetric' && socket.rinfo.type === 'Symmetric') { 15 | rej('cowardly refuse to punch against two symmetric NATs'); 16 | return ; 17 | } 18 | socket.on('message', function onmsg (msg, rinfo) { 19 | if (msg.toString() === code) { 20 | socket.rinfo.pong = true; 21 | socket.rinfo.port = rinfo.port; 22 | send(socket, rinfo.port); 23 | socket.removeListener('message', onmsg); 24 | res(socket); 25 | } 26 | }); 27 | for (var i = 0; i < 3; ++i) 28 | send(socket); 29 | 30 | setTimeout(() => { 31 | if (socket.rinfo.pong) return ; 32 | 33 | for (var epoch = 0; epoch < 3; ++epoch) { 34 | for (var i = 0; i <= 300; ++i) { 35 | setTimeout(((i) => () => { 36 | if (socket.rinfo.pong) return ; 37 | if (socket.linfo.type === 'Symmetric') { 38 | send(socket); 39 | } else { 40 | send(socket, socket.rinfo.port + i); 41 | send(socket, socket.rinfo.port - i); 42 | } 43 | })(i), 3 * i + 1000 * epoch); 44 | } 45 | } 46 | setTimeout(() => rej('UDP hole punching failed'), 3000); 47 | }, 1000); 48 | }); 49 | -------------------------------------------------------------------------------- /stun.js: -------------------------------------------------------------------------------- 1 | var stun = require('node-stun'); 2 | 3 | module.exports = (stunServer) => new Promise((res, rej) => { 4 | var client = stun.createClient(); 5 | client.setServerAddr(stunServer); 6 | client.start(e => { 7 | if (e) rej(e); 8 | 9 | var linfo = client.getMappedAddr(); 10 | linfo.type = client.isNatted() ? client.getNatType() : 'Open'; 11 | 12 | var socket = client._soc0; 13 | socket.linfo = linfo; 14 | socket.removeAllListeners(); 15 | res(socket); 16 | }); 17 | }); 18 | --------------------------------------------------------------------------------