Skip to content

Commit

Permalink
tunnel server TLS endpoint (#427)
Browse files Browse the repository at this point in the history
Add TLS port with routing to either SSH or HTTP server according to the SNI servername, using the same socket
  • Loading branch information
Roy Razon authored Feb 11, 2024
1 parent 845aaf2 commit 3eb8d73
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 56 deletions.
31 changes: 7 additions & 24 deletions tunnel-server/docker-compose.tls.yml
Original file line number Diff line number Diff line change
@@ -1,40 +1,23 @@
version: '3.7'
## NEED TO ADD TLS CONFIGURATION for traefik and stunnel

secrets:
tls-key:
file: ./tls/key.pem
configs:
tls-cert:
file: ./tls/cert.pem
sslh-config:
file: ./tls/sslh.conf

services:
proxy:
environment:
BASE_URL: ${BASE_URL:-https://local.livecycle.run:8044}
sslh:
image: oorabona/sslh:v2.0-rc1
command: [sslh-ev, --config=/etc/sslh/config]
configs:
- source: sslh-config
target: /etc/sslh/config

stunnel:
image: dweomer/stunnel
environment:
- STUNNEL_SERVICE=proxy
- STUNNEL_ACCEPT=0.0.0.0:443
- STUNNEL_CONNECT=sslh:2443
- STUNNEL_KEY=/etc/certs/preview-proxy/key.pem
- STUNNEL_CRT=/etc/certs/preview-proxy/cert.pem
- STUNNEL_DEBUG=err
ports:
- '8044:443'
BASE_URL: ${BASE_URL:-https://local.livecycle.run:8443}
secrets:
- source: tls-key
target: /etc/certs/preview-proxy/key.pem
target: /app/tls/key.pem
configs:
- source: tls-cert
target: /etc/certs/preview-proxy/cert.pem
target: /app/tls/cert.pem
ports:
- '8030:3000'
- '8443:8443'
- '2223:2222'
33 changes: 32 additions & 1 deletion tunnel-server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { cookieSessionStore } from './src/session.js'
import { IdentityProvider, claimsSchema, cliIdentityProvider, jwtAuthenticator, saasIdentityProvider } from './src/auth.js'
import { createSshServer } from './src/ssh/index.js'
import { calcLoginUrl } from './src/app/urls.js'
import { createTlsServer } from './src/tls-server.js'

const log = pino.default(appLoggerFromEnv())

Expand Down Expand Up @@ -47,6 +48,17 @@ const readFileSyncOrUndefined = (filename: string) => {
}
}

const tlsConfig = (() => {
const cert = readFileSyncOrUndefined('./tls/cert.pem')
const key = readFileSyncOrUndefined('./tls/key.pem')
if (!cert || !key) {
log.info('No TLS cert or key found, TLS will be disabled')
return undefined
}
log.info('TLS will be enabled')
return { cert, key }
})()

const saasIdp = (() => {
const saasPublicKeyStr = process.env.SAAS_PUBLIC_KEY || readFileSyncOrUndefined('/etc/certs/preview-proxy/saas.key.pub')
if (!saasPublicKeyStr) {
Expand Down Expand Up @@ -120,14 +132,33 @@ app.listen({ host: LISTEN_HOST, port: PORT }).catch(err => {
process.exit(1)
})

const TLS_PORT = numberFromEnv('TLS_PORT') ?? 8443
const tlsLog = log.child({ name: 'tls_server' })
const tlsServer = tlsConfig
? createTlsServer({
log: tlsLog,
tlsConfig,
sshServer,
httpServer:
app.server,
sshHostnames: new Set([BASE_URL.hostname]),
})
: undefined

tlsServer?.listen({ host: LISTEN_HOST, port: TLS_PORT }, () => { tlsLog.info('TLS server listening on port %j', TLS_PORT) })

runMetricsServer(8888).catch(err => {
app.log.error(err)
});

['SIGTERM', 'SIGINT'].forEach(signal => {
process.once(signal, () => {
app.log.info(`shutting down on ${signal}`)
Promise.all([promisify(sshServer.close).call(sshServer), app.close()])
Promise.all([
promisify(sshServer.close).call(sshServer),
app.close(),
tlsServer ? promisify(tlsServer.close).call(tlsServer) : undefined,
])
.catch(err => {
app.log.error(err)
process.exit(1)
Expand Down
30 changes: 16 additions & 14 deletions tunnel-server/src/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import http from 'http'
import { Logger } from 'pino'
import { KeyObject } from 'crypto'
import { validatorCompiler, serializerCompiler, ZodTypeProvider } from 'fastify-type-provider-zod'
import { Duplex } from 'stream'
import { SessionStore } from '../session.js'
import { Authenticator, Claims } from '../auth.js'
import { ActiveTunnelStore } from '../tunnel-store/index.js'
Expand All @@ -30,30 +31,31 @@ const serverFactory = ({
log.debug('authHostname %j', authHostname)

const isNonProxyRequest = ({ headers }: http.IncomingMessage) => {
log.debug('isNonProxyRequest %j', headers)
const host = headers.host?.split(':')?.[0]
return (host === authHostname) || (host === apiHostname)
}

const server = http.createServer((req, res) => {
const serverHandler = (req: http.IncomingMessage, res: http.ServerResponse) => {
if (req.url !== HEALTZ_URL) {
log.debug('request %j', { method: req.method, url: req.url, headers: req.headers })
}
const proxyHandler = !isNonProxyRequest(req) && proxy.routeRequest(req)
return proxyHandler ? proxyHandler(req, res) : handler(req, res)
})
.on('upgrade', (req, socket, head) => {
log.debug('upgrade %j', { method: req.method, url: req.url, headers: req.headers })
const proxyHandler = !isNonProxyRequest(req) && proxy.routeUpgrade(req)
if (proxyHandler) {
return proxyHandler(req, socket, head)
}
}

const serverUpgradeHandler = (req: http.IncomingMessage, socket: Duplex, head: Buffer) => {
log.debug('upgrade %j', { method: req.method, url: req.url, headers: req.headers })
const proxyHandler = !isNonProxyRequest(req) && proxy.routeUpgrade(req)
if (proxyHandler) {
return proxyHandler(req, socket, head)
}

log.warn('upgrade request %j not found', { method: req.method, url: req.url, host: req.headers.host })
socket.end('Not found')
return undefined
}

log.warn('upgrade request %j not found', { method: req.method, url: req.url, host: req.headers.host })
socket.end('Not found')
return undefined
})
return server
return http.createServer(serverHandler).on('upgrade', serverUpgradeHandler)
}

export const createApp = async ({
Expand Down
9 changes: 8 additions & 1 deletion tunnel-server/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,12 @@ export const requiredEnv = (key: string): string => {

export const numberFromEnv = (key: string) => {
const s = process.env[key]
return s === undefined ? undefined : Number(s)
if (!s) {
return undefined
}
const result = Number(s)
if (Number.isNaN(result)) {
throw new Error(`env var ${key} is not a number: "${s}"`)
}
return result
}
22 changes: 22 additions & 0 deletions tunnel-server/src/tls-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Logger } from 'pino'
import http from 'http'
import ssh from 'ssh2'
import tls from 'tls'

export const createTlsServer = ({ log, httpServer, sshServer, tlsConfig, sshHostnames }: {
log: Logger
httpServer: Pick<http.Server, 'emit'>
sshServer: Pick<ssh.Server, 'injectSocket'>
tlsConfig: tls.TlsOptions
sshHostnames: Set<string>
}) => tls.createServer(tlsConfig)
.on('error', err => { log.error(err) })
.on('secureConnection', socket => {
const { servername } = (socket as { servername?: string })
log.debug('TLS connection: %j', servername)
if (servername && sshHostnames.has(servername)) {
sshServer.injectSocket(socket)
} else {
httpServer.emit('connection', socket)
}
})
16 changes: 0 additions & 16 deletions tunnel-server/tls/sslh.conf

This file was deleted.

0 comments on commit 3eb8d73

Please sign in to comment.