mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-01-29 23:54:12 +00:00
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:
parent
d60ec13d5c
commit
7ae83d9ce5
@ -15,10 +15,14 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
import type { BalenaSDK } from 'balena-sdk';
|
import type { BalenaSDK } from 'balena-sdk';
|
||||||
import { Socket } from 'net';
|
import { Socket } from 'net';
|
||||||
|
import * as tls from 'tls';
|
||||||
import { TypedError } from 'typed-error';
|
import { TypedError } from 'typed-error';
|
||||||
|
import { ExpectedError } from '../errors';
|
||||||
|
|
||||||
const PROXY_CONNECT_TIMEOUT_MS = 10000;
|
const PROXY_CONNECT_TIMEOUT_MS = 10000;
|
||||||
|
|
||||||
|
class TunnelServerNotTrustedError extends ExpectedError {}
|
||||||
|
|
||||||
class UnableToConnectError extends TypedError {
|
class UnableToConnectError extends TypedError {
|
||||||
public status: string;
|
public status: string;
|
||||||
public statusCode: string;
|
public statusCode: string;
|
||||||
@ -42,17 +46,17 @@ export const tunnelConnectionToDevice = (
|
|||||||
sdk: BalenaSDK,
|
sdk: BalenaSDK,
|
||||||
) => {
|
) => {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
sdk.settings.get('vpnUrl'),
|
sdk.settings.get('tunnelUrl'),
|
||||||
sdk.auth.whoami(),
|
sdk.auth.whoami(),
|
||||||
sdk.auth.getToken(),
|
sdk.auth.getToken(),
|
||||||
]).then(([vpnUrl, whoami, token]) => {
|
]).then(([tunnelUrl, whoami, token]) => {
|
||||||
const auth = {
|
const auth = {
|
||||||
user: whoami || 'root',
|
user: whoami || 'root',
|
||||||
password: token,
|
password: token,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (client: Socket): Promise<void> =>
|
return (client: Socket): Promise<void> =>
|
||||||
openPortThroughProxy(vpnUrl, 3128, auth, uuid, port)
|
openPortThroughProxy(tunnelUrl, 443, auth, uuid, port)
|
||||||
.then((remote) => {
|
.then((remote) => {
|
||||||
client.pipe(remote);
|
client.pipe(remote);
|
||||||
remote.pipe(client);
|
remote.pipe(client);
|
||||||
@ -96,30 +100,41 @@ const openPortThroughProxy = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
return new Promise<Socket>((resolve, reject) => {
|
return new Promise<Socket>((resolve, reject) => {
|
||||||
const proxyTunnel = new Socket();
|
const proxyTunnel = tls.connect(
|
||||||
proxyTunnel.on('error', reject);
|
proxyPort,
|
||||||
proxyTunnel.connect(proxyPort, proxyServer, () => {
|
proxyServer,
|
||||||
const proxyConnectionHandler = (data: Buffer) => {
|
{ servername: proxyServer }, // send the hostname in the SNI field
|
||||||
proxyTunnel.removeListener('data', proxyConnectionHandler);
|
() => {
|
||||||
const [httpStatus] = data.toString('utf8').split('\r\n');
|
if (!proxyTunnel.authorized) {
|
||||||
const [, httpStatusCode, ...httpMessage] = httpStatus.split(' ');
|
console.error('Unable to authorize the tunnel server');
|
||||||
|
|
||||||
if (parseInt(httpStatusCode, 10) === 200) {
|
|
||||||
proxyTunnel.setTimeout(0);
|
|
||||||
resolve(proxyTunnel);
|
|
||||||
} else {
|
|
||||||
reject(
|
reject(
|
||||||
new UnableToConnectError(httpStatusCode, httpMessage.join(' ')),
|
new TunnelServerNotTrustedError(proxyTunnel.authorizationError),
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
proxyTunnel.on('timeout', () => {
|
proxyTunnel.once('data', (data: Buffer) => {
|
||||||
reject(new RemoteSocketNotListening(devicePort));
|
const [httpStatus] = data.toString('utf8').split('\r\n');
|
||||||
});
|
const [, httpStatusCode, ...httpMessage] = httpStatus.split(' ');
|
||||||
proxyTunnel.on('data', proxyConnectionHandler);
|
|
||||||
proxyTunnel.setTimeout(PROXY_CONNECT_TIMEOUT_MS);
|
if (parseInt(httpStatusCode, 10) === 200) {
|
||||||
proxyTunnel.write(httpHeaders.join('\r\n').concat('\r\n\r\n'));
|
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
6
npm-shrinkwrap.json
generated
@ -2760,9 +2760,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"balena-settings-client": {
|
"balena-settings-client": {
|
||||||
"version": "4.0.5",
|
"version": "4.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/balena-settings-client/-/balena-settings-client-4.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/balena-settings-client/-/balena-settings-client-4.0.6.tgz",
|
||||||
"integrity": "sha512-w1SWIQYViMP51PYnPvbwgGavipkBv8wbRj1ISjPYZ5M45oEVRcktDfix8c3xOlWl+vWqW8aA4L8BjhqnxhAvRQ==",
|
"integrity": "sha512-bB14Zvg1N6t7XXPJqZs48SajgTuk2WTMm2AnxcOfoIQ2d/Lh0RsEGxD9toF2v+WhF2Ip4u7ko5tKlCr2kFddXA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@resin.io/types-hidepath": "1.0.1",
|
"@resin.io/types-hidepath": "1.0.1",
|
||||||
"@resin.io/types-home-or-tmp": "3.0.0",
|
"@resin.io/types-home-or-tmp": "3.0.0",
|
||||||
|
@ -203,7 +203,7 @@
|
|||||||
"balena-release": "^3.0.0",
|
"balena-release": "^3.0.0",
|
||||||
"balena-sdk": "^15.20.0",
|
"balena-sdk": "^15.20.0",
|
||||||
"balena-semver": "^2.3.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-settings-storage": "^7.0.0",
|
||||||
"balena-sync": "^11.0.2",
|
"balena-sync": "^11.0.2",
|
||||||
"bluebird": "^3.7.2",
|
"bluebird": "^3.7.2",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user