From d6a065a2300e2f3828eebb6a56e301259b22b9db Mon Sep 17 00:00:00 2001 From: Paulo Castro Date: Fri, 27 Mar 2020 01:24:30 +0000 Subject: [PATCH 1/4] Delete unused code (ssh.coffee) Change-type: patch --- lib/actions/local/ssh.coffee | 113 ----------------------------------- 1 file changed, 113 deletions(-) delete mode 100644 lib/actions/local/ssh.coffee diff --git a/lib/actions/local/ssh.coffee b/lib/actions/local/ssh.coffee deleted file mode 100644 index 83524310..00000000 --- a/lib/actions/local/ssh.coffee +++ /dev/null @@ -1,113 +0,0 @@ -### -Copyright 2017 Balena - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -### - -{ hostOSAccess } = require('../command-options') -_ = require('lodash') - -localHostOSAccessOption = _.cloneDeep(hostOSAccess) -localHostOSAccessOption.description = 'get a shell into the host OS' - -module.exports = - signature: 'local ssh [deviceIp]' - description: 'Get a shell into a balenaOS device' - help: ''' - Warning: 'balena local ssh' requires an openssh-compatible client to be correctly - installed in your shell environment. For more information (including Windows - support) please check the README here: https://github.com/balena-io/balena-cli - - Use this command to get a shell into the running application container of - your device. - - The '--host' option will get you a shell into the Host OS of the balenaOS device. - No option will return a list of containers to enter or you can explicitly select - one by passing its name to the --container option - - Examples: - - $ balena local ssh - $ balena local ssh --host - $ balena local ssh --container chaotic_water - $ balena local ssh --container chaotic_water --port 22222 - $ balena local ssh --verbose - ''' - options: [ - signature: 'verbose' - boolean: true - description: 'increase verbosity' - alias: 'v' - , - localHostOSAccessOption, - signature: 'container' - parameter: 'container' - default: null - description: 'name of container to access' - alias: 'c' - , - signature: 'port' - parameter: 'port' - description: 'ssh port number (default: 22222)' - alias: 'p' - ] - root: true - action: (params, options) -> - child_process = require('child_process') - Promise = require 'bluebird' - _ = require('lodash') - { forms } = require('balena-sync') - - { selectContainerFromDevice, getSubShellCommand } = require('./common') - { exitWithExpectedError } = require('../../utils/patterns') - - if (options.host is true and options.container?) - exitWithExpectedError('Please pass either --host or --container option') - - if not options.port? - options.port = 22222 - - verbose = if options.verbose then '-vvv' else '' - - Promise.try -> - if not params.deviceIp? - return forms.selectLocalBalenaOsDevice() - return params.deviceIp - .then (deviceIp) -> - _.assign(options, { deviceIp }) - - return if options.host - - if not options.container? - return selectContainerFromDevice(deviceIp) - - return options.container - .then (container) -> - - command = "ssh \ - #{verbose} \ - -t \ - -p #{options.port} \ - -o LogLevel=ERROR \ - -o StrictHostKeyChecking=no \ - -o UserKnownHostsFile=/dev/null \ - root@#{options.deviceIp}" - - if not options.host - shellCmd = '''/bin/sh -c $"'if [ -e /bin/bash ]; then exec /bin/bash; else exec /bin/sh; fi'"''' - dockerCmd = "'$(if [ -f /usr/bin/balena ]; then echo \"balena\"; else echo \"docker\"; fi)'" - command += " #{dockerCmd} exec -ti #{container} #{shellCmd}" - - subShellCommand = getSubShellCommand(command) - child_process.spawn subShellCommand.program, subShellCommand.args, - stdio: 'inherit' From be76b8adbd8cc4f04327fb3852f732de7cb77be1 Mon Sep 17 00:00:00 2001 From: Paulo Castro Date: Fri, 27 Mar 2020 01:26:37 +0000 Subject: [PATCH 2/4] Fix 'balena ssh' on MSYS Windows shell ("unexpected end of file") Resolves: #1681 Change-type: patch --- doc/cli.markdown | 6 +- lib/actions/local/common.coffee | 2 - lib/actions/ssh.ts | 139 ++++++++++++++++---------------- lib/utils/device/ssh.ts | 53 +++++------- lib/utils/helpers.ts | 70 ++++++++-------- npm-shrinkwrap.json | 5 -- package.json | 1 - typings/bash/index.d.ts | 1 - 8 files changed, 129 insertions(+), 148 deletions(-) delete mode 100644 typings/bash/index.d.ts diff --git a/doc/cli.markdown b/doc/cli.markdown index 429d09e3..f36ce69c 100644 --- a/doc/cli.markdown +++ b/doc/cli.markdown @@ -1205,7 +1205,8 @@ support) please check: #### --port, -p <port> -SSH gateway port +SSH server port number (default 22222) if the target is an IP address or .local +hostname. Otherwise, port number for the balenaCloud gateway (default 22). #### --verbose, -v @@ -1213,8 +1214,7 @@ Increase verbosity #### --noproxy -Don't use the proxy configuration for this connection. This flag -only make sense if you've configured a proxy globally. +Bypass global proxy configuration for the ssh connection ## tunnel <deviceOrApplication> diff --git a/lib/actions/local/common.coffee b/lib/actions/local/common.coffee index 2b04dead..210ccb34 100644 --- a/lib/actions/local/common.coffee +++ b/lib/actions/local/common.coffee @@ -58,5 +58,3 @@ exports.pipeContainerStream = Promise.method ({ deviceIp, name, outStream, follo if err is '404' return console.log(getChalk().red.bold("Container '#{name}' not found.")) throw err - -exports.getSubShellCommand = require('../../utils/helpers').getSubShellCommand diff --git a/lib/actions/ssh.ts b/lib/actions/ssh.ts index 0860628d..065567c4 100644 --- a/lib/actions/ssh.ts +++ b/lib/actions/ssh.ts @@ -1,5 +1,5 @@ /* -Copyright 2016-2019 Balena +Copyright 2016-2020 Balena Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ import * as BalenaSdk from 'balena-sdk'; import { CommandDefinition } from 'capitano'; import { stripIndent } from 'common-tags'; -import { BalenaDeviceNotFound } from 'balena-errors'; import { getBalenaSdk } from '../utils/lazy'; import { validateDotLocalUrl, validateIPAddress } from '../utils/validation'; @@ -27,7 +26,7 @@ async function getContainerId( serviceName: string, sshOpts: { port?: number; - proxyCommand?: string; + proxyCommand?: string[]; proxyUrl: string; username: string; }, @@ -70,7 +69,7 @@ async function getContainerId( } containerId = body.services[serviceName]; } else { - console.log(stripIndent` + console.error(stripIndent` Using legacy method to detect container ID. This will be slow. To speed up this process, please update your device to an OS which has a supervisor version of at least v8.6.0. @@ -80,30 +79,29 @@ async function getContainerId( // container const { child_process } = await import('mz'); const escapeRegex = await import('lodash/escapeRegExp'); - const { getSubShellCommand } = await import('../utils/helpers'); + const { which } = await import('../utils/helpers'); const { deviceContainerEngineBinary } = await import('../utils/device/ssh'); - const command = generateVpnSshCommand({ + const sshBinary = await which('ssh'); + const sshArgs = generateVpnSshCommand({ uuid, verbose: false, port: sshOpts.port, - command: `host ${uuid} '"${deviceContainerEngineBinary}" ps --format "{{.ID}} {{.Names}}"'`, + command: `host ${uuid} "${deviceContainerEngineBinary}" ps --format "{{.ID}} {{.Names}}"`, proxyCommand: sshOpts.proxyCommand, proxyUrl: sshOpts.proxyUrl, username: sshOpts.username, }); - const subShellCommand = getSubShellCommand(command); - const subprocess = child_process.spawn( - subShellCommand.program, - subShellCommand.args, - { - stdio: [null, 'pipe', null], - }, - ); + if (process.env.DEBUG) { + console.error(`[debug] [${sshBinary}, ${sshArgs.join(', ')}]`); + } + const subprocess = child_process.spawn(sshBinary, sshArgs, { + stdio: [null, 'pipe', null], + }); const containers = await new Promise((resolve, reject) => { - let output = ''; - subprocess.stdout.on('data', chunk => (output += chunk.toString())); + const output: string[] = []; + subprocess.stdout.on('data', chunk => output.push(chunk.toString())); subprocess.on('close', (code: number) => { if (code !== 0) { reject( @@ -112,7 +110,7 @@ async function getContainerId( ), ); } else { - resolve(output); + resolve(output.join('')); } }); }); @@ -143,17 +141,21 @@ function generateVpnSshCommand(opts: { port?: number; username: string; proxyUrl: string; - proxyCommand?: string; + proxyCommand?: string[]; }) { - return ( - `ssh ${ - opts.verbose ? '-vvv' : '' - } -t -o LogLevel=ERROR -o StrictHostKeyChecking=no ` + - `-o UserKnownHostsFile=/dev/null ` + - `${opts.proxyCommand != null ? opts.proxyCommand : ''} ` + - `${opts.port != null ? `-p ${opts.port}` : ''} ` + - `${opts.username}@ssh.${opts.proxyUrl} ${opts.command}` - ); + return [ + ...(opts.verbose ? ['-vvv'] : []), + '-t', + ...['-o', 'LogLevel=ERROR'], + ...['-o', 'StrictHostKeyChecking=no'], + ...['-o', 'UserKnownHostsFile=/dev/null'], + ...(opts.proxyCommand && opts.proxyCommand.length + ? ['-o', `ProxyCommand=${opts.proxyCommand.join(' ')}`] + : []), + ...(opts.port ? ['-p', opts.port.toString()] : []), + `${opts.username}@ssh.${opts.proxyUrl}`, + opts.command, + ]; } export const ssh: CommandDefinition< @@ -207,7 +209,9 @@ export const ssh: CommandDefinition< { signature: 'port', parameter: 'port', - description: 'SSH gateway port', + description: stripIndent` + SSH server port number (default 22222) if the target is an IP address or .local + hostname. Otherwise, port number for the balenaCloud gateway (default 22).`, alias: 'p', }, { @@ -219,24 +223,19 @@ export const ssh: CommandDefinition< { signature: 'noproxy', boolean: true, - description: stripIndent` - Don't use the proxy configuration for this connection. This flag - only make sense if you've configured a proxy globally.`, + description: 'Bypass global proxy configuration for the ssh connection', }, ], action: async (params, options) => { const applicationOrDevice = params.applicationOrDevice_raw || params.applicationOrDevice; - const bash = await import('bash'); - const { getProxyConfig, getSubShellCommand, which } = await import( + const { ExpectedError } = await import('../errors'); + const { getProxyConfig, which, whichSpawn } = await import( '../utils/helpers' ); - const { child_process } = await import('mz'); - const { - exitIfNotLoggedIn, - exitWithExpectedError, - getOnlineTargetUuid, - } = await import('../utils/patterns'); + const { checkLoggedIn, getOnlineTargetUuid } = await import( + '../utils/patterns' + ); const sdk = getBalenaSdk(); const verbose = options.verbose === true; @@ -259,34 +258,30 @@ export const ssh: CommandDefinition< } // this will be a tunnelled SSH connection... - await exitIfNotLoggedIn(); + await checkLoggedIn(); const uuid = await getOnlineTargetUuid(sdk, applicationOrDevice); let version: string | undefined; let id: number | undefined; - try { - const device = await sdk.models.device.get(uuid, { - $select: ['id', 'supervisor_version', 'is_online'], - }); - id = device.id; - version = device.supervisor_version; - } catch (e) { - if (e instanceof BalenaDeviceNotFound) { - exitWithExpectedError(`Could not find device: ${uuid}`); - } - } + const device = await sdk.models.device.get(uuid, { + $select: ['id', 'supervisor_version', 'is_online'], + }); + id = device.id; + version = device.supervisor_version; const [whichProxytunnel, username, proxyUrl] = await Promise.all([ useProxy ? which('proxytunnel', false) : undefined, sdk.auth.whoami(), + // note that `proxyUrl` refers to the balenaCloud "resin-proxy" + // service, currently "balena-devices.com", rather than some + // local proxy server URL sdk.settings.get('proxyUrl'), ]); const getSshProxyCommand = () => { - if (!useProxy) { - return ''; + if (!proxyConfig) { + return; } - if (!whichProxytunnel) { console.warn(stripIndent` Proxy is enabled but the \`proxytunnel\` binary cannot be found. @@ -295,27 +290,32 @@ export const ssh: CommandDefinition< for the \`ssh\` requests. Attempting the unproxied request for now.`); - return ''; + return; } - const p = proxyConfig!; - const tunnelOptions: Dictionary = { - proxy: `${p.host}:${p.port}`, - dest: '%h:%p', - }; + const p = proxyConfig; if (p.username && p.password) { - tunnelOptions.user = p.username; - tunnelOptions.pass = p.password; + // proxytunnel understands these variables for proxy authentication. + // Setting the variables instead of command-line options avoids the + // need for shell-specific escaping of special characters like '$'. + process.env.PROXYUSER = p.username; + process.env.PROXYPASS = p.password; } - const ProxyCommand = `proxytunnel ${bash.args(tunnelOptions, '--', '=')}`; - return `-o ${bash.args({ ProxyCommand }, '', '=')}`; + return [ + 'proxytunnel', + `--proxy=${p.host}:${p.port}`, + // ssh replaces these %h:%p variables in the ProxyCommand option + // https://linux.die.net/man/5/ssh_config + '--dest=%h:%p', + ...(verbose ? ['--verbose'] : []), + ]; }; - const proxyCommand = getSshProxyCommand(); + const proxyCommand = useProxy ? getSshProxyCommand() : undefined; if (username == null) { - exitWithExpectedError( + throw new ExpectedError( `Opening an SSH connection to a remote device requires you to be logged in.`, ); } @@ -356,9 +356,6 @@ export const ssh: CommandDefinition< username: username!, }); - const subShellCommand = getSubShellCommand(command); - await child_process.spawn(subShellCommand.program, subShellCommand.args, { - stdio: 'inherit', - }); + await whichSpawn('ssh', command); }, }; diff --git a/lib/utils/device/ssh.ts b/lib/utils/device/ssh.ts index 2621949b..9ed64fec 100644 --- a/lib/utils/device/ssh.ts +++ b/lib/utils/device/ssh.ts @@ -1,5 +1,5 @@ /* -Copyright 2016-2019 Balena +Copyright 2016-2020 Balena Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -27,12 +27,10 @@ export const deviceContainerEngineBinary = `$(if [ -f /usr/bin/balena ]; then ec export async function performLocalDeviceSSH( opts: DeviceSSHOpts, ): Promise { - const childProcess = await import('child_process'); const reduce = await import('lodash/reduce'); - const { getSubShellCommand } = await import('../helpers'); - const { exitWithExpectedError } = await import('../patterns'); + const { whichSpawn } = await import('../helpers'); + const { ExpectedError } = await import('../../errors'); const { stripIndent } = await import('common-tags'); - const os = await import('os'); let command = ''; @@ -57,10 +55,9 @@ export async function performLocalDeviceSSH( try { allContainers = await docker.listContainers(); } catch (_e) { - exitWithExpectedError(stripIndent` + throw new ExpectedError(stripIndent` Could not access docker daemon on device ${opts.address}. Please ensure the device is in local mode.`); - return; } const serviceNames: string[] = []; @@ -80,7 +77,7 @@ export async function performLocalDeviceSSH( .filter(c => c != null); if (containers.length === 0) { - exitWithExpectedError( + throw new ExpectedError( `Could not find a service on device with name ${opts.service}. ${ serviceNames.length > 0 ? `Available services:\n${reduce( @@ -93,36 +90,26 @@ export async function performLocalDeviceSSH( ); } if (containers.length > 1) { - exitWithExpectedError(stripIndent` + throw new ExpectedError(stripIndent` Found more than one container with a service name ${opts.service}. This state is not supported, please contact support. `); } - // Getting a command to work on all platforms is a pain, - // so we just define slightly different ones for windows - if (os.platform() !== 'win32') { - const shellCmd = `/bin/sh -c "if [ -e /bin/bash ]; then exec /bin/bash; else exec /bin/sh; fi"`; - command = `'${deviceContainerEngineBinary}' exec -ti ${ - containers[0]!.id - } '${shellCmd}'`; - } else { - const shellCmd = `/bin/sh -c "if [ -e /bin/bash ]; then exec /bin/bash; else exec /bin/sh; fi"`; - command = `${deviceContainerEngineBinary} exec -ti ${ - containers[0]!.id - } ${shellCmd}`; - } + const shellCmd = `/bin/sh -c "if [ -e /bin/bash ]; then exec /bin/bash; else exec /bin/sh; fi"`; + command = `${deviceContainerEngineBinary} exec -ti ${ + containers[0]!.id + } ${shellCmd}`; } - // Generate the SSH command - const sshCommand = `ssh \ - ${opts.verbose ? '-vvv' : ''} \ - -t \ - -p ${opts.port ? opts.port : 22222} \ - -o LogLevel=ERROR \ - -o StrictHostKeyChecking=no \ - -o UserKnownHostsFile=/dev/null \ - root@${opts.address} ${command}`; - const subShell = getSubShellCommand(sshCommand); - childProcess.spawn(subShell.program, subShell.args, { stdio: 'inherit' }); + return whichSpawn('ssh', [ + ...(opts.verbose ? ['-vvv'] : []), + '-t', + ...['-p', opts.port ? opts.port.toString() : '22222'], + ...['-o', 'LogLevel=ERROR'], + ...['-o', 'StrictHostKeyChecking=no'], + ...['-o', 'UserKnownHostsFile=/dev/null'], + `root@${opts.address}`, + ...(command ? [command] : []), + ]); } diff --git a/lib/utils/helpers.ts b/lib/utils/helpers.ts index d1d71f84..cb3ab9f2 100644 --- a/lib/utils/helpers.ts +++ b/lib/utils/helpers.ts @@ -16,9 +16,10 @@ limitations under the License. import { InitializeEmitter, OperationState } from 'balena-device-init'; import * as BalenaSdk from 'balena-sdk'; -import Bluebird = require('bluebird'); -import _ = require('lodash'); -import os = require('os'); +import * as Bluebird from 'bluebird'; +import { spawn, SpawnOptions } from 'child_process'; +import * as _ from 'lodash'; +import * as os from 'os'; import * as ShellEscape from 'shell-escape'; import { ExpectedError } from '../errors'; @@ -187,35 +188,6 @@ export function getApplication(applicationName: string) { return balena.models.application.get(applicationName, extraOptions); } -/** - * Choose between 'cmd.exe' and '/bin/sh' for running the given command string, - * depending on the value of `os.platform()`. - * When writing new code, consider whether it would be possible to avoid using a - * shell at all, using the which() function in this module to obtain a program's - * full path, executing the program directly and passing the arguments as an - * array instead of a long string. Avoiding a shell has several benefits: - * - Avoids the need to shell-escape arguments, especially nested commands. - * - Bypasses the incompatibilities between cmd.exe and /bin/sh. - * - Reduces the security risks of lax input validation. - * Code example avoiding a shell: - * const program = await which('ssh'); - * const args = ['root@192.168.1.1', 'cat /etc/os-release']; - * const child = spawn(program, args); - */ -export function getSubShellCommand(command: string) { - if (os.platform() === 'win32') { - return { - program: 'cmd.exe', - args: ['/s', '/c', command], - }; - } else { - return { - program: '/bin/sh', - args: ['-c', command], - }; - } -} - /** * Call `func`, and if func() throws an error or returns a promise that * eventually rejects, retry it `times` many times, each time printing a @@ -406,6 +378,40 @@ export async function which( return programPath; } +/** + * Call which(programName) and spawn() with the given arguments. + * Reject the promise if the process exit code is not zero. + */ +export async function whichSpawn( + programName: string, + args: string[], + options: SpawnOptions = { stdio: 'inherit' }, +): Promise { + const program = await which(programName); + if (process.env.DEBUG) { + console.error(`[debug] [${program}, ${args.join(', ')}]`); + } + let error: Error | undefined; + let exitCode: number | undefined; + try { + exitCode = await new Promise((resolve, reject) => { + spawn(program, args, options) + .on('error', reject) + .on('close', resolve); + }); + } catch (err) { + error = err; + } + if (error || exitCode) { + const msg = [ + `${programName} failed with exit code ${exitCode}:`, + `[${program}, ${args.join(', ')}]`, + ...(error ? [`${error}`] : []), + ]; + throw new Error(msg.join('\n')); + } +} + export interface ProxyConfig { host: string; port: string; diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 8478ebe1..5b6e46fa 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -2431,11 +2431,6 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" }, - "bash": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/bash/-/bash-0.0.1.tgz", - "integrity": "sha1-nuDnp1K8Xu8Wi8SHGVVqdFSKrgw=" - }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", diff --git a/package.json b/package.json index 3afdf48e..6bd65830 100644 --- a/package.json +++ b/package.json @@ -177,7 +177,6 @@ "balena-semver": "^2.2.0", "balena-settings-client": "^4.0.4", "balena-sync": "^10.2.0", - "bash": "0.0.1", "bluebird": "^3.7.2", "body-parser": "^1.19.0", "capitano": "^1.9.2", diff --git a/typings/bash/index.d.ts b/typings/bash/index.d.ts deleted file mode 100644 index 60e29952..00000000 --- a/typings/bash/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'bash'; From 30738d93b0cd3b63fc4de4b54bb8f2dbffc508a5 Mon Sep 17 00:00:00 2001 From: Paulo Castro Date: Mon, 30 Mar 2020 00:19:07 +0100 Subject: [PATCH 3/4] Fix "the input device is not a TTY" when piping to 'balena ssh' (local device) Change-type: patch --- lib/utils/device/ssh.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/utils/device/ssh.ts b/lib/utils/device/ssh.ts index 9ed64fec..ede77d9b 100644 --- a/lib/utils/device/ssh.ts +++ b/lib/utils/device/ssh.ts @@ -31,6 +31,7 @@ export async function performLocalDeviceSSH( const { whichSpawn } = await import('../helpers'); const { ExpectedError } = await import('../../errors'); const { stripIndent } = await import('common-tags'); + const { isatty } = await import('tty'); let command = ''; @@ -96,10 +97,14 @@ export async function performLocalDeviceSSH( `); } + const containerId = containers[0]!.id; const shellCmd = `/bin/sh -c "if [ -e /bin/bash ]; then exec /bin/bash; else exec /bin/sh; fi"`; - command = `${deviceContainerEngineBinary} exec -ti ${ - containers[0]!.id - } ${shellCmd}`; + // stdin (fd=0) is not a tty when data is piped in, for example + // echo 'ls -la; exit;' | balena ssh 192.168.0.20 service1 + // See https://www.balena.io/blog/balena-monthly-roundup-january-2020/#charliestipsntricks + // https://assets.balena.io/newsletter/2020-01/pipe.png + const ttyFlag = isatty(0) ? '-t' : ''; + command = `${deviceContainerEngineBinary} exec -i ${ttyFlag} ${containerId} ${shellCmd}`; } return whichSpawn('ssh', [ From 3e955f3a910a305fc89eacff57d3df9c65a6a2b4 Mon Sep 17 00:00:00 2001 From: Paulo Castro Date: Sun, 29 Mar 2020 23:59:44 +0100 Subject: [PATCH 4/4] Update README regarding proxy server support Change-type: patch --- README.md | 52 +++++++++++++++++++++++++----------------------- doc/cli.markdown | 52 +++++++++++++++++++++++++----------------------- 2 files changed, 54 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 26f8fb73..ad5c8534 100644 --- a/README.md +++ b/README.md @@ -88,21 +88,31 @@ HTTP(S) proxies can be configured through any of the following methods, in prece * The `HTTPS_PROXY` and/or `HTTP_PROXY` environment variables, in the same URL format as `BALENARC_PROXY`. -> Note: The `balena ssh` command has additional setup requirements to work behind a proxy. -> Check the [installation instructions](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md). +> Note: The `balena ssh` command has additional setup requirements to work behind a proxy. +> Check the [installation instructions](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md), +> and ensure that the proxy server is configured to allow proxy requests to ssh port 22, using +> SSL encryption. For example, in the case of the [Squid](http://www.squid-cache.org/) proxy +> server, it should be configured with the following rules in the `squid.conf` file: +> `acl SSL_ports port 22` +> `acl Safe_ports port 22` -Some installations of the balena CLI also include support for the `BALENARC_NO_PROXY` environment -variable, which allows proxy exclusion patterns to be defined. The current support status is listed -below. Eventually, all installation types will have support for it. +#### Proxy exclusion -OS | Installation type | BALENARC_NO_PROXY environment variable support --- | ----------------- | ---------------------------------------------- -Windows | standalone zip | Supported with CLI v11.24.0 and later -Windows | native/GUI | Not supported -macOS | standalone zip | Not supported -macOS | native/GUI | Supported with CLI v11.24.0 and later -Linux | standalone zip | Not supported -Any | npm | Supported with Node.js >= v10.16.0 and CLI >= v11.24.0 +The `BALENARC_NO_PROXY` variable may be used to exclude specified destinations from proxying. + +> * This feature requires balena CLI version 11.30.8 or later. In the case of the npm [installation +> option](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md), it also requires +> Node.js version 10.16.0 or later. +> * To exclude a `balena ssh` target from proxying (IP address or `.local` hostname), the +> `--noproxy` option should be specified in addition to the `BALENARC_NO_PROXY` variable. + +By default (if `BALENARC_NO_PROXY` is not defined), all [private IPv4 +addresses](https://en.wikipedia.org/wiki/Private_network) and `'*.local'` hostnames are excluded +from proxying. Other hostnames that resolve to private IPv4 addresses are **not** excluded by +default, because matching takes place before name resolution. + +`localhost` and `127.0.0.1` are always excluded from proxying, regardless of the value of +BALENARC_NO_PROXY. The format of the `BALENARC_NO_PROXY` environment variable is a comma-separated list of patterns that are matched against hostnames or IP addresses. For example: @@ -111,18 +121,10 @@ that are matched against hostnames or IP addresses. For example: export BALENARC_NO_PROXY='*.local,dev*.mycompany.com,192.168.*' ``` -Matched patterns are excluded from proxying. Matching takes place _before_ name resolution, so a -pattern like `'192.168.*'` will **not** match a hostname like `proxy.company.com` even if the -hostname resolves to an IP address like `192.168.1.2`. Pattern matching expressions are documented -at [matcher](https://www.npmjs.com/package/matcher#usage). - -By default, if BALENARC_NO_PROXY is not defined, all [private IPv4 -addresses](https://en.wikipedia.org/wiki/Private_network) and `'*.local'` are excluded from -proxying. Other hostnames that may resolve to private IPv4 addresses are **not** excluded by -default, as matching takes place _before_ name resolution. In addition, `localhost` and `127.0.0.1` -are always excluded from proxying, regardless of the value of BALENARC_NO_PROXY. These default -exclusions only apply to the CLI installations where BALENARC_NO_PROXY is supported, as listed in -the table above. +Matched patterns are excluded from proxying. Wildcard expressions are documented at +[matcher](https://www.npmjs.com/package/matcher#usage). Matching takes place _before_ name +resolution, so a pattern like `'192.168.*'` will **not** match a hostname that resolves to an IP +address like `192.168.1.2`. ## Command reference documentation diff --git a/doc/cli.markdown b/doc/cli.markdown index f36ce69c..217e1132 100644 --- a/doc/cli.markdown +++ b/doc/cli.markdown @@ -81,21 +81,31 @@ HTTP(S) proxies can be configured through any of the following methods, in prece * The `HTTPS_PROXY` and/or `HTTP_PROXY` environment variables, in the same URL format as `BALENARC_PROXY`. -> Note: The `balena ssh` command has additional setup requirements to work behind a proxy. -> Check the [installation instructions](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md). +> Note: The `balena ssh` command has additional setup requirements to work behind a proxy. +> Check the [installation instructions](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md), +> and ensure that the proxy server is configured to allow proxy requests to ssh port 22, using +> SSL encryption. For example, in the case of the [Squid](http://www.squid-cache.org/) proxy +> server, it should be configured with the following rules in the `squid.conf` file: +> `acl SSL_ports port 22` +> `acl Safe_ports port 22` -Some installations of the balena CLI also include support for the `BALENARC_NO_PROXY` environment -variable, which allows proxy exclusion patterns to be defined. The current support status is listed -below. Eventually, all installation types will have support for it. +#### Proxy exclusion -OS | Installation type | BALENARC_NO_PROXY environment variable support --- | ----------------- | ---------------------------------------------- -Windows | standalone zip | Supported with CLI v11.24.0 and later -Windows | native/GUI | Not supported -macOS | standalone zip | Not supported -macOS | native/GUI | Supported with CLI v11.24.0 and later -Linux | standalone zip | Not supported -Any | npm | Supported with Node.js >= v10.16.0 and CLI >= v11.24.0 +The `BALENARC_NO_PROXY` variable may be used to exclude specified destinations from proxying. + +> * This feature requires balena CLI version 11.30.8 or later. In the case of the npm [installation +> option](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md), it also requires +> Node.js version 10.16.0 or later. +> * To exclude a `balena ssh` target from proxying (IP address or `.local` hostname), the +> `--noproxy` option should be specified in addition to the `BALENARC_NO_PROXY` variable. + +By default (if `BALENARC_NO_PROXY` is not defined), all [private IPv4 +addresses](https://en.wikipedia.org/wiki/Private_network) and `'*.local'` hostnames are excluded +from proxying. Other hostnames that resolve to private IPv4 addresses are **not** excluded by +default, because matching takes place before name resolution. + +`localhost` and `127.0.0.1` are always excluded from proxying, regardless of the value of +BALENARC_NO_PROXY. The format of the `BALENARC_NO_PROXY` environment variable is a comma-separated list of patterns that are matched against hostnames or IP addresses. For example: @@ -104,18 +114,10 @@ that are matched against hostnames or IP addresses. For example: export BALENARC_NO_PROXY='*.local,dev*.mycompany.com,192.168.*' ``` -Matched patterns are excluded from proxying. Matching takes place _before_ name resolution, so a -pattern like `'192.168.*'` will **not** match a hostname like `proxy.company.com` even if the -hostname resolves to an IP address like `192.168.1.2`. Pattern matching expressions are documented -at [matcher](https://www.npmjs.com/package/matcher#usage). - -By default, if BALENARC_NO_PROXY is not defined, all [private IPv4 -addresses](https://en.wikipedia.org/wiki/Private_network) and `'*.local'` are excluded from -proxying. Other hostnames that may resolve to private IPv4 addresses are **not** excluded by -default, as matching takes place _before_ name resolution. In addition, `localhost` and `127.0.0.1` -are always excluded from proxying, regardless of the value of BALENARC_NO_PROXY. These default -exclusions only apply to the CLI installations where BALENARC_NO_PROXY is supported, as listed in -the table above. +Matched patterns are excluded from proxying. Wildcard expressions are documented at +[matcher](https://www.npmjs.com/package/matcher#usage). Matching takes place _before_ name +resolution, so a pattern like `'192.168.*'` will **not** match a hostname that resolves to an IP +address like `192.168.1.2`. ## Support, FAQ and troubleshooting