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 <rich@balena.io>
---
 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<void> =>
-			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<Socket>((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",