removed tunnel from config and API and made DDNS responsible

This commit is contained in:
tigerbot 2017-09-28 11:18:44 -06:00
parent 5cc7e3f187
commit 0dd20e4dfc
7 changed files with 75 additions and 240 deletions

18
API.md
View File

@ -9,24 +9,6 @@ localhost.admin.daplie.me
All requests require an OAuth3 token in the request headers. All requests require an OAuth3 token in the request headers.
## Tunnel
### Check Status
* **URL** `/api/goldilocks@daplie.com/tunnel`
* **Method** `POST`
* **Reponse**: An object whose keys are the URLs for the tunnels, and whose
properties are arrays of the tunnel tokens.
This route with return only the sessions started by the same user who is
checking the status.
### Start Tunnel
* **URL** `/api/goldilocks@daplie.com/tunnel`
* **Method** `POST`
This route will use the stored token for the user matching the request
header to request a tunnel token from the audience of the stored token.
## Socks5 Proxy ## Socks5 Proxy
### Check Status ### Check Status

View File

@ -356,20 +356,6 @@ The tunnel client is meant to be run from behind a firewalls, carrier-grade NAT,
or otherwise inaccessible devices to allow them to be accessed publicly on the or otherwise inaccessible devices to allow them to be accessed publicly on the
internet. internet.
It has no options per se, but is rather a list of tokens that can be used to
connect to tunnel servers. If the token does not have an `aud` field it must be
provided in an object with the token provided in the `jwt` field and the tunnel
server url provided in the `tunnelUrl` field.
Example config:
```yml
tunnel:
- 'some.jwt_encoded.token'
- jwt: 'other.jwt_encoded.token'
tunnelUrl: 'wss://api.tunnel.example.com/'
```
### ddns ### ddns
TODO TODO

View File

@ -212,8 +212,6 @@ function fillConfig(config, args) {
config.addresses = addresses; config.addresses = addresses;
config.device = { hostname: require('os').hostname() }; config.device = { hostname: require('os').hostname() };
config.tunnel = args.tunnel || config.tunnel;
if (Array.isArray(config.tcp.bind)) { if (Array.isArray(config.tcp.bind)) {
return PromiseA.resolve(config); return PromiseA.resolve(config);
} }
@ -310,8 +308,7 @@ function readEnv(args) {
} catch (err) {} } catch (err) {}
var env = { var env = {
tunnel: process.env.GOLDILOCKS_TUNNEL_TOKEN || process.env.GOLDILOCKS_TUNNEL && true email: process.env.GOLDILOCKS_EMAIL
, email: process.env.GOLDILOCKS_EMAIL
, cwd: process.env.GOLDILOCKS_HOME || process.cwd() , cwd: process.env.GOLDILOCKS_HOME || process.cwd()
, debug: process.env.GOLDILOCKS_DEBUG && true , debug: process.env.GOLDILOCKS_DEBUG && true
}; };
@ -325,7 +322,6 @@ program
.version(require('../package.json').version) .version(require('../package.json').version)
.option('--agree-tos [url1,url2]', "agree to all Terms of Service for Daplie, Let's Encrypt, etc (or specific URLs only)") .option('--agree-tos [url1,url2]', "agree to all Terms of Service for Daplie, Let's Encrypt, etc (or specific URLs only)")
.option('-c --config <file>', 'Path to config file (Goldilocks.json or Goldilocks.yml) example: --config /etc/goldilocks/Goldilocks.json') .option('-c --config <file>', 'Path to config file (Goldilocks.json or Goldilocks.yml) example: --config /etc/goldilocks/Goldilocks.json')
.option('--tunnel [token]', 'Turn tunnel on. This will enter interactive mode for login if no token is specified.')
.option('--email <email>', "(Re)set default email to use for Daplie, Let's Encrypt, ACME, etc.") .option('--email <email>', "(Re)set default email to use for Daplie, Let's Encrypt, ACME, etc.")
.option('--debug', "Enable debug output") .option('--debug', "Enable debug output")
.parse(process.argv); .parse(process.argv);

View File

