added first implementation of DDNS

This commit is contained in:
tigerbot 2017-09-14 15:26:19 -06:00
parent 44d11e094b
commit bc301b94c9
6 changed files with 108 additions and 190 deletions

View File

@ -154,6 +154,9 @@ function fillConfig(config, args) {
if (!config.dns) { if (!config.dns) {
config.dns = { bind: [ 53 ], modules: [{ name: 'proxy', port: 3053 }] }; config.dns = { bind: [ 53 ], modules: [{ name: 'proxy', port: 3053 }] };
} }
if (!config.ddns) {
config.ddns = { enabled: false };
}
// Use Object.assign to add any properties needed but not defined in the mdns config. // Use Object.assign to add any properties needed but not defined in the mdns config.
// It will first copy the defaults into an empty object, then copy any real config over that. // It will first copy the defaults into an empty object, then copy any real config over that.
var mdnsDefaults = { port: 5353, broadcast: '224.0.0.251', ttl: 300 }; var mdnsDefaults = { port: 5353, broadcast: '224.0.0.251', ttl: 300 };

View File

@ -1,88 +1,116 @@
'use strict'; 'use strict';
module.exports.create = function (opts/*, servers*/) { module.exports.create = function (deps, conf) {
var PromiseA = opts.PromiseA; var PromiseA = deps.PromiseA;
var dns = PromiseA.promisifyAll(require('dns')); var request = PromiseA.promisify(require('request'));
var OAUTH3 = require('../packages/assets/org.oauth3');
require('../packages/assets/org.oauth3/oauth3.dns.js');
return PromiseA.all([ function dnsType(addr) {
dns.resolve4Async(opts._old_server_name).then(function (results) { if (/^\d+\.\d+\.\d+\.\d+$/.test(addr)) {
return results; return 'A';
}, function () {}) }
, dns.resolve6Async(opts._old_server_name).then(function (results) { if (-1 !== addr.indexOf(':') && /^[a-f:\.\d]+$/i.test(addr)) {
return results; return 'AAAA';
}, function () {}) }
]).then(function (results) { }
var ipv4 = results[0] || [];
var ipv6 = results[1] || [];
var record;
opts.dnsRecords = { function setDeviceAddress(addr) {
A: ipv4 return deps.storage.owners.all().then(function (sessions) {
, AAAA: ipv6 return sessions.filter(function (sess) {
}; return sess.token.scp.indexOf('dns') >= 0;
})[0];
}).then(function (session) {
if (!session) {
return PromiseA.reject(new Error('no sessions with DNS grants'));
}
Object.keys(opts.ifaces).some(function (ifacename) { return OAUTH3.discover(session.aud).then(function (directives) {
var iface = opts.ifaces[ifacename]; return request({
url: 'https://'+directives.api+'/api/org.oauth3.dns/acl/devices/' + conf.device.hostname
return iface.ipv4.some(function (localIp) { , method: 'POST'
return ipv4.some(function (remoteIp) { , headers: {
if (localIp.address === remoteIp) { 'Authorization': 'Bearer ' + session.refresh_token
record = localIp; , 'Accept': 'application/json; charset=utf-8'
return record;
} }
}); , json: {
}) || iface.ipv6.some(function (localIp) { addresses: [
return ipv6.forEach(function (remoteIp) { { value: addr, type: dnsType(addr) }
if (localIp.address === remoteIp) { ]
record = localIp;
return record;
} }
}); });
}); });
}); });
}
if (!record) { function getDeviceAddresses() {
console.info("DNS Record '" + ipv4.concat(ipv6).join(',') + "' does not match any local IP address."); return deps.storage.owners.all().then(function (sessions) {
console.info("Use --ddns to allow the people of the Internet to access your server."); return sessions.filter(function (sess) {
return sess.token.scp.indexOf('dns') >= 0;
})[0];
}).then(function (session) {
if (!session) {
return PromiseA.reject(new Error('no sessions with DNS grants'));
}
return OAUTH3.discover(session.aud).then(function (directives) {
return request({
url: 'https://'+directives.api+'/api/org.oauth3.dns/acl/devices'
, method: 'GET'
, headers: {
'Authorization': 'Bearer ' + session.refresh_token
, 'Accept': 'application/json; charset=utf-8'
}
, json: true
});
}).then(function (result) {
if (!result.body) {
return PromiseA.reject(new Error('No response body in request for device addresses'));
}
if (result.body.error) {
var err = new Error(result.body.error.message);
return PromiseA.reject(Object.assign(err, result.body.error));
}
return result.body.devices.filter(function (dev) {
return dev.name === conf.device.hostname;
})[0];
}).then(function (dev) {
return (dev || {}).addresses || [];
});
});
}
var publicAddress;
function recheckPubAddr() {
if (!conf.ddns.enabled) {
return;
} }
opts.externalIps.ipv4.some(function (localIp) { deps.storage.owners.all().then(function (sessions) {
return ipv4.some(function (remoteIp) { return sessions.filter(function (sess) {
if (localIp.address === remoteIp) { return sess.token.scp.indexOf('dns') >= 0;
record = localIp; })[0];
return record; }).then(function (session) {
if (!session) {
return;
}
OAUTH3.discover(session.aud).then(function (directives) {
return deps.loopback.checkPublicAddr(directives.api);
}).then(function (addr) {
if (publicAddress !== addr) {
publicAddress = addr;
setDeviceAddress(addr);
} }
}); });
}); });
}
opts.externalIps.ipv6.some(function (localIp) { setInterval(recheckPubAddr, 5*60*1000);
return ipv6.some(function (remoteIp) {
if (localIp.address === remoteIp) {
record = localIp;
return record;
}
});
});
if (!record) { return {
console.info("DNS Record '" + ipv4.concat(ipv6).join(',') + "' does not match any local IP address."); setDeviceAddress: setDeviceAddress
console.info("Use --ddns to allow the people of the Internet to access your server."); , getDeviceAddresses: getDeviceAddresses
} , recheckPubAddr: recheckPubAddr
});
};
if (require.main === module) {
var opts = {
_old_server_name: 'aj.daplie.me'
, PromiseA: require('bluebird')
}; };
// ifaces };
opts.ifaces = require('./local-ip.js').find();
console.log('opts.ifaces');
console.log(opts.ifaces);
require('./match-ips.js').match(opts._old_server_name, opts).then(function (ips) {
opts.matchingIps = ips.matchingIps || [];
opts.externalIps = ips.externalIps;
module.exports.create(opts);
});
}

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
module.exports.create = function (deps) { module.exports.create = function (deps, conf) {
var PromiseA = require('bluebird'); var PromiseA = require('bluebird');
var request = PromiseA.promisify(require('request')); var request = PromiseA.promisify(require('request'));
var pending = {}; var pending = {};
@ -49,7 +49,7 @@ module.exports.create = function (deps) {
// If the loopback requests don't go to us then there are all kinds of ways it could // If the loopback requests don't go to us then there are all kinds of ways it could
// error, but none of them really provide much extra information so we don't do // error, but none of them really provide much extra information so we don't do
// anything that will break the PromiseA.all out and mask the other results. // anything that will break the PromiseA.all out and mask the other results.
if (result.body.error) { if (conf.debug && result.body.error) {
console.log('error on remote side of port '+port+' loopback', result.body.error); console.log('error on remote side of port '+port+' loopback', result.body.error);
} }
return !!result.body.success; return !!result.body.success;
@ -68,7 +68,10 @@ module.exports.create = function (deps) {
return checkSinglePort(directives.api, address, port); return checkSinglePort(directives.api, address, port);
})) }))
.then(function (values) { .then(function (values) {
console.log(pending); if (conf.debug) {
console.log('remaining loopback tokens', pending);
}
var result = {error: null, address: address}; var result = {error: null, address: address};
ports.forEach(function (port, ind) { ports.forEach(function (port, ind) {
result[port] = values[ind]; result[port] = values[ind];
@ -79,6 +82,7 @@ module.exports.create = function (deps) {
}); });
} }
loopback.checkPublicAddr = checkPublicAddr;
loopback.server = require('http').createServer(function (req, res) { loopback.server = require('http').createServer(function (req, res) {
var parsed = require('url').parse(req.url); var parsed = require('url').parse(req.url);
var token = parsed.pathname.replace('/.well-known/cloud-challenge/', ''); var token = parsed.pathname.replace('/.well-known/cloud-challenge/', '');

View File

@ -1,117 +0,0 @@
'use strict';
var PromiseA = require('bluebird');
module.exports.match = function (servername, opts) {
return PromiseA.promisify(require('ipify'))().then(function (externalIp) {
var dns = PromiseA.promisifyAll(require('dns'));
opts.externalIps = [ { address: externalIp, family: 'IPv4' } ];
opts.ifaces = require('./local-ip.js').find({ externals: opts.externalIps });
opts.externalIfaces = Object.keys(opts.ifaces).reduce(function (all, iname) {
var iface = opts.ifaces[iname];
iface.ipv4.forEach(function (addr) {
if (addr.external) {
addr.iface = iname;
all.push(addr);
}
});
iface.ipv6.forEach(function (addr) {
if (addr.external) {
addr.iface = iname;
all.push(addr);
}
});
return all;
}, []).filter(Boolean);
function resolveIps(hostname) {
var allIps = [];
return PromiseA.all([
dns.resolve4Async(hostname).then(function (records) {
records.forEach(function (ip) {
allIps.push({
address: ip
, family: 'IPv4'
});
});
}, function () {})
, dns.resolve6Async(hostname).then(function (records) {
records.forEach(function (ip) {
allIps.push({
address: ip
, family: 'IPv6'
});
});
}, function () {})
]).then(function () {
return allIps;
});
}
function resolveIpsAndCnames(hostname) {
return PromiseA.all([
resolveIps(hostname)
, dns.resolveCnameAsync(hostname).then(function (records) {
return PromiseA.all(records.map(function (hostname) {
return resolveIps(hostname);
})).then(function (allIps) {
return allIps.reduce(function (all, ips) {
return all.concat(ips);
}, []);
});
}, function () {
return [];
})
]).then(function (ips) {
return ips.reduce(function (all, set) {
return all.concat(set);
}, []);
});
}
return resolveIpsAndCnames(servername).then(function (allIps) {
var matchingIps = [];
if (!allIps.length) {
console.warn("Could not resolve '" + servername + "'");
}
// { address, family }
allIps.some(function (ip) {
function match(addr) {
if (ip.address === addr.address) {
matchingIps.push(addr);
}
}
opts.externalIps.forEach(match);
// opts.externalIfaces.forEach(match);
Object.keys(opts.ifaces).forEach(function (iname) {
var iface = opts.ifaces[iname];
iface.ipv4.forEach(match);
iface.ipv6.forEach(match);
});
return matchingIps.length;
});
matchingIps.externalIps = {
ipv4: [
{ address: externalIp
, family: 'IPv4'
}
]
, ipv6: [
]
};
matchingIps.matchingIps = matchingIps;
return matchingIps;
});
});
};

View File

@ -31,6 +31,7 @@ function create(conf) {
deps.proxy = require('./proxy-conn').create(deps, conf); deps.proxy = require('./proxy-conn').create(deps, conf);
deps.socks5 = require('./socks5-server').create(deps, conf); deps.socks5 = require('./socks5-server').create(deps, conf);
deps.loopback = require('./loopback').create(deps, conf); deps.loopback = require('./loopback').create(deps, conf);
deps.ddns = require('./ddns').create(deps, conf);
require('./goldilocks.js').create(deps, conf); require('./goldilocks.js').create(deps, conf);
process.removeListener('message', create); process.removeListener('message', create);

View File

@ -48,7 +48,6 @@
"http-proxy": "^1.16.2", "http-proxy": "^1.16.2",
"human-readable-ids": "git+https://git.daplie.com/Daplie/human-readable-ids-js#master", "human-readable-ids": "git+https://git.daplie.com/Daplie/human-readable-ids-js#master",
"ipaddr.js": "git+https://github.com/whitequark/ipaddr.js.git#v1.3.0", "ipaddr.js": "git+https://github.com/whitequark/ipaddr.js.git#v1.3.0",
"ipify": "^1.1.0",
"js-yaml": "^3.8.3", "js-yaml": "^3.8.3",
"jsonwebtoken": "^7.4.0", "jsonwebtoken": "^7.4.0",
"le-challenge-ddns": "git+https://git.daplie.com/Daplie/le-challenge-ddns.git#master", "le-challenge-ddns": "git+https://git.daplie.com/Daplie/le-challenge-ddns.git#master",