├── README.md ├── debug-routes.js ├── debug.html ├── dns-sd.js ├── mdns-swarm.js ├── package.json ├── scripts ├── one-peer.sh └── three-peers.sh ├── signal-server.js └── simple-swarm.js /README.md: -------------------------------------------------------------------------------- 1 | ## mdns-swarm 2 | 3 | create a swarm of connected webrtc peers using multicast DNS! 4 | 5 | ### Example 6 | 7 | Try this on two computers on the same network; for debug information you can 8 | run with `DEBUG=mdns-swarm*`. 9 | 10 | ```js 11 | var Swarm = require('./mdns-swarm.js'); 12 | var wrtc = require('wrtc'); 13 | 14 | var swarm = new Swarm('simple-swarm', {wrtc: wrtc}); 15 | 16 | swarm.on('peer', function (stream) { 17 | process.stdin.pipe(stream).pipe(process.stdout); 18 | }); 19 | ``` 20 | -------------------------------------------------------------------------------- /debug-routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var async = require('async'); 4 | var debug = require('debug')('signal-server'); 5 | var request = require('request'); 6 | var _ = require('lodash'); 7 | 8 | module.exports = function (app) { 9 | // this function is called with `this` bound to the Swarm instance 10 | var self = this; 11 | 12 | app.get('/debug.html', function (req, res) { 13 | var path = require('path'); 14 | 15 | res.sendFile(path.join(__dirname, 'debug.html')); 16 | }); 17 | 18 | app.get('/debug', function (req, res) { 19 | var hosts = _.keys(self._peers); 20 | 21 | var connections = hosts.map(function (host) { 22 | return self.connectivity[host]; 23 | }); 24 | 25 | var mine = _.zipObject(hosts, connections); 26 | 27 | if (req.query.all) { 28 | debug('← GET /debug?all=true'); 29 | 30 | async.map(hosts, function (host, cbMap) { 31 | debug('→ GET /debug %s', host); 32 | 33 | request.get({ 34 | url: self.connectivity[host] + 'debug', 35 | json: true 36 | }, function (err, response, body) { 37 | var result = {host: host}; 38 | 39 | if (err || !body) { 40 | result.connections = self.connectivity[host]; 41 | } else { 42 | result.connections = body; 43 | } 44 | 45 | cbMap(null, result); 46 | }); 47 | }, function (err, results) { 48 | if (err) { 49 | return res.send({error: err}); 50 | } 51 | 52 | var resultsObject = _.zipObject(_.pluck(results, 'host'), 53 | _.pluck(results, 'connections')); 54 | 55 | 56 | resultsObject[self] = mine; 57 | 58 | res.send(resultsObject); 59 | }); 60 | 61 | return; 62 | } 63 | 64 | debug('← GET /debug'); 65 | 66 | res.send(mine); 67 | }); 68 | }; 69 | -------------------------------------------------------------------------------- /debug.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /dns-sd.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var debug = require('debug'); 4 | var mainDebug = require('debug')('dns-sd'); 5 | var events = require('events'); 6 | var ip = require('ip-address'); 7 | var multicastdns = require('multicast-dns'); 8 | var network = require('network'); 9 | var os = require('os'); 10 | var util = require('util'); 11 | var _ = require('lodash'); 12 | 13 | var ADVERTISE_INTERVAL = 4.9 * 1000; 14 | var QUERY_INTERVAL = 4.9 * 1000; 15 | 16 | // TODO: sort these based on local -> remote? 17 | function getAddresses(hostname) { 18 | var addresses = _(os.networkInterfaces()).map(function (value) { 19 | return value; 20 | }) 21 | .flatten() 22 | .filter(function (address) { 23 | return !address.internal; 24 | }) 25 | .valueOf(); 26 | 27 | var addresses4 = addresses.filter(function (address) { 28 | return address.family === 'IPv4'; 29 | }).map(function (address) { 30 | return { 31 | name: hostname, 32 | type: 'A', 33 | data: address.address 34 | }; 35 | }); 36 | 37 | var addresses6 = addresses.filter(function (address) { 38 | if (address.family !== 'IPv6') { 39 | return false; 40 | } 41 | 42 | var v6 = new ip.v6.Address(address.address); 43 | 44 | return v6.isValid() && 45 | (v6.getScope() === 'Global' || v6.getScope() === 'Reserved'); 46 | }).map(function (address) { 47 | return { 48 | name: hostname, 49 | type: 'AAAA', 50 | data: address.address 51 | }; 52 | }).sort(function (a, b) { 53 | // Prioritize link-local unicast addresses first 54 | var x = a.data; 55 | var y = b.data; 56 | 57 | if (x.indexOf('fe80') === 0) { 58 | x = ' ' + x; 59 | } 60 | 61 | if (y.indexOf('fe80') === 0) { 62 | y = ' ' + y; 63 | } 64 | 65 | return x.localeCompare(y); 66 | }); 67 | 68 | mainDebug('addresses6 %j', addresses6); 69 | 70 | return addresses6.concat(addresses4); 71 | } 72 | 73 | function Service(name) { 74 | events.EventEmitter.call(this); 75 | 76 | this.debug = debug(['dns-sd', name].join(':')); 77 | 78 | this.name = name; 79 | this.hostname = os.hostname(); 80 | 81 | this.mdnsName = '_' + name + '._tcp.local'; 82 | this.mdnsHostname = this.hostname + '.' + this.mdnsName; 83 | } 84 | 85 | util.inherits(Service, events.EventEmitter); 86 | 87 | Service.prototype.initialize = function (cb) { 88 | var self = this; 89 | 90 | function ready() { 91 | self.debug('multicast-dns ready'); 92 | } 93 | 94 | function warning(err) { 95 | self.debug('multicast-dns warning: %j', err); 96 | } 97 | 98 | this.mdns4 = multicastdns(); 99 | 100 | this.mdns4.on('warning', warning); 101 | this.mdns4.on('ready', ready); 102 | 103 | // TODO: allow overriding 104 | // TODO: different module for this? 105 | network.get_active_interface(function (err, activeInterface) { 106 | if (err) { 107 | self.debug('failed to find active network interface'); 108 | 109 | return cb(); 110 | } 111 | 112 | var activeAddresses = os.networkInterfaces()[activeInterface.name] 113 | .filter(function (address) { 114 | return address.family === 'IPv6'; 115 | }); 116 | 117 | if (!activeAddresses.length) { 118 | self.debug('failed to find IPv6 address for interface %s', 119 | activeInterface.name); 120 | 121 | return cb(); 122 | } 123 | 124 | self.debug('using IPv6 interface "%s" and address "%s"', 125 | activeAddresses[0].address, activeInterface.name); 126 | 127 | self.mdns6 = multicastdns({ 128 | type: 'udp6', 129 | interface: activeAddresses[0].address + '%' + activeInterface.name, 130 | ip: 'ff02::fb%' + activeInterface.name 131 | }); 132 | 133 | self.mdns6.on('warning', warning); 134 | self.mdns6.on('ready', ready); 135 | 136 | cb(); 137 | }); 138 | }; 139 | 140 | Service.prototype.browse = function () { 141 | var self = this; 142 | 143 | function onResponse(packet) { 144 | var answers = (packet.answers || []).concat(packet.additionals || []); 145 | 146 | var servicePointer = _.find(answers, function (answer) { 147 | return answer.type === 'PTR' && answer.name === self.mdnsName; 148 | }); 149 | 150 | if (!servicePointer) { 151 | return; 152 | } 153 | 154 | var serviceService = _.find(answers, function (answer) { 155 | return answer.type === 'SRV' && answer.name === servicePointer.data; 156 | }); 157 | 158 | if (!serviceService) { 159 | return; 160 | } 161 | 162 | var serviceText = _.find(answers, function (answer) { 163 | return answer.type === 'TXT' && answer.name === servicePointer.data; 164 | }); 165 | 166 | if (!serviceText) { 167 | return; 168 | } 169 | 170 | var text = serviceText.data.split(''); 171 | var attrs = []; 172 | 173 | while (text.length) { 174 | // XXX: not strictly conforming, doesn't handle quoted keys 175 | attrs.push(text.splice(0, text.splice(0, 1)[0].charCodeAt(0)) 176 | .join('') 177 | .split('=')); 178 | } 179 | 180 | var service = _.zipObject(attrs); 181 | 182 | service.port = serviceService.data.port; 183 | service.hostname = serviceService.data.target; 184 | 185 | if (serviceService.name === self.mdnsHostname && 186 | service.port === self.port) { 187 | self.debug('skipping my own service'); 188 | 189 | return; 190 | } 191 | 192 | service.addresses = _(answers).filter(function (answer) { 193 | return answer.type === 'A' || answer.type === 'AAAA'; 194 | }) 195 | .pluck('data') 196 | .valueOf(); 197 | 198 | self.debug('emitting service %j', service); 199 | 200 | self.emit('service', service); 201 | } 202 | 203 | this.mdns4.on('response', onResponse); 204 | this.mdns6.on('response', onResponse); 205 | 206 | function query() { 207 | self.mdns4.query(self.mdnsName, 'SRV'); 208 | self.mdns6.query(self.mdnsName, 'SRV'); 209 | } 210 | 211 | query(); 212 | 213 | setInterval(query, QUERY_INTERVAL); 214 | }; 215 | 216 | Service.prototype.advertise = function (port, data) { 217 | this.port = port; 218 | 219 | var self = this; 220 | 221 | function advertise() { 222 | self.debug('advertising response'); 223 | 224 | // TXT records store key/value pairs preceeded by a single byte 225 | // specifying their length 226 | var txt = _.map(data, function (value, key) { 227 | return key + '=' + value; 228 | }) 229 | .map(function (attr) { 230 | return String.fromCharCode(attr.length) + attr; 231 | }) 232 | .join(''); 233 | 234 | var hostname = self.hostname.replace(/\.local$/, '') + '.local'; 235 | 236 | var packet = { 237 | answers: [{ 238 | name: self.mdnsHostname, 239 | type: 'SRV', 240 | data: { 241 | target: hostname, 242 | port: port 243 | } 244 | }, { 245 | name: self.mdnsHostname, 246 | type: 'TXT', 247 | data: txt 248 | }, { 249 | name: self.mdnsName, 250 | type: 'PTR', 251 | data: self.mdnsHostname 252 | }, { 253 | name: '_services._dns-sd._udp.local', 254 | type: 'PTR', 255 | data: self.mdnsName 256 | }], 257 | additionals: getAddresses(hostname) 258 | }; 259 | 260 | function cb() { 261 | // console.log('XXX', err, result); 262 | } 263 | 264 | self.mdns4.respond(packet, cb); 265 | self.mdns6.respond(packet, cb); 266 | } 267 | 268 | function questionForMe(question) { 269 | return question.type === 'SRV' && question.name === self.mdnsName; 270 | } 271 | 272 | function onQuery(query, requestInfo) { 273 | if (!_.any(query.questions, questionForMe)) { 274 | return; 275 | } 276 | 277 | // TODO: ignore our own requests, but how? 278 | 279 | self.debug('request for SRV %s from %j', self.mdnsName, 280 | requestInfo); 281 | 282 | advertise(); 283 | } 284 | 285 | this.mdns4.on('query', onQuery); 286 | this.mdns6.on('query', onQuery); 287 | 288 | advertise(); 289 | 290 | setInterval(advertise, ADVERTISE_INTERVAL); 291 | 292 | this.debug('advertising %s:%d: %j', this.name, port, data); 293 | }; 294 | 295 | exports.Service = Service; 296 | -------------------------------------------------------------------------------- /mdns-swarm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var async = require('async'); 4 | var cuid = require('cuid'); 5 | var debug = require('debug'); 6 | var events = require('events'); 7 | var ip = require('ip-address'); 8 | var once = require('once'); 9 | var request = require('request'); 10 | var Service = require('./dns-sd.js').Service; 11 | var signalServer = require('./signal-server.js'); 12 | var SimplePeer = require('simple-peer'); 13 | var util = require('util'); 14 | // var _ = require('lodash'); 15 | 16 | function Swarm(identifier, channel, simplePeerOptions) { 17 | events.EventEmitter.call(this); 18 | 19 | this.debug = debug(['mdns-swarm', identifier, channel].join(':')); 20 | 21 | this.identifier = identifier; 22 | this.channel = channel; 23 | 24 | this.service = new Service(identifier); 25 | 26 | this.simplePeerOptions = simplePeerOptions || {}; 27 | 28 | this.peers = []; 29 | this._peers = {}; 30 | 31 | this.signalQueue = {}; 32 | this.connectivity = {}; 33 | 34 | // a fingerprint of this host/process 35 | this.id = cuid(); 36 | 37 | this.connectivityQueue = async.queue(function (task, cb) { 38 | task(cb); 39 | }, 1); 40 | 41 | var self = this; 42 | 43 | this.on('connectivity', function (host, baseUrl) { 44 | self.debug('connectivity for %s: %s', host, baseUrl); 45 | 46 | // TODO: use _.extend here if there are other options that can be passed 47 | var options = {wrtc: self.simplePeerOptions.wrtc}; 48 | 49 | // only connect to hosts with IDs that sort higher 50 | if (self.id < host) { 51 | options.initiator = true; 52 | } 53 | 54 | var peer = this._peers[host] = new SimplePeer(options); 55 | 56 | peer.on('signal', function (signal) { 57 | var url = baseUrl + 'signal/' + self.id; 58 | 59 | self.debug('→ POST: %s', url); 60 | 61 | request.post({ 62 | url: url, 63 | json: true, 64 | body: signal 65 | }, function (err, response) { 66 | if (err || response.statusCode !== 200) { 67 | console.error('→ POST err /signal/' + self.id + ': ' + err + ' ' + 68 | (response && response.statusCode)); 69 | } 70 | }); 71 | }); 72 | 73 | peer.on('connect', function () { 74 | self.debug('connected to host %s', host); 75 | 76 | self.peers.push(peer); 77 | 78 | self.emit('peer', peer, host); 79 | self.emit('connection', peer, host); 80 | }); 81 | 82 | var onClose = once(function () { 83 | self.debug('peer close %s', host); 84 | 85 | if (self._peers[host] === peer) { 86 | delete self._peers[host]; 87 | } 88 | 89 | var i = self.peers.indexOf(peer); 90 | 91 | if (i > -1) { 92 | self.peers.splice(i, 1); 93 | } 94 | }); 95 | 96 | peer.on('error', onClose); 97 | peer.once('close', onClose); 98 | 99 | if (self.signalQueue[host]) { 100 | self.debug('host %s has %d queued signals', host, 101 | self.signalQueue[host].length); 102 | 103 | var queuedSignal; 104 | 105 | while ((queuedSignal = self.signalQueue[host].shift())) { 106 | self.debug('applying queued signal for %s', host); 107 | 108 | peer.signal(queuedSignal); 109 | } 110 | } 111 | }); 112 | 113 | this.service.initialize(function () { 114 | self.advertise(); 115 | self.browse(); 116 | }); 117 | } 118 | 119 | util.inherits(Swarm, events.EventEmitter); 120 | 121 | Swarm.prototype.advertise = function () { 122 | var self = this; 123 | 124 | var app = signalServer.listen.call(this, function onListening(port) { 125 | self.service.advertise(port, { 126 | host: self.id, 127 | channel: self.channel 128 | }); 129 | }, function onSignal(host, signal) { 130 | if (!self._peers[host]) { 131 | if (!self.signalQueue[host]) { 132 | self.signalQueue[host] = []; 133 | } 134 | 135 | self.debug('queuing signal for host %s', host); 136 | 137 | self.signalQueue[host].push(signal); 138 | 139 | return; 140 | } 141 | 142 | self._peers[host].signal(signal); 143 | }); 144 | 145 | if (process.env.DEBUG) { 146 | require('./debug-routes.js').call(this, app); 147 | } 148 | }; 149 | 150 | Swarm.prototype.browse = function () { 151 | var self = this; 152 | 153 | this.service.on('service', function (service) { 154 | if (!service.host || !service.channel) { 155 | self.debug('could not find host and/or channel in %j', service); 156 | 157 | return; 158 | } 159 | 160 | if (service.host === self.id || service.channel !== self.channel) { 161 | return; 162 | } 163 | 164 | self.debug('service %s:%s, %s, %s, %j', 165 | service.host, 166 | service.channel, 167 | service.hostname, 168 | service.port, 169 | service.addresses); 170 | 171 | service.addresses.forEach(function (address) { 172 | self.connectivityQueue.push(function (cb) { 173 | if (self.connectivity[service.host]) { 174 | return cb(); 175 | } 176 | 177 | var v6 = new ip.v6.Address(address); 178 | 179 | var hostBase; 180 | 181 | if (v6.isValid()) { 182 | if (v6.getScope() !== 'Global') { 183 | self.debug('using hostname in favor of scoped IPv6 address'); 184 | 185 | hostBase = 'http://' + service.hostname + ':' + service.port + '/'; 186 | } else { 187 | hostBase = v6.href(service.port); 188 | } 189 | } else { 190 | hostBase = 'http://' + address + ':' + service.port + '/'; 191 | } 192 | 193 | var url = hostBase + 'connectivity'; 194 | 195 | self.debug('→ GET %s', url); 196 | 197 | request.get({url: url, json: true}, function (err, response, body) { 198 | if (err) { 199 | console.error('→ GET err /connectivity: ' + err); 200 | 201 | return cb(); 202 | } 203 | 204 | if (body.host !== service.host) { 205 | console.error('→ GET /connectivity host mismatch: ' + body.host + 206 | ' !== ' + service.host); 207 | 208 | return cb(); 209 | } 210 | 211 | self.connectivity[service.host] = hostBase; 212 | 213 | self.emit('connectivity', service.host, hostBase); 214 | 215 | cb(); 216 | }); 217 | }); 218 | }); 219 | }); 220 | 221 | this.service.browse(); 222 | }; 223 | 224 | module.exports = Swarm; 225 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mdns-swarm", 3 | "version": "5.1.0", 4 | "description": "Create a swarm of p2p connections using mDNS", 5 | "main": "mdns-swarm.js", 6 | "scripts": { 7 | "start": "cd scripts && ./three-peers.sh" 8 | }, 9 | "bin": { 10 | "simple-swarm": "./simple-swarm.js" 11 | }, 12 | "keywords": [ 13 | "mdns", 14 | "swarm", 15 | "p2p" 16 | ], 17 | "author": "Beau Gunderson ", 18 | "license": "MIT", 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/beaugunderson/mdns-swarm.git" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/beaugunderson/mdns-swarm/issues" 25 | }, 26 | "homepage": "https://github.com/beaugunderson/mdns-swarm#readme", 27 | "dependencies": { 28 | "async": "^1.2.1", 29 | "body-parser": "^1.12.4", 30 | "cuid": "^1.2.5", 31 | "debug": "^2.2.0", 32 | "express": "^4.12.4", 33 | "ip-address": "^4.0.0", 34 | "lodash": "^3.9.3", 35 | "multicast-dns": "^2.2.0", 36 | "network": "^0.1.3", 37 | "once": "^1.3.2", 38 | "request": "^2.57.0", 39 | "simple-peer": "^5.11.0" 40 | }, 41 | "devDependencies": {} 42 | } 43 | -------------------------------------------------------------------------------- /scripts/one-peer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # TODO: maybe extend $DEBUG cuz simple-peer* might be useful sometimes too 4 | 5 | DEBUG_COLORS=true DEBUG=mdns-swarm* node ../simple-swarm.js 6 | -------------------------------------------------------------------------------- /scripts/three-peers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ./one-peer.sh &> 1.log & 4 | PEER_1=$! 5 | 6 | ./one-peer.sh &> 2.log & 7 | PEER_2=$! 8 | 9 | ./one-peer.sh &> 3.log & 10 | PEER_3=$! 11 | 12 | echo "pids: $PEER_1 $PEER_2 $PEER_3" 13 | 14 | multitail -cT ANSI 1.log \ 15 | -cT ANSI 2.log \ 16 | -cT ANSI 3.log 17 | 18 | echo "killing..." 19 | 20 | # TODO: actually get the pids from the subshell so we can kill those 21 | pgrep -f "node ../simple-swarm.js" | xargs kill 22 | -------------------------------------------------------------------------------- /signal-server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var bodyParser = require('body-parser'); 4 | var express = require('express'); 5 | 6 | exports.listen = function (listeningCb, signalCb) { 7 | var app = express(); 8 | 9 | app.use(bodyParser.json()); 10 | 11 | var self = this; 12 | 13 | app.get('/connectivity', function (req, res) { 14 | self.debug('← GET /connectivity'); 15 | 16 | res.send({host: self.id}); 17 | }); 18 | 19 | app.post('/signal/:host', function (req, res) { 20 | var type = req.body.type; 21 | 22 | if (!type) { 23 | if (req.body.candidate) { 24 | type = 'candidate'; 25 | } else { 26 | type = 'unknown type'; 27 | } 28 | } 29 | 30 | self.debug('← POST /signal/%s %s', req.params.host, type); 31 | 32 | signalCb(req.params.host, req.body); 33 | 34 | res.send(); 35 | }); 36 | 37 | var server = app.listen(0, function () { 38 | listeningCb(server.address().port); 39 | }); 40 | 41 | return app; 42 | }; 43 | -------------------------------------------------------------------------------- /simple-swarm.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var Swarm = require('./mdns-swarm.js'); 6 | var wrtc = require('wrtc'); 7 | 8 | var swarm = new Swarm('simple-swarm', 'channel', {wrtc: wrtc}); 9 | 10 | swarm.on('peer', function (stream) { 11 | process.stdin.pipe(stream).pipe(process.stdout); 12 | }); 13 | --------------------------------------------------------------------------------