From 1ba0557c2ce1e1b1374826cacdd2acdc738875ea Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 23 May 2018 11:12:39 +0000 Subject: [PATCH] turning into a service, first steps --- LICENSE | 52 +++-- README.md | 153 ++++++++----- admin/index.html | 9 + admin/setup/index.html | 16 ++ bin/install-stunneld-js.sh | 13 -- bin/stunneld.js | 194 ---------------- bin/telebitd.js | 280 +++++++++++++++++++++++ dist/etc/systemd/system/stunneld.service | 23 -- dist/etc/systemd/system/telebitd.service | 66 ++++++ examples/telebitd.yml | 3 + handlers.js | 94 ++++++-- installer/get.sh | 145 ++++++++++++ lib/unwrap-tls.js | 15 +- package.json | 25 +- wstunneld.js => telebitd.js | 0 15 files changed, 747 insertions(+), 341 deletions(-) create mode 100644 admin/index.html create mode 100644 admin/setup/index.html delete mode 100644 bin/install-stunneld-js.sh delete mode 100755 bin/stunneld.js create mode 100755 bin/telebitd.js delete mode 100644 dist/etc/systemd/system/stunneld.service create mode 100644 dist/etc/systemd/system/telebitd.service create mode 100644 examples/telebitd.yml create mode 100644 installer/get.sh rename wstunneld.js => telebitd.js (100%) diff --git a/LICENSE b/LICENSE index a7782bc..433a93e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,41 @@ +Copyright 2016 AJ ONeal + +This is open source software; you can redistribute it and/or modify it under the +terms of either: + + a) the "MIT License" + b) the "Apache-2.0 License" + MIT License -Copyright (c) 2016 Daplie, Inc + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Apache-2.0 License Summary + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index a19aa3b..09ef81f 100644 --- a/README.md +++ b/README.md @@ -1,85 +1,124 @@ -# Telebit™ Relay +# Telebit Relay -A server that works in combination with [stunnel.js](https://git.coolaj86.com/coolaj86/tunnel-client.js) +Friends don't let friends localhost™ + +A server that works in combination with [Telebit Remote](https://git.coolaj86.com/coolaj86/telebit.js) to allow you to serve http and https from any computer, anywhere through a secure tunnel. -| Sponsored by [ppl](https://ppl.family) | **Telebit Relay** | [Telebit](https://git.coolaj86.com/coolaj86/tunnel-client.js) | +| Sponsored by [ppl](https://ppl.family) | **Telebit Relay** | [Telebit Remote](https://git.coolaj86.com/coolaj86/telebit.js) | -CLI +Features +======== + +* [x] Expose your bits even in the harshest of network environments + * [x] NAT, Home Routers + * [x] College Dorms, HOAs + * [x] Corporate Firewalls, Public libraries, Airports + * [x] and even Airplanes, yep +* [x] Automated HTTPS (Free SSL) + +Install +======= + +Mac & Linux +----------- + +Open Terminal and run this install script: + +```bash +curl -fsS https://get.telebit.cloud/ | bash +``` + +This will install Telebit Relay to `/opt/telebitd` and +put a symlink to `/opt/telebitd/bin/telebitd` in `/usr/local/bin/telebitd` +for convenience. + +You can customize the installation: + +```bash +export NODEJS_VER=v8.11.1 +export TELEBITD_PATH=/opt/telebitd +curl -fsS https://get.telebit.cloud/ | bash +``` + +This will change which version of node.js is bundled with Telebit Relay +and the path to which Telebit Relay installs. + +Windows & Node.js +----------------- + +1. Install [node.js](https://nodejs.org) +2. Open _Node.js_ +2. Run the command `npm install -g telebitd` + +**Note**: Use node.js v8.x or v10.x + +There is [a bug](https://github.com/nodejs/node/issues/20241) in node v9.x that causes telebitd to crash. + +Service Install === -Installs as `stunnel.js` with the alias `jstunnel` -(for those that regularly use `stunnel` but still like commandline completion). +TODO automate this: -### Install +`./dist/etc/systemd/system/telebitd.service` should be copied to `/etc/systemd/system/telebitd.service`. -```bash -npm install -g stunneld -``` +The user and group `telebit` should be created. -Then `dist/etc/systemd/system/stunneld.service` should be copied to `/etc/systemd/system/stunneld.service` and -the ARGUMENTS, such as SECRET, MUST BE CHANGED. - -*TODO*: make `--config /path/to/config` the only argument (and have the secret auto-generated on first run?) - -## Note: Use node.js v8.x - -There is a bug in node v9.x that causes stunneld to crash. - -https://github.com/nodejs/node/issues/20241 - -### Advanced Usage - -How to use `stunnel.js` with your own instance of `stunneld.js`: - -```bash -stunneld.js --servernames tunnel.example.com --protocols wss --secret abc123 -``` - -Options - -``` ---secret the same secret used by stunnel client (used for authentication) ---serve comma separated list of :: to which - incoming http and https should be forwarded -``` - -### Privileged Ports without sudo +**Privileged Ports without sudo**: ```bash # Linux sudo setcap 'cap_net_bind_service=+ep' $(which node) ``` -### Alterntive Methods +Usage +==== -**NOT YET IMPLEMENTED** +```bash +telebitd --config /etc/telebit/telebitd.yml +``` + +Options + +`/etc/telebit/telebitd.yml:` +``` +servernames: + - telebit.example.com + - telebit.example.net +email: 'jon@example.com' +agree_tos: true +community_member: true +secret: 'xxxyyyzzzaaabbbccc' +``` + +Security +======== + +The bottom line: As with everything in life, there is no such thing as anonymity +or absolute security. Only use Telebit Relays that you trust or self-host. :D + +Even though the traffic is encrypted end-to-end, you can't just trust any Telebit Relay +willy-nilly. + +A man-in-the-middle attack is possible using Let's Encrypt since an evil Telebit Relay +would be able to complete the http-01 and tls-sni-01 challenges without a problem +(since that's where your DNS is pointed when you use the service). + +Also, the traffic could still be copied and stored for decryption is some era when quantum +computers exist (probably never). + +Why? +==== We created this for anyone to use on their own server or VPS, but those generally cost $5 - $20 / month and so it's probably cheaper to purchase data transfer (which we supply, obviously), which is only $1/month for most people. -Just use the client ([stunnel.js](https://git.coolaj86.com/coolaj86/tunnel-client.js)) -with this tunneling service (the default) and save yourself the monthly fee -by only paying for the data you need. +TODO show how to do on * Node WS Tunnel (zero setup) * Heroku (zero cost) * Chunk Host (best deal per TB/month) -Security -======== -The bottom line: As with everything in life, there is no such thing as anonymity -or absolute security. Only use stunneld services that you trust. :D - -Even though the traffic is encrypted end-to-end, you can't just trust any stunneld service -willy-nilly. - -A man-in-the-middle attack is possible using Let's Encrypt since an evil stunneld service -would be able to complete the http-01 and tls-sni-01 challenges without a problem -(since that's where your DNS is pointed when you use the service). - -Also, the traffic could still be copied and stored for decryption is some era when quantum -computers exist (probably never). diff --git a/admin/index.html b/admin/index.html new file mode 100644 index 0000000..2708d6a --- /dev/null +++ b/admin/index.html @@ -0,0 +1,9 @@ + + + + Telebit Relay + + + [TODO: Admin Interface] + + diff --git a/admin/setup/index.html b/admin/setup/index.html new file mode 100644 index 0000000..30084ea --- /dev/null +++ b/admin/setup/index.html @@ -0,0 +1,16 @@ + + + + Telebit Relay + + + [TODO: Setup Interface] +
+
    +
  • Admin Server Name
  • +
  • Administrator Email
  • +
  • SSL ToS Agree
  • +
  • Community Member
  • +
