From 7ae83d9ce578dc82d8324fa78f3b59c4bc965e4f Mon Sep 17 00:00:00 2001 From: Balena CI <34882892+balena-ci@users.noreply.github.com> Date: Tue, 19 Jan 2021 08:22:20 +0000 Subject: [PATCH] tls: Use TLS for tunnel connection Switch to using the exposed tunnelUrl and TLS for making tunnels to the device, to improve security. Change-type: patch Signed-off-by: Rich Bayliss --- lib/utils/tunnel.ts | 63 ++++++++++++++++++++++++++++----------------- npm-shrinkwrap.json | 6 ++--- package.json | 2 +- 3 files changed, 43 insertions(+), 28 deletions(-) diff --git a/lib/utils/tunnel.ts b/lib/utils/tunnel.ts index 04bf64a5..288a0e6f 100644 --- a/lib/utils/tunnel.ts +++ b/lib/utils/tunnel.ts @@ -15,10 +15,14 @@ limitations under the License. */ import type { BalenaSDK } from 'balena-sdk'; import { Socket } from 'net'; +import * as tls from 'tls'; import { TypedError } from 'typed-error'; +import { ExpectedError } from '../errors'; const PROXY_CONNECT_TIMEOUT_MS = 10000; +class TunnelServerNotTrustedError extends ExpectedError {} + class UnableToConnectError extends TypedError { public status: string; public statusCode: string; @@ -42,17 +46,17 @@ export const tunnelConnectionToDevice = ( sdk: BalenaSDK, ) => { return Promise.all([ - sdk.settings.get('vpnUrl'), + sdk.settings.get('tunnelUrl'), sdk.auth.whoami(), sdk.auth.getToken(), - ]).then(([vpnUrl, whoami, token]) => { + ]).then(([tunnelUrl, whoami, token]) => { const auth = { user: whoami || 'root', password: token, }; return (client: Socket): Promise => - openPortThroughProxy(vpnUrl, 3128, auth, uuid, port) + openPortThroughProxy(tunnelUrl, 443, auth, uuid, port) .then((remote) => { client.pipe(remote); remote.pipe(client); @@ -96,30 +100,41 @@ const openPortThroughProxy = ( } return new Promise((resolve, reject) => { - const proxyTunnel = new Socket(); - proxyTunnel.on('error', reject); - proxyTunnel.connect(proxyPort, proxyServer, () => { - const proxyConnectionHandler = (data: Buffer) => { - proxyTunnel.removeListener('data', proxyConnectionHandler); - const [httpStatus] = data.toString('utf8').split('\r\n'); - const [, httpStatusCode, ...httpMessage] = httpStatus.split(' '); - - if (parseInt(httpStatusCode, 10) === 200) { - proxyTunnel.setTimeout(0); - resolve(proxyTunnel); - } else { + const proxyTunnel = tls.connect( + proxyPort, + proxyServer, + { servername: proxyServer }, // send the hostname in the SNI field + () => { + if (!proxyTunnel.authorized) { + console.error('Unable to authorize the tunnel server'); reject( - new UnableToConnectError(httpStatusCode, httpMessage.join(' ')), + new TunnelServerNotTrustedError(proxyTunnel.authorizationError), ); + return; } - }; - proxyTunnel.on('timeout', () => { - reject(new RemoteSocketNotListening(devicePort)); - }); - proxyTunnel.on('data', proxyConnectionHandler); - proxyTunnel.setTimeout(PROXY_CONNECT_TIMEOUT_MS); - proxyTunnel.write(httpHeaders.join('\r\n').concat('\r\n\r\n')); - }); + proxyTunnel.once('data', (data: Buffer) => { + const [httpStatus] = data.toString('utf8').split('\r\n'); + const [, httpStatusCode, ...httpMessage] = httpStatus.split(' '); + + if (parseInt(httpStatusCode, 10) === 200) { + proxyTunnel.setTimeout(0); + resolve(proxyTunnel); + } else { + reject( + new UnableToConnectError(httpStatusCode, httpMessage.join(' ')), + ); + } + }); + + proxyTunnel.on('timeout', () => { + reject(new RemoteSocketNotListening(devicePort)); + }); + + proxyTunnel.setTimeout(PROXY_CONNECT_TIMEOUT_MS); + proxyTunnel.write(httpHeaders.join('\r\n').concat('\r\n\r\n')); + }, + ); + proxyTunnel.on('error', reject); }); }; diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 88a0dcfc..d4675dfb 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -2760,9 +2760,9 @@ } }, "balena-settings-client": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/balena-settings-client/-/balena-settings-client-4.0.5.tgz", - "integrity": "sha512-w1SWIQYViMP51PYnPvbwgGavipkBv8wbRj1ISjPYZ5M45oEVRcktDfix8c3xOlWl+vWqW8aA4L8BjhqnxhAvRQ==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/balena-settings-client/-/balena-settings-client-4.0.6.tgz", + "integrity": "sha512-bB14Zvg1N6t7XXPJqZs48SajgTuk2WTMm2AnxcOfoIQ2d/Lh0RsEGxD9toF2v+WhF2Ip4u7ko5tKlCr2kFddXA==", "requires": { "@resin.io/types-hidepath": "1.0.1", "@resin.io/types-home-or-tmp": "3.0.0", diff --git a/package.json b/package.json index 6ad1bf23..ef731857 100644 --- a/package.json +++ b/package.json @@ -203,7 +203,7 @@ "balena-release": "^3.0.0", "balena-sdk": "^15.20.0", "balena-semver": "^2.3.0", - "balena-settings-client": "^4.0.5", + "balena-settings-client": "^4.0.6", "balena-settings-storage": "^7.0.0", "balena-sync": "^11.0.2", "bluebird": "^3.7.2",