tls-router

Route incoming TLS clients to backend plaintext TCP servers based on the ALPN (protocol) and SNI (servername).

Usage no npm install needed!

<script type="module">
  import tlsRouter from 'https://cdn.skypack.dev/tls-router';
</script>

README

tls-router 🛂

Route incoming TLS clients to backend plaintext TCP servers based on the ALPN (protocol) and SNI (servername) of the TLS handshake.

Tls Router can serve both encrypted (TLS) and plaintext (TCP) traffic on the same port. It detects whether a client is using TLS based on the first three bytes of the first data packet, also known as the TLS "ClientHello" handshake. Plaintext TCP clients are directed to an optional fallback backend.

Performance impact on network throughput should be minimal since all the data processing is handled by the internals of Node.js, specifically OpenSSL and libuv. No streaming data is processed at the JavaScript level.

This tool can be used with any TLS/TCP based connections. Its initial purpose is to allow legacy Gopher servers to transparently support Gopher over TLS (using ALPN) and virtual hosting multiple domains on the same IP address (using SNI).

Usage

Node.js must be available on the system, exposing the global node, npm, and npx commands.

Global Installation

System-wide installation makes the tls-router command globally available. The trade-off is that only a single version can be installed.

npm install --global tls-router

tls-router [options]

Local Installation

To include tls-router in a package for Node.js, use local installation to allow dependency versioning.

npm install --development tls-router

... or:

npm install --production tls-router

Then use tls-router via its API in your application code, or run the local command as npx tls-router, or use it within an npm-run script inside your package.json file.

npx tls-router [options]

... or:

{
  "name": "my-app",
  "scripts": {
    "my-script": "tls-router"
  },
  "devDepencencies": {
    "tls-router": "*"
  }
}
npm run my-script --  [options]

On-demand Installation

To run tls-router as a one-off command, without permanent global installation nor requiring a package.json project file, use the handy npx command that comes with Node.js. This downloads and installs into a temporary location, then cleans up after the command terminates. Downloads are cached so subsequent runs should be faster.

npx tls-router [options]

CLI

Example: Gopher over TLS with various ways to configure routing rules.

npx tls-router
  # Accept both TLS and TCP clients on port 70
  --listen 70

  # Offered ALPN names
  --alpn gopher

  # TLS crypto files
  --public-certificate cert.pem
  --private-key key.pem
  --certificate-authority ca.pem

  # TCP connections are piped to a local backend server on port 7777
  --plaintext localhost:7777

  # ALPN=gopher SNI=example.net to localhost:7000 (DNS lookup)
  --route gopher:example.net:localhost:7000

  # ALPN=gopher SNI=example.net to 127.0.0.1:7000 (IPv4)
  --route gopher:example.net:127.0.0.1:7000

  # ALPN=gopher SNI=example.net to [::1]:7000 (IPv6)
  --route gopher:example.net:[::1]:7000

  # ALPN=* SNI=example.net to 127.0.0.1:7000
  --route :example.net:127.0.0.1:7000

  # ALPN=gopher SNI=* to 127.0.0.1:7000
  --route gopher::127.0.0.1:7000

  # all trafic as plaintext to localhost:7000
  --route 7000

  # all traffic as plaintext to 127.0.0.1:7000 (IPv4)
  --route 127.0.0.1:7000

  # all traffic as plaintext to [::1]:7000 (IPv6)
  --route '[::1]:7000'

  # ALPN=* SNI=example.net to 127.0.0.1:7000
  --route :example.net:127.0.0.1:7000

CLI options can also be specified using:

  • Environment variables with the TLS_ROUTER_ prefix. E.g. TLS_ROUTER_LISTEN=70
  • JSON configuration file whose path is specified with the --config option. E.g. --config options.json

For more information run with the options:

  • --help to see a list of all available options.
  • --version to show the current application version.

API

Example: Accept Gopher over TLS on port 70. All TLS clients are routed to a regular (plaintext, non-TLS) Gopher server on port 7000. Plaintext TCP clients are routed to a fallback server on port 7777.

const { TlsRouter } = require('tls-router')

const router = new TlsRouter((rule, client, backend) => {
  console.log(`Client proxied to ${backend.port}`)
})

const [key, cert, ca] = await Promise.all([
  readFile('./private-key.pem'),
  readFile('./public-certificate.pem'),
  readFile('./certificate-authority.pem')
])
router.setSecureContext({
  key, cert, ca,
  ALPNProtocols: ['gopher']
})

router.route({ port: 7000 })
router.plaintext = { port: 7777 }

router.listen(70)

Class: new TlsRouter([options][, routedConnectionListener])

Extends: net.Server

Also implements several methods of, but does not inherit from, tls.TLSServer.

options - Passed to the net.Server constructor. See: https://nodejs.org/api/net.html#net_new_net_server_options_connectionlistener

  • ttfbTimeout - The maximum time-to-first-byte before a client is disconnected. Default: 10000

routedConnectionListener - Optional handler for the routedConnection event.

Event: routedConnection

Emitted when an incoming TLS connection is routed to a backend.

Arguments:

  • rule - The matching rule, if any. This is undefined if the client is a plaintext connection. See: router.route()
  • client - The incoming tls.TLSSocket (TLS) or net.Socket (TCP) connection.
  • backend - The forwarded net.Socket (TCP) connection.

Event: plaintextConnection

Emits once a socket is confirmed to be TCP and not TLS. Use this instead of the connection event.

See: https://nodejs.org/api/net.html#net_event_connection

Event: secureConnection

See: https://nodejs.org/api/tls.html#tls_event_secureconnection

Event: missingRoute

When a TLS client connects but no route matches its SNI and ALPN, thi event is emitted. The handler receives the incoming net.Socket (TCP) instance.

It is up to the event handler to deal with the connection. If this event is not listened for, the client connection is gracefully closed.

Event: clientError

Fires when either the client (TLS) or backend (TCP) socket throws an error.

Arguments:

  • error - Error instance
  • client or backend - net.Socket or tls.TLSSocket instance respectively

router.plaintext

Destination address for incoming plaintext TCP connections.

Set to an object with properties:

  • address
  • family
  • port

See: server.address()

Detault: undefined

router.route(...rule)

Each rule has the properties:

  • sni - Match the server domain name provided by the TLS client. Default: Any server name matches.
  • alpn - Match the negotiated protocol provided by the TLS client. Default: Any protocol name matches.
  • port - TCP port of the route destination. Required.
  • address - TCP IP address or DNS hostname of the route destination. Default: localhost
  • family - The IP type, either 4, 6, or 0. Default: 0 (auto).

router.getTicketKeys()

See: https://nodejs.org/api/tls.html#tls_server_getticketkeys

router.setTicketKeys(keys)

See: https://nodejs.org/api/tls.html#tls_server_setticketkeys_keys

router.setSecureContext(options)

See: https://nodejs.org/api/tls.html#tls_server_setsecurecontext_options