@ -9,11 +9,6 @@ tcp:
- 22 - 22
address: '127.0.0.1:8022' address: '127.0.0.1:8022'
# tunnel: jwt
# tunnel:
# - jwt1
# - jwt2
tunnel_server: tunnel_server:
secret: abc123 secret: abc123
servernames: servernames:
@ -91,3 +86,10 @@ mdns:
port: 5353 port: 5353
broadcast: '224.0.0.251' broadcast: '224.0.0.251'
ttl: 300 ttl: 300
ddns:
enabled: true
domains:
- www.example.com
- api.example.com
- test.example.com

View File

@ -5,7 +5,8 @@ module.exports.create = function (deps, conf) {
var loopback = require('./loopback').create(deps, conf); var loopback = require('./loopback').create(deps, conf);
var dnsCtrl = require('./dns-ctrl').create(deps, conf); var dnsCtrl = require('./dns-ctrl').create(deps, conf);
var localAddr, gateway, accessible; var localAddr, gateway;
var tunnelActive = false;
async function checkNetworkEnv() { async function checkNetworkEnv() {
// Since we can't detect the OS level events when a user plugs in an ethernet cable to recheck // Since we can't detect the OS level events when a user plugs in an ethernet cable to recheck
// what network environment we are in we check our local network address and the gateway to // what network environment we are in we check our local network address and the gateway to
@ -23,17 +24,25 @@ module.exports.create = function (deps, conf) {
return !loopResult.ports[port]; return !loopResult.ports[port];
}); });
// All ports come back to us, so we are either a public address or the router has already // if (notLooped.length) {
// been configured to forward these ports to us, so no configuration needs to be done we // // TODO: try to automatically configure router to forward ports to us.
// just have to make sure the DNS records stay in sync with our public address. // }
if (!notLooped.length) {
accessible = true;
return;
}
// TODO: try to automatically configure router to forward ports to us. // If we are on a public accress or all ports we are listening on are forwarded to us then
accessible = false; // we don't need the tunnel and we can set the DNS records for all our domains to our public
// TODO: move tunnel client here as fall back. // address. Otherwise we need to use the tunnel to accept traffic.
if (!notLooped.length) {
if (tunnelActive) {
deps.tunnelClients.disconnect();
tunnelActive = false;
}
} else {
if (!tunnelActive) {
var session = await getSession();
await deps.tunnelClients.start(session, conf.dns.domains);
tunnelActive = true;
}
}
} }
async function getSession() { async function getSession() {
@ -61,7 +70,7 @@ module.exports.create = function (deps, conf) {
} }
await checkNetworkEnv(); await checkNetworkEnv();
if (!accessible) { if (tunnelActive) {
return; return;
} }
var session = await getSession(); var session = await getSession();

View File

@ -1,109 +1,15 @@
'use strict'; 'use strict';
module.exports.create = function (deps, config) { module.exports.create = function (deps, config) {
var PromiseA = require('bluebird');
var fs = PromiseA.promisifyAll(require('fs'));
var stunnel = require('stunnel'); var stunnel = require('stunnel');
var activeTunnels = {}; var activeTunnels = {};
var path = require('path');
var tokensPath = path.join(__dirname, '..', 'var', 'tokens.json');
var storage = {
_read: function () {
var tokens;
try {
tokens = require(tokensPath);
} catch (err) {
tokens = {};
}
return tokens;
}
, _write: function (tokens) {
return fs.mkdirAsync(path.dirname(tokensPath)).catch(function (err) {
if (err.code !== 'EEXIST') {
console.error('failed to mkdir', path.dirname(tokensPath), err.toString());
}
}).then(function () {
return fs.writeFileAsync(tokensPath, JSON.stringify(tokens), 'utf8');
});
}
, _makeKey: function (token) {
// We use a stripped down version of the token contents so that if the token is
// re-issued the nonce and the iat and any other less important things are different
// we don't save essentially duplicate tokens multiple times.
var parsed = JSON.parse((new Buffer(token.split('.')[1], 'base64')).toString());
var stripped = {};
['aud', 'iss', 'domains'].forEach(function (key) {
if (parsed[key]) {
stripped[key] = parsed[key];
}
});
stripped.domains.sort();
var hash = require('crypto').createHash('sha256');
return hash.update(JSON.stringify(stripped)).digest('hex');
}
, all: function () {
var tokens = storage._read();
return PromiseA.resolve(Object.keys(tokens).map(function (key) {
return tokens[key];
}));
}
, save: function (token) {
return PromiseA.resolve().then(function () {
var curTokens = storage._read();
curTokens[storage._makeKey(token.jwt)] = token;
return storage._write(curTokens);
});
}
, del: function (token) {
return PromiseA.resolve().then(function () {
var curTokens = storage._read();
delete curTokens[storage._makeKey(token.jwt)];
return storage._write(curTokens);
});
}
};
function acquireToken(session) {
var OAUTH3 = deps.OAUTH3;
// session seems to be changed by the API call for some reason, so save the
// owner before that happens.
var owner = session.id;
// The OAUTH3 library stores some things on the root session object that we usually
// just leave inside the token, but we need to pull those out before we use it here
session.provider_uri = session.provider_uri || session.token.provider_uri || session.token.iss;
session.client_uri = session.client_uri || session.token.azp;
session.scope = session.scope || session.token.scp;
console.log('asking for tunnel token from', session.token.aud);
return OAUTH3.discover(session.token.aud).then(function (directives) {
var opts = {
api: 'tunnel.token'
, session: session
, data: {
// filter to all domains that are on this device
//domains: Object.keys(domainsMap)
device: {
hostname: config.device.hostname
, id: config.device.uid || config.device.id
}
}
};
return OAUTH3.api(directives.api, opts).then(function (result) {
console.log('got a token from the tunnel server?');
result.owner = owner;
return result;
});
});
}
function addToken(data) { function addToken(data) {
if (typeof data === 'string') {
data = { jwt: data };
}
if (!data.jwt) { if (!data.jwt) {
return PromiseA.reject(new Error("missing 'jwt' from tunnel data")); return deps.PromiseA.reject(new Error("missing 'jwt' from tunnel data"));
} }
if (!data.tunnelUrl) { if (!data.tunnelUrl) {
var decoded; var decoded;
@ -111,12 +17,12 @@ module.exports.create = function (deps, config) {
decoded = JSON.parse(new Buffer(data.jwt.split('.')[1], 'base64').toString('ascii')); decoded = JSON.parse(new Buffer(data.jwt.split('.')[1], 'base64').toString('ascii'));
} catch (err) { } catch (err) {
console.warn('invalid web token given to tunnel manager', err); console.warn('invalid web token given to tunnel manager', err);
return PromiseA.reject(err); return deps.PromiseA.reject(err);
} }
if (!decoded.aud) { if (!decoded.aud) {
console.warn('tunnel manager given token with no tunnelUrl or audience'); console.warn('tunnel manager given token with no tunnelUrl or audience');
var err = new Error('missing tunnelUrl and audience'); var err = new Error('missing tunnelUrl and audience');
return PromiseA.reject(err); return deps.PromiseA.reject(err);
} }
data.tunnelUrl = 'wss://' + decoded.aud + '/'; data.tunnelUrl = 'wss://' + decoded.aud + '/';
} }
@ -146,19 +52,49 @@ module.exports.create = function (deps, config) {
return activeTunnels[data.tunnelUrl].append(data.jwt); return activeTunnels[data.tunnelUrl].append(data.jwt);
} }
function acquireToken(session, domains) {
var OAUTH3 = deps.OAUTH3;
// The OAUTH3 library stores some things on the root session object that we usually
// just leave inside the token, but we need to pull those out before we use it here
session.provider_uri = session.provider_uri || session.token.provider_uri || session.token.iss;
session.client_uri = session.client_uri || session.token.azp;
session.scope = session.scope || session.token.scp;
console.log('asking for tunnel token from', session.token.aud);
return OAUTH3.discover(session.token.aud).then(function (directives) {
var opts = {
api: 'tunnel.token'
, session: session
, data: {
domains: domains
, device: {
hostname: config.device.hostname
, id: config.device.uid || config.device.id
}
}
};
return OAUTH3.api(directives.api, opts).then(addToken);
});
}
function removeToken(data) { function removeToken(data) {
if (typeof data === 'string') {
data = { jwt: data };
}
if (!data.tunnelUrl) { if (!data.tunnelUrl) {
var decoded; var decoded;
try { try {
decoded = JSON.parse(new Buffer(data.jwt.split('.')[1], 'base64').toString('ascii')); decoded = JSON.parse(new Buffer(data.jwt.split('.')[1], 'base64').toString('ascii'));
} catch (err) { } catch (err) {
console.warn('invalid web token given to tunnel manager', err); console.warn('invalid web token given to tunnel manager', err);
return PromiseA.reject(err); return deps.PromiseA.reject(err);
} }
if (!decoded.aud) { if (!decoded.aud) {
console.warn('tunnel manager given token with no tunnelUrl or audience'); console.warn('tunnel manager given token with no tunnelUrl or audience');
var err = new Error('missing tunnelUrl and audience'); var err = new Error('missing tunnelUrl and audience');
return PromiseA.reject(err); return deps.PromiseA.reject(err);
} }
data.tunnelUrl = 'wss://' + decoded.aud + '/'; data.tunnelUrl = 'wss://' + decoded.aud + '/';
} }
@ -166,72 +102,23 @@ module.exports.create = function (deps, config) {
// Not sure if we actually want to return an error that the token didn't even belong to a // Not sure if we actually want to return an error that the token didn't even belong to a
// server that existed, but since it never existed we can consider it as "removed". // server that existed, but since it never existed we can consider it as "removed".
if (!activeTunnels[data.tunnelUrl]) { if (!activeTunnels[data.tunnelUrl]) {
return PromiseA.resolve(); return deps.PromiseA.resolve();
} }
console.log('removing token from tunnel at', data.tunnelUrl); console.log('removing token from tunnel at', data.tunnelUrl);
return activeTunnels[data.tunnelUrl].clear(data.jwt); return activeTunnels[data.tunnelUrl].clear(data.jwt);
} }
if (config.tunnel) { function disconnectAll() {
var confTokens = config.tunnel; Object.keys(activeTunnels).forEach(function (key) {
if (typeof confTokens === 'string') { activeTunnels[key].end();
confTokens = confTokens.split(',');
}
confTokens.forEach(function (jwt) {
if (typeof jwt === 'object') {
jwt.owner = 'config';
addToken(jwt);
} else {
addToken({ jwt: jwt, owner: 'config' });
}
}); });
} }
storage.all().then(function (stored) {
stored.forEach(function (result) {
addToken(result);
});
});
return { return {
start: function (session) { start: acquireToken
return acquireToken(session).then(function (token) { , startDirect: addToken
return addToken(token).then(function () { , remove: removeToken
return storage.save(token); , disconnect: disconnectAll
});
});
}
, add: function (data) {
return addToken(data).then(function () {
return storage.save(data);
});
}
, remove: function (data) {
return storage.del(data.jwt).then(function () {
return removeToken(data);
});
}
, get: function (owner) {
return storage.all().then(function (tokens) {
var result = {};
tokens.forEach(function (data) {
if (!result[data.owner]) {
result[data.owner] = {};
}
if (!result[data.owner][data.tunnelUrl]) {
result[data.owner][data.tunnelUrl] = [];
}
data.decoded = JSON.parse(new Buffer(data.jwt.split('.')[0], 'base64'));
result[data.owner][data.tunnelUrl].push(data);
});
if (owner) {
return result[owner] || {};
}
return result;
});
}
}; };
}; };

View File

@ -235,33 +235,6 @@ module.exports.create = function (deps, conf) {
}); });
} }
, tunnel: function (req, res) {
if (handleCors(req, res)) {
return;
}
isAuthorized(req, res, function () {
if ('POST' !== req.method) {
res.setHeader('Content-Type', 'application/json');
return deps.tunnelClients.get(req.userId).then(function (result) {
res.end(JSON.stringify(result));
}, function (err) {
res.statusCode = 500;
res.end(JSON.stringify({ error: { message: err.message, code: err.code, uri: err.uri } }));
});
}
return deps.storage.owners.get(req.userId).then(function (session) {
return deps.tunnelClients.start(session).then(function () {
res.setHeader('Content-Type', 'application/json;');
res.end(JSON.stringify({ success: true }));
}, function (err) {
res.setHeader('Content-Type', 'application/json;');
res.statusCode = 500;
res.end(JSON.stringify({ error: { message: err.message, code: err.code, uri: err.uri } }));
});
});
});
}
, config: function (req, res) { , config: function (req, res) {
if (handleCors(req, res)) { if (handleCors(req, res)) {
return; return;