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>
This commit is contained in:
Balena CI 2021-01-19 08:22:20 +00:00 committed by Rich Bayliss
parent d60ec13d5c
commit 7ae83d9ce5
3 changed files with 43 additions and 28 deletions

View File

@ -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);
});
};

6
npm-shrinkwrap.json generated
View File

@ -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",

View File

@ -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",