'use strict'; var PromiseA = require('bluebird'); var queryName = '_cloud._tcp.local'; var dnsSuite = require('dns-suite'); function createResponse(name, ownerIds, packet, ttl, mainPort) { var rpacket = { header: { id: packet.header.id , qr: 1 , opcode: 0 , aa: 1 , tc: 0 , rd: 0 , ra: 0 , res1: 0 , res2: 0 , res3: 0 , rcode: 0 , } , question: packet.question , answer: [] , authority: [] , additional: [] , edns_options: [] }; rpacket.answer.push({ name: queryName , typeName: 'PTR' , ttl: ttl , className: 'IN' , data: name + '.' + queryName }); var ifaces = require('./local-ip').find(); Object.keys(ifaces).forEach(function (iname) { var iface = ifaces[iname]; iface.ipv4.forEach(function (addr) { rpacket.additional.push({ name: name + '.local' , typeName: 'A' , ttl: ttl , className: 'IN' , address: addr.address }); }); iface.ipv6.forEach(function (addr) { rpacket.additional.push({ name: name + '.local' , typeName: 'AAAA' , ttl: ttl , className: 'IN' , address: addr.address }); }); }); rpacket.additional.push({ name: name + '.' + queryName , typeName: 'SRV' , ttl: ttl , className: 'IN' , priority: 1 , weight: 0 , port: mainPort , target: name + ".local" }); rpacket.additional.push({ name: name + '._device-info.' + queryName , typeName: 'TXT' , ttl: ttl , className: 'IN' , data: ["model=CloudHome1,1", "dappsvers=1"] }); ownerIds.forEach(function (id) { rpacket.additional.push({ name: name + '._owner-id.' + queryName , typeName: 'TXT' , ttl: ttl , className: 'IN' , data: [id] }); }); return dnsSuite.DNSPacket.write(rpacket); } module.exports.create = function (deps, config) { var socket; var nextBroadcast = -1; function handlePacket(message, rinfo) { // console.log('Received %d bytes from %s:%d', message.length, rinfo.address, rinfo.port); var packet; try { packet = dnsSuite.DNSPacket.parse(message); } catch (er) { // `dns-suite` actually errors on a lot of the packets floating around in our network, // so don't bother logging any errors. (We still use `dns-suite` because unlike `dns-js` // it can successfully craft the one packet we want to send.) return; } // Only respond to queries. if (packet.header.qr !== 0) { return; } // Only respond if they were asking for cloud devices. if (packet.question.length !== 1) { return; } if (packet.question[0].name !== queryName) { return; } if (packet.question[0].typeName !== 'PTR') { return; } if (packet.question[0].className !== 'IN' ) { return; } var proms = [ deps.storage.mdnsId.get() , deps.storage.owners.all().then(function (owners) { // The ID is the sha256 hash of the PPID, which shouldn't be reversible and therefore // should be safe to expose without needing authentication. return owners.map(function (owner) { return owner.id; }); }) ]; PromiseA.all(proms).then(function (results) { var resp = createResponse(results[0], results[1], packet, config.mdns.ttl, deps.tcp.mainPort); var now = Date.now(); if (now > nextBroadcast) { socket.send(resp, config.mdns.port, config.mdns.broadcast); nextBroadcast = now + config.mdns.ttl * 1000; } else { socket.send(resp, rinfo.port, rinfo.address); } }); } function start() { socket = require('dgram').createSocket({ type: 'udp4', reuseAddr: true }); socket.on('message', handlePacket); return new Promise(function (resolve, reject) { socket.once('error', reject); socket.bind(config.mdns.port, function () { var addr = this.address(); console.log('bound on UDP %s:%d for mDNS', addr.address, addr.port); socket.setBroadcast(true); socket.addMembership(config.mdns.broadcast); // This is supposed to be a local device discovery mechanism, so we shouldn't // need to hop through any gateways. This helps with security by making it // much more difficult for someone to use us as part of a DDoS attack by // spoofing the UDP address a request came from. socket.setTTL(1); socket.removeListener('error', reject); resolve(); }); }); } function stop() { return new Promise(function (resolve, reject) { socket.once('error', reject); socket.close(function () { socket.removeListener('error', reject); socket = null; resolve(); }); }); } function updateConf() { var promise; if (config.mdns.disabled) { if (socket) { promise = stop(); } } else { if (!socket) { promise = start(); } else if (socket.address().port !== config.mdns.port) { promise = stop().then(start); } else { // Can't check membership, so just add the current broadcast address to make sure // it's set. If it's already set it will throw an exception (at least on linux). try { socket.addMembership(config.mdns.broadcast); } catch (e) {} promise = Promise.resolve(); } } } updateConf(); return { updateConf }; };