+ + diff --git a/bin/install-stunneld-js.sh b/bin/install-stunneld-js.sh deleted file mode 100644 index 38620ce..0000000 --- a/bin/install-stunneld-js.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -rm -rf ./node-installer.sh -curl -fsSL bit.ly/node-installer -o ./node-installer.sh -bash ./node-installer.sh --dev-deps - -git clone https://git.coolaj86.com/coolaj86/tunnel-server.js.git -pushd tunnel-server.js/ - npm install - my_secret=$(node bin/generate-secret.js) - echo "Your secret is:\n\n\t"$my_secret - echo "node bin/server.js --servernames tunnel.example.com --secret $my_secret" -popd diff --git a/bin/stunneld.js b/bin/stunneld.js deleted file mode 100755 index b3951c2..0000000 --- a/bin/stunneld.js +++ /dev/null @@ -1,194 +0,0 @@ -#!/usr/bin/env node -(function () { -'use strict'; - -var pkg = require('../package.json'); - -var program = require('commander'); -var stunneld = require('../wstunneld.js'); -var greenlock = require('greenlock'); - -function collectServernames(val, memo) { - var lowerCase = val.split(/,/).map(function (servername) { - return servername.toLowerCase(); - }); - - return memo.concat(lowerCase); -} - -function collectProxies(val, memo) { - var vals = val.split(/,/g); - vals.map(function (location) { - // http:john.example.com:3000 - // http://john.example.com:3000 - var parts = location.split(':'); - if (1 === parts.length) { - parts[1] = parts[0]; - parts[0] = 'wss'; - } - if (2 === parts.length) { - if (/\./.test(parts[0])) { - parts[2] = parts[1]; - parts[1] = parts[0]; - parts[0] = 'wss'; - } - if (!/\./.test(parts[1])) { - throw new Error("bad --serve option Example: wss://tunnel.example.com:1337"); - } - } - parts[0] = parts[0].toLowerCase(); - parts[1] = parts[1].toLowerCase().replace(/(\/\/)?/, '') || '*'; - parts[2] = parseInt(parts[2], 10) || 0; - if (!parts[2]) { - // TODO grab OS list of standard ports? - if (-1 !== [ 'ws', 'http' ].indexOf(parts[0])) { - //parts[2] = 80; - } - else if (-1 !== [ 'wss', 'https' ].indexOf(parts[0])) { - //parts[2] = 443; - } - else { - throw new Error("port must be specified - ex: tls:*:1337"); - } - } - - return { - protocol: parts[0] - , hostname: parts[1] - , port: parts[2] - }; - }).forEach(function (val) { - memo.push(val); - }); - - return memo; -} - -function collectPorts(val, memo) { - return memo.concat(val.split(/,/g).map(Number).filter(Boolean)); -} - -program - .version(pkg.version) - .option('--agree-tos', "Accept the Daplie and Let's Encrypt Terms of Service") - .option('--email ', "Email to use for Daplie and Let's Encrypt accounts") - .option('--serve ', 'comma separated list of :: to which matching incoming http and https should forward (reverse proxy). Ex: https://john.example.com,tls:*:1337', collectProxies, [ ]) - .option('--ports ', 'comma separated list of ports on which to listen. Ex: 80,443,1337', collectPorts, [ ]) - .option('--servernames ', 'comma separated list of servernames to use for the admin interface. Ex: tunnel.example.com,tunnel.example.net', collectServernames, [ ]) - .option('--secret ', 'the same secret used by stunneld (used for JWT authentication)') - .parse(process.argv) - ; - -var portsMap = {}; -var servernamesMap = {}; -program.serve.forEach(function (proxy) { - servernamesMap[proxy.hostname] = true; - if (proxy.port) { - portsMap[proxy.port] = true; - } -}); -program.servernames.forEach(function (name) { - servernamesMap[name] = true; -}); -program.ports.forEach(function (port) { - portsMap[port] = true; -}); - -program.servernames = Object.keys(servernamesMap); -if (!program.servernames.length) { - throw new Error('You must give this server at least one servername for its admin interface. Example:\n\n\t--servernames tunnel.example.com,tunnel.example.net'); -} - -program.ports = Object.keys(portsMap); -if (!program.ports.length) { - program.ports = [ 80, 443 ]; -} - -if (!program.secret) { - // TODO randomly generate and store in file? - console.warn("[SECURITY] you must provide --secret '" + require('crypto').randomBytes(16).toString('hex') + "'"); - process.exit(1); - return; -} - -// TODO letsencrypt -program.tlsOptions = require('localhost.daplie.me-certificates').merge({}); - -function approveDomains(opts, certs, cb) { - // This is where you check your database and associated - // email addresses with domains and agreements and such - - // The domains being approved for the first time are listed in opts.domains - // Certs being renewed are listed in certs.altnames - if (certs) { - opts.domains = certs.altnames; - } - else { - if (-1 !== program.servernames.indexOf(opts.domain)) { - opts.email = program.email; - opts.agreeTos = program.agreeTos; - } - } - - // NOTE: you can also change other options such as `challengeType` and `challenge` - // opts.challengeType = 'http-01'; - // opts.challenge = require('le-challenge-fs').create({}); - - cb(null, { options: opts, certs: certs }); -} - -if (!program.email || !program.agreeTos) { - console.error("You didn't specify --email and --agree-tos"); - console.error("(required for ACME / Let's Encrypt / Greenlock TLS/SSL certs)"); - console.error(""); -} -else { - program.greenlock = greenlock.create({ - - version: 'draft-11' - , server: 'https://acme-v02.api.letsencrypt.org/directory' - - , challenges: { - // TODO dns-01 - 'http-01': require('le-challenge-fs').create({ webrootPath: '/tmp/acme-challenges' }) - } - - , store: require('le-store-certbot').create({ webrootPath: '/tmp/acme-challenges' }) - - , email: program.email - - , agreeTos: program.agreeTos - - , approveDomains: approveDomains - - //, approvedDomains: program.servernames - - }); -} -//program.tlsOptions.SNICallback = program.greenlock.httpsOptions.SNICallback; -/* -program.middleware = program.greenlock.middleware(function (req, res) { - res.end('Hello, World!'); -}); -*/ - -require('../handlers').create(program); // adds directly to program for now... - -//require('cluster-store').create().then(function (store) { - //program.store = store; - - var net = require('net'); - var netConnHandlers = stunneld.create(program); // { tcp, ws } - var WebSocketServer = require('ws').Server; - var wss = new WebSocketServer({ server: (program.httpTunnelServer || program.httpServer) }); - wss.on('connection', netConnHandlers.ws); - program.ports.forEach(function (port) { - var tcp3000 = net.createServer(); - tcp3000.listen(port, function () { - console.log('listening on ' + port); - }); - tcp3000.on('connection', netConnHandlers.tcp); - }); -//}); - -}()); diff --git a/bin/telebitd.js b/bin/telebitd.js new file mode 100755 index 0000000..b3838e5 --- /dev/null +++ b/bin/telebitd.js @@ -0,0 +1,280 @@ +#!/usr/bin/env node +(function () { +'use strict'; + +var pkg = require('../package.json'); + +var argv = process.argv.slice(2); +var telebitd = require('../telebitd.js'); +var greenlock = require('greenlock'); + +var confIndex = argv.indexOf('--config'); +var confpath; +if (-1 === confIndex) { + confIndex = argv.indexOf('-c'); +} +confpath = argv[confIndex + 1]; + +function help() { + console.info(''); + console.info('Usage:'); + console.info(''); + console.info('\ttelebitd --config '); + console.info(''); + console.info('Example:'); + console.info(''); + console.info('\ttelebitd --config /etc/telebit/telebitd.yml'); + console.info(''); + console.info('Config:'); + console.info(''); + console.info('\tSee https://git.coolaj86.com/coolaj86/telebitd.js'); + console.info(''); + console.info(''); + process.exit(0); +} + +if (-1 === confIndex || -1 !== argv.indexOf('-h') || -1 !== argv.indexOf('--help')) { + help(); +} +if (!confpath || /^--/.test(confpath)) { + help(); +} + +function applyConfig(config) { + var state = { ports: [ 80, 443 ], tcp: {} }; + state.tlsOptions = {}; // TODO just close the sockets that would use this early? or use the admin servername + state.config = config; + state.servernames = config.servernames || []; + state.secret = state.config.secret; + if (state.secret) { + state.secret = require('crypto').randomBytes(16).toString('hex'); + console.info(""); + console.info("Secret for this session:"); + console.info(""); + console.info("\t" + state.secret); + console.info(""); + console.info(""); + } + require('../handlers').create(state); // adds directly to config for now... + + //require('cluster-store').create().then(function (store) { + //program.store = store; + + var net = require('net'); + var netConnHandlers = telebitd.create(state); // { tcp, ws } + var WebSocketServer = require('ws').Server; + var wss = new WebSocketServer({ server: (state.httpTunnelServer || state.httpServer) }); + wss.on('connection', netConnHandlers.ws); + state.ports.forEach(function (port) { + if (state.tcp[port]) { + console.error("skipping previously added port " + port); + return; + } + state.tcp[port] = net.createServer(); + state.tcp[port].listen(port, function () { + console.log('listening plain TCP on ' + port); + }); + state.tcp[port].on('connection', netConnHandlers.tcp); + }); + //}); + + function approveDomains(opts, certs, cb) { + console.log('Approve Domains', opts.domains); + // This is where you check your database and associated + // email addresses with domains and agreements and such + + // The domains being approved for the first time are listed in opts.domains + // Certs being renewed are listed in certs.altnames + if (certs) { + opts.domains = certs.altnames; + } else { + if (-1 !== state.servernames.indexOf(opts.domain) || -1 !== (state._servernames||[]).indexOf(opts.domain)) { + opts.email = state.config.email; + opts.agreeTos = state.config.agreeTos; + opts.challenges = { + // TODO dns-01 + 'http-01': require('le-challenge-fs').create({ webrootPath: '/tmp/acme-challenges' }) + }; + opts.communityMember = state.config.communityMember; + } + } + + // NOTE: you can also change other options such as `challengeType` and `challenge` + // opts.challengeType = 'http-01'; + // opts.challenge = require('le-challenge-fs').create({}); + + console.log('Approve Domains cb'); + cb(null, { options: opts, certs: certs }); + } + +/* + if (!config.email || !config.agreeTos) { + console.error("You didn't specify --email and --agree-tos"); + console.error("(required for ACME / Let's Encrypt / Greenlock TLS/SSL certs)"); + console.error(""); + process.exit(1); + } +*/ + + state.greenlock = greenlock.create({ + + version: 'draft-11' + , server: 'https://acme-staging-v02.api.letsencrypt.org/directory' + + , store: require('le-store-certbot').create({ debug: true, webrootPath: '/tmp/acme-challenges' }) + + , approveDomains: approveDomains + + , configDir: '/root/acme' + , debug: true + + //, approvedDomains: program.servernames + + }); +} + +require('fs').readFile(confpath, 'utf8', function (err, text) { + var config; + + var recase = require('recase').create({}); + var camelCopy = recase.camelCopy.bind(recase); + + if (err) { + console.error("\nCouldn't load config:\n\n\t" + err.message + "\n"); + process.exit(1); + return; + } + + try { + config = JSON.parse(text); + } catch(e1) { + try { + config = require('js-yaml').safeLoad(text); + } catch(e2) { + console.error(e1.message); + console.error(e2.message); + process.exit(1); + return; + } + } + + applyConfig(camelCopy(config)); +}); + + + +function adjustArgs() { + function collectServernames(val, memo) { + var lowerCase = val.split(/,/).map(function (servername) { + return servername.toLowerCase(); + }); + + return memo.concat(lowerCase); + } + + function collectProxies(val, memo) { + var vals = val.split(/,/g); + vals.map(function (location) { + // http:john.example.com:3000 + // http://john.example.com:3000 + var parts = location.split(':'); + if (1 === parts.length) { + parts[1] = parts[0]; + parts[0] = 'wss'; + } + if (2 === parts.length) { + if (/\./.test(parts[0])) { + parts[2] = parts[1]; + parts[1] = parts[0]; + parts[0] = 'wss'; + } + if (!/\./.test(parts[1])) { + throw new Error("bad --serve option Example: wss://tunnel.example.com:1337"); + } + } + parts[0] = parts[0].toLowerCase(); + parts[1] = parts[1].toLowerCase().replace(/(\/\/)?/, '') || '*'; + parts[2] = parseInt(parts[2], 10) || 0; + if (!parts[2]) { + // TODO grab OS list of standard ports? + if (-1 !== [ 'ws', 'http' ].indexOf(parts[0])) { + //parts[2] = 80; + } + else if (-1 !== [ 'wss', 'https' ].indexOf(parts[0])) { + //parts[2] = 443; + } + else { + throw new Error("port must be specified - ex: tls:*:1337"); + } + } + + return { + protocol: parts[0] + , hostname: parts[1] + , port: parts[2] + }; + }).forEach(function (val) { + memo.push(val); + }); + + return memo; + } + + function collectPorts(val, memo) { + return memo.concat(val.split(/,/g).map(Number).filter(Boolean)); + } + + program + .version(pkg.version) + .option('--agree-tos', "Accept the Daplie and Let's Encrypt Terms of Service") + .option('--email ', "Email to use for Daplie and Let's Encrypt accounts") + .option('--serve ', 'comma separated list of :: to which matching incoming http and https should forward (reverse proxy). Ex: https://john.example.com,tls:*:1337', collectProxies, [ ]) + .option('--ports ', 'comma separated list of ports on which to listen. Ex: 80,443,1337', collectPorts, [ ]) + .option('--servernames ', 'comma separated list of servernames to use for the admin interface. Ex: tunnel.example.com,tunnel.example.net', collectServernames, [ ]) + .option('--secret ', 'the same secret used by telebitd (used for JWT authentication)') + .parse(process.argv) + ; + + var portsMap = {}; + var servernamesMap = {}; + program.serve.forEach(function (proxy) { + servernamesMap[proxy.hostname] = true; + if (proxy.port) { + portsMap[proxy.port] = true; + } + }); + program.servernames.forEach(function (name) { + servernamesMap[name] = true; + }); + program.ports.forEach(function (port) { + portsMap[port] = true; + }); + + program.servernames = Object.keys(servernamesMap); + if (!program.servernames.length) { + throw new Error('You must give this server at least one servername for its admin interface. Example:\n\n\t--servernames tunnel.example.com,tunnel.example.net'); + } + + program.ports = Object.keys(portsMap); + if (!program.ports.length) { + program.ports = [ 80, 443 ]; + } + + if (!program.secret) { + // TODO randomly generate and store in file? + console.warn("[SECURITY] you must provide --secret '" + require('crypto').randomBytes(16).toString('hex') + "'"); + process.exit(1); + return; + } + + //program.tlsOptions.SNICallback = program.greenlock.httpsOptions.SNICallback; + /* + program.middleware = program.greenlock.middleware(function (req, res) { + res.end('Hello, World!'); + }); + */ +} + +//adjustArgs(); + +}()); diff --git a/dist/etc/systemd/system/stunneld.service b/dist/etc/systemd/system/stunneld.service deleted file mode 100644 index 45bde07..0000000 --- a/dist/etc/systemd/system/stunneld.service +++ /dev/null @@ -1,23 +0,0 @@ -[Unit] -Description=Daplie Tunnel Server -After=network-online.target -Wants=network-online.target systemd-networkd-wait-online.service - -[Service] -# Always restart, unless it's restarting fast enough for us to believe it's completely broken -Restart=always -StartLimitInterval=10 -StartLimitBurst=3 - -User=www-data -Group=www-data -WorkingDirectory=/srv/stunneld -# TODO needs --config option and these options should go in a config file -ExecStart=/srv/stunneld/bin/stunneld.js --servernames tunnel.example.com --secret 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' --email tunnel@example.com --agree-tos - -CapabilityBoundingSet=CAP_NET_BIND_SERVICE -AmbientCapabilities=CAP_NET_BIND_SERVICE -NoNewPrivileges=true - -[Install] -WantedBy=multi-user.target diff --git a/dist/etc/systemd/system/telebitd.service b/dist/etc/systemd/system/telebitd.service new file mode 100644 index 0000000..468fa8a --- /dev/null +++ b/dist/etc/systemd/system/telebitd.service @@ -0,0 +1,66 @@ +# Pre-req +# sudo adduser telebit --home /opt/telebitd +# sudo mkdir -p /opt/telebitd/ +# sudo chown -R telebit:telebit /opt/telebitd/ + +[Unit] +Description=Telebit Relay +Documentation=https://git.coolaj86.com/coolaj86/telebitd.js/ +After=network-online.target +Wants=network-online.target systemd-networkd-wait-online.service + +[Service] +# Restart on crash (bad signal), but not on 'clean' failure (error exit code) +# Allow up to 3 restarts within 10 seconds +# (it's unlikely that a user or properly-running script will do this) +Restart=on-abnormal +StartLimitInterval=10 +StartLimitBurst=3 + +# User and group the process will run as +# (git is the de facto standard on most systems) +User=telebit +Group=telebit + +WorkingDirectory=/opt/telebitd +# custom directory cannot be set and will be the place where gitea exists, not the working directory +ExecStart=/opt/telebitd/bin/node /opt/telebitd/bin/telebitd.js --config /etc/telebit/telebitd.yml +ExecReload=/bin/kill -USR1 $MAINPID + +# Limit the number of file descriptors and processes; see `man systemd.exec` for more limit settings. +# Unmodified gitea is not expected to use more than this. +LimitNOFILE=1048576 +LimitNPROC=64 + +# Use private /tmp and /var/tmp, which are discarded after gitea stops. +PrivateTmp=true +# Use a minimal /dev +PrivateDevices=true +# Hide /home, /root, and /run/user. Nobody will steal your SSH-keys. +ProtectHome=true +# Make /usr, /boot, /etc and possibly some more folders read-only. +ProtectSystem=full +# ... except /opt/gitea because we want a place for the database +# and /var/log/gitea because we want a place where logs can go. +# This merely retains r/w access rights, it does not add any new. +# Must still be writable on the host! +ReadWriteDirectories=/opt/telebitd + +# Note: in v231 and above ReadWritePaths has been renamed to ReadWriteDirectories +; ReadWritePaths=/opt/telebitd + +# The following additional security directives only work with systemd v229 or later. +# They further retrict privileges that can be gained by gitea. +# Note that you may have to add capabilities required by any plugins in use. +CapabilityBoundingSet=CAP_NET_BIND_SERVICE +AmbientCapabilities=CAP_NET_BIND_SERVICE +NoNewPrivileges=true + +# Caveat: Some features may need additional capabilities. +# For example an "upload" may need CAP_LEASE +; CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_LEASE +; AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_LEASE +; NoNewPrivileges=true + +[Install] +WantedBy=multi-user.target diff --git a/examples/telebitd.yml b/examples/telebitd.yml new file mode 100644 index 0000000..9b6af63 --- /dev/null +++ b/examples/telebitd.yml @@ -0,0 +1,3 @@ +servernames: [] +email: 'jon@example.com' +agree_tos: true diff --git a/handlers.js b/handlers.js index aa228a7..8ca03c3 100644 --- a/handlers.js +++ b/handlers.js @@ -5,8 +5,19 @@ var tls = require('tls'); var wrapSocket = require('tunnel-packer').wrapSocket; var redirectHttps = require('redirect-https')(); -module.exports.create = function (program) { +module.exports.create = function (state) { var tunnelAdminTlsOpts = {}; + var setupSniCallback; + var setupTlsOpts = { + SNICallback: function (servername, cb) { + if (!setupSniCallback) { + console.error("No way to get https certificates..."); + cb(new Error("telebitd sni setup fail")); + return; + } + setupSniCallback(servername, cb); + } + }; // Probably a reverse proxy on an internal network (or ACME challenge) function notFound(req, res) { @@ -14,14 +25,14 @@ module.exports.create = function (program) { res.statusCode = 404; res.end("File not found.\n"); } - program.httpServer = http.createServer( - program.greenlock && program.greenlock.middleware(notFound) + state.httpServer = http.createServer( + state.greenlock && state.greenlock.middleware(notFound) || notFound ); - program.handleHttp = function (servername, socket) { + state.handleHttp = function (servername, socket) { console.log("handleHttp('" + servername + "', socket)"); socket.__my_servername = servername; - program.httpServer.emit('connection', socket); + state.httpServer.emit('connection', socket); }; // Probably something that needs to be redirected to https @@ -29,37 +40,37 @@ module.exports.create = function (program) { res.setHeader('Connection', 'close'); redirectHttps(req, res); } - program.httpInsecureServer = http.createServer( - program.greenlock && program.greenlock.middleware(redirectHttpsAndClose) + state.httpInsecureServer = http.createServer( + state.greenlock && state.greenlock.middleware(redirectHttpsAndClose) || redirectHttpsAndClose ); - program.handleInsecureHttp = function (servername, socket) { + state.handleInsecureHttp = function (servername, socket) { console.log("handleInsecureHttp('" + servername + "', socket)"); socket.__my_servername = servername; - program.httpInsecureServer.emit('connection', socket); + state.httpInsecureServer.emit('connection', socket); }; // // SNI is not recogonized / cannot be handled // - program.httpInvalidSniServer = http.createServer(function (req, res) { + state.httpInvalidSniServer = http.createServer(function (req, res) { res.end("This is an old error message that shouldn't be actually be acessible anymore. If you get this please tell AJ so that he finds where it was still referenced and removes it"); }); - program.tlsInvalidSniServer = tls.createServer(program.tlsOptions, function (tlsSocket) { + state.tlsInvalidSniServer = tls.createServer(state.tlsOptions, function (tlsSocket) { console.log('tls connection'); // things get a little messed up here - program.httpInvalidSniServer.emit('connection', tlsSocket); + state.httpInvalidSniServer.emit('connection', tlsSocket); }); - program.httpsInvalid = function (servername, socket) { + state.httpsInvalid = function (servername, socket) { // none of these methods work: // httpsServer.emit('connection', socket); // this didn't work // tlsServer.emit('connection', socket); // this didn't work either //console.log('chunkLen', firstChunk.byteLength); console.log('httpsInvalid servername', servername); - //program.tlsInvalidSniServer.emit('connection', wrapSocket(socket)); - var tlsInvalidSniServer = tls.createServer(program.tlsOptions, function (tlsSocket) { + //state.tlsInvalidSniServer.emit('connection', wrapSocket(socket)); + var tlsInvalidSniServer = tls.createServer(state.tlsOptions, function (tlsSocket) { console.log('tls connection'); // things get a little messed up here var httpInvalidSniServer = http.createServer(function (req, res) { @@ -90,29 +101,62 @@ module.exports.create = function (program) { // // To ADMIN / CONTROL PANEL of the Tunnel Server Itself // - program.httpTunnelServer = http.createServer(function (req, res) { + var serveAdmin = require('serve-static')(__dirname + '/admin', { redirect: true }); + var finalhandler = require('finalhandler'); + state.httpTunnelServer = http.createServer(function (req, res) { console.log('req.socket.encrypted', req.socket.encrypted); - res.end('Hello, World!'); + serveAdmin(req, res, finalhandler(req, res)); }); - Object.keys(program.tlsOptions).forEach(function (key) { - tunnelAdminTlsOpts[key] = program.tlsOptions[key]; + Object.keys(state.tlsOptions).forEach(function (key) { + tunnelAdminTlsOpts[key] = state.tlsOptions[key]; }); - tunnelAdminTlsOpts.SNICallback = (program.greenlock && program.greenlock.httpsOptions && function (servername, cb) { + tunnelAdminTlsOpts.SNICallback = (state.greenlock && state.greenlock.tlsOptions && function (servername, cb) { console.log("time to handle '" + servername + "'"); - program.greenlock.httpsOptions.SNICallback(servername, cb); + state.greenlock.tlsOptions.SNICallback(servername, cb); }) || tunnelAdminTlsOpts.SNICallback; - program.tlsTunnelServer = tls.createServer(tunnelAdminTlsOpts, function (tlsSocket) { + state.tlsTunnelServer = tls.createServer(tunnelAdminTlsOpts, function (tlsSocket) { console.log('tls connection'); // things get a little messed up here - (program.httpTunnelServer || program.httpServer).emit('connection', tlsSocket); + (state.httpTunnelServer || state.httpServer).emit('connection', tlsSocket); }); - program.httpsTunnel = function (servername, socket) { + state.httpsTunnel = function (servername, socket) { // none of these methods work: // httpsServer.emit('connection', socket); // this didn't work // tlsServer.emit('connection', socket); // this didn't work either //console.log('chunkLen', firstChunk.byteLength); console.log('httpsTunnel (Admin) servername', servername); - program.tlsTunnelServer.emit('connection', wrapSocket(socket)); + state.tlsTunnelServer.emit('connection', wrapSocket(socket)); + }; + + // + // First time setup + // + var serveSetup = require('serve-static')(__dirname + '/admin/setup', { redirect: true }); + var finalhandler = require('finalhandler'); + state.httpSetupServer = http.createServer(function (req, res) { + console.log('req.socket.encrypted', req.socket.encrypted); + if (req.socket.encrypted) { + serveSetup(req, res, finalhandler(req, res)); + return; + } + console.log('try greenlock middleware'); + (state.greenlock && state.greenlock.middleware(redirectHttpsAndClose) + || redirectHttpsAndClose)(req, res, function () { + console.log('fallthrough to setup ui'); + serveSetup(req, res, finalhandler(req, res)); + }); + }); + state.tlsSetupServer = tls.createServer(setupTlsOpts, function (tlsSocket) { + console.log('tls connection'); + // things get a little messed up here + state.httpSetupServer.emit('connection', tlsSocket); + }); + state.httpsSetupServer = function (servername, socket) { + console.log('httpsTunnel (Admin) servername', servername); + state._servernames = [servername]; + state.config.agreeTos = true; // TODO: BUG XXX BAD, make user accept + setupSniCallback = state.greenlock.tlsOptions.SNICallback; + state.tlsSetupServer.emit('connection', wrapSocket(socket)); }; }; diff --git a/installer/get.sh b/installer/get.sh new file mode 100644 index 0000000..23fe37e --- /dev/null +++ b/installer/get.sh @@ -0,0 +1,145 @@ +#!/bin/bash + +# This is a 3 step process +# 1. First we need to figure out whether to use wget or curl for fetching remote files +# 2. Next we need to figure out whether to use unzip or tar for downloading releases +# 3. We need to actually install the stuff + +set -e +set -u + +############################### +# # +# http_get # +# boilerplate for curl / wget # +# # +############################### + +# See https://git.coolaj86.com/coolaj86/snippets/blob/master/bash/http-get.sh + +_my_http_get="" +_my_http_opts="" +_my_http_out="" + +detect_http_get() +{ + set +e + if type -p curl >/dev/null 2>&1; then + _my_http_get="curl" + _my_http_opts="-fsSL" + _my_http_out="-o" + elif type -p wget >/dev/null 2>&1; then + _my_http_get="wget" + _my_http_opts="--quiet" + _my_http_out="-O" + else + echo "Aborted, could not find curl or wget" + return 7 + fi + set -e +} + +http_get() +{ + $_my_http_get $_my_http_opts $_my_http_out "$2" "$1" + touch "$2" +} + +http_bash() +{ + _http_url=$1 + my_args=${2:-} + rm -rf my-tmp-runner.sh + $_my_http_get $_my_http_opts $_my_http_out my-tmp-runner.sh "$_http_url"; bash my-tmp-runner.sh $my_args; rm my-tmp-runner.sh +} + +detect_http_get + +############################### +## END HTTP_GET ## +############################### + +echo "" +echo "" +echo "" + +my_app="telebitd" +my_bin="telebitd.js" +my_name="Telebit Relay" +my_repo="telebitd.js" + +if [ -z "${TELEBITD_PATH:-}" ]; then + echo 'TELEBITD_PATH="'${TELEBITD_PATH:-}'"' + TELEBITD_PATH=/opt/$my_app +fi + +echo "Installing $my_name to '$TELEBITD_PATH'" +echo "" + +echo "sudo mkdir -p '$TELEBITD_PATH'" +sudo mkdir -p "$TELEBITD_PATH" +echo "sudo chown -R $(whoami) '$TELEBITD_PATH'" +sudo chown -R $(whoami) "$TELEBITD_PATH" + +echo "Installing node.js dependencies into $TELEBITD_PATH" +# until node v10.x gets fix for ursa we have no advantage to switching from 8.x +export NODEJS_VER=v8.11.1 +export NODE_PATH="$TELEBITD_PATH/lib/node_modules" +export NPM_CONFIG_PREFIX="$TELEBITD_PATH" +export PATH="$TELEBITD_PATH/bin:$PATH" +sleep 1 +http_bash https://git.coolaj86.com/coolaj86/node-installer.sh/raw/branch/master/install.sh --no-dev-deps >/dev/null 2>/dev/null + +my_tree="master" +my_node="$TELEBITD_PATH/bin/node" +my_npm="$my_node $TELEBITD_PATH/bin/npm" +my_tmp="$TELEBITD_PATH/tmp" +mkdir -p $my_tmp + +echo "Installing $my_name into $TELEBITD_PATH" +set +e +my_unzip=$(type -p unzip) +my_tar=$(type -p tar) +if [ -n "$my_unzip" ]; then + rm -f $my_tmp/$my_app-$my_tree.zip + http_get https://git.coolaj86.com/coolaj86/$my_repo/archive/$my_tree.zip $my_tmp/$my_app-$my_tree.zip + # -o means overwrite, and there is no option to strip + $my_unzip -o $my_tmp/$my_app-$my_tree.zip -d $TELEBITD_PATH/ > /dev/null + cp -ar $TELEBITD_PATH/$my_repo/* $TELEBITD_PATH/ > /dev/null + rm -rf $TELEBITD_PATH/$my_bin +elif [ -n "$my_tar" ]; then + rm -f $my_tmp/$my_app-$my_tree.tar.gz + http_get https://git.coolaj86.com/coolaj86/$my_repo/archive/$my_tree.tar.gz $my_tmp/$my_app-$my_tree.tar.gz + ls -lah $my_tmp/$my_app-$my_tree.tar.gz + $my_tar -xzf $my_tmp/$my_app-$my_tree.tar.gz --strip 1 -C $TELEBITD_PATH/ +else + echo "Neither tar nor unzip found. Abort." + exit 13 +fi +set -e + +pushd $TELEBITD_PATH >/dev/null + $my_npm install >/dev/null 2>/dev/null +popd >/dev/null + +cat << EOF > $TELEBITD_PATH/bin/$my_app +#!/bin/bash +$my_node $TELEBITD_PATH/bin/$my_bin +EOF +chmod a+x $TELEBITD_PATH/bin/$my_app +echo "Creating link to '$my_app' in /usr/local/bin" +ln -sf $TELEBITD_PATH/bin/$my_app /usr/local/bin/$my_app + +echo "" +echo "" +echo "Installed successfully. Try it out:" +echo "" +echo " $my_app --help" +echo "" +echo "" + +#sudo setcap cap_net_bind_service=+ep $TELEBITD_PATH/bin/node + +#https://git.coolaj86.com/coolaj86/telebitd.js.git +#https://git.coolaj86.com/coolaj86/telebitd.js/archive/:tree:.tar.gz +#https://git.coolaj86.com/coolaj86/telebitd.js/archive/:tree:.zip diff --git a/lib/unwrap-tls.js b/lib/unwrap-tls.js index afec015..0986b2a 100644 --- a/lib/unwrap-tls.js +++ b/lib/unwrap-tls.js @@ -80,6 +80,12 @@ module.exports.createTcpConnectionHandler = function (copts) { var m; function tryTls() { + if (!copts.servernames.length) { + console.log("https => admin => setup => (needs bogus tls certs to start?)"); + copts.httpsSetupServer(servername, conn); + return; + } + if (-1 !== copts.servernames.indexOf(servername)) { console.log("Lock and load, admin interface time!"); copts.httpsTunnel(servername, conn); @@ -118,9 +124,16 @@ module.exports.createTcpConnectionHandler = function (copts) { m = str.match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im); servername = (m && m[1].toLowerCase() || '').split(':')[0]; console.log('servername', servername); + if (/HTTP\//i.test(str)) { + if (!copts.servernames.length) { + console.log('copts.httpSetupServer', copts.httpSetupServer); + copts.httpSetupServer.emit('connection', conn); + return; + } + service = 'http'; - // TODO disallow http entirely + // TODO make https redirect configurable // /^\/\.well-known\/acme-challenge\//.test(str) if (/well-known/.test(str)) { // HTTP diff --git a/package.json b/package.json index 0a12df8..a995724 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,17 @@ { - "name": "stunneld", + "name": "telebitd", "version": "0.10.0", - "description": "A pure-JavaScript tunnel daemon for http and https similar to a localtunnel.me server, but uses TLS (SSL) with ServerName Indication (SNI) over https to work even in harsh network conditions such as in student dorms and behind HOAs, corporate firewalls, public libraries, airports, airplanes, etc. Can also tunnel tls and plain tcp.", - "main": "wstunneld.js", + "description": "Friends don't let friends localhost. Expose your bits with a secure connection even from behind NAT, Firewalls, in a box, with a fox, on a train or in a plane... or a Raspberry Pi in your closet. An attempt to create a better localtunnel.me server, a more open ngrok. Uses Automated HTTPS (Free SSL) via ServerName Indication (SNI). Can also tunnel tls and plain tcp.", + "main": "telebitd.js", "bin": { - "jstunneld": "bin/stunneld.js", - "stunneld.js": "bin/stunneld.js", - "stunneld-js": "bin/stunneld.js" + "telebitd": "bin/telebitd.js" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", - "url": "git+https://git.coolaj86.com/coolaj86/tunnel-server.js.git" + "url": "https://git.coolaj86.com/coolaj86/telebitd.js.git" }, "keywords": [ "server", @@ -37,22 +35,25 @@ "reverse-proxy", "reverseproxy", "vpn", - "sni" + "sni", + "ngrok" ], "author": "AJ ONeal (https://coolaj86.com/)", "license": "(MIT OR Apache-2.0)", "bugs": { - "url": "https://git.coolaj86.com/coolaj86/tunnel-server.js/issues" + "url": "https://git.coolaj86.com/coolaj86/telebitd.js/issues" }, - "homepage": "https://git.coolaj86.com/coolaj86/tunnel-server.js", + "homepage": "https://git.coolaj86.com/coolaj86/telebitd.js", "dependencies": { "bluebird": "^3.5.1", "cluster-store": "^2.0.8", - "commander": "^2.15.1", + "finalhandler": "^1.1.1", "greenlock": "^2.2.4", + "js-yaml": "^3.11.0", "jsonwebtoken": "^8.2.1", - "localhost.daplie.me-certificates": "^1.3.5", + "recase": "^1.0.4", "redirect-https": "^1.1.5", + "serve-static": "^1.13.2", "sni": "^1.0.0", "tunnel-packer": "^1.4.0", "ws": "^5.1.1" diff --git a/wstunneld.js b/telebitd.js similarity index 100% rename from wstunneld.js rename to telebitd.js