├── .gitignore ├── LICENSE ├── docs ├── workflow.rst └── annotated_config.rst ├── README.rst └── cmc /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Timid Robot Zehta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /docs/workflow.rst: -------------------------------------------------------------------------------- 1 | ******** 2 | Workflow 3 | ******** 4 | 5 | SSH is a powerful tool. When configured correctly it should not only provide 6 | excellent security, it should also make your work *easier* and 7 | *more convenient*. 8 | 9 | 10 | Assumptions 11 | =========== 12 | 1. You're using Mac OS X 13 | 14 | - This is not a requirement. The ``cmc`` script should work on any \*nix. 15 | 16 | 2. You have already created a SSH key pair and added the secret key to your 17 | Mac keychain. 18 | 19 | - Test to see currently available keys: 20 | 21 | A. ``ssh-add -L`` 22 | 23 | - SSH key pair creation example: 24 | 25 | A. ``ssh-keygen -b 4096 -C USERNAME@COMPUTER_DESC`` 26 | B. ``ssh-add -K`` 27 | 28 | 3. Your name is Arthur Dent 29 | 30 | - Your username on your Mac laptop is ``arthurdent`` 31 | - Your username on remote systems is ``arthur`` 32 | 33 | 4. Only the ``~/.ssh/config`` on your laptop will ever need to be edited. 34 | 35 | 5. Three hypothetical hosts (see below) 36 | 37 | 38 | Example ``~/.ssh/config`` Configuration 39 | --------------------------------------- 40 | 41 | :: 42 | 43 | # insecure 44 | Host insecure insecure.example.com 45 | HostName insecure.example.com 46 | 47 | # bastion 48 | Host bastion bastion.example.com 49 | HostName bastion.example.com 50 | ForwardAgent yes 51 | ControlMaster auto 52 | 53 | # production 54 | Host prod production prod*.example.com 55 | HostName production.example.com 56 | ForwardAgent yes 57 | ProxyCommand ssh -q bastion nc -w30 %h %p 58 | 59 | # global defaults 60 | Host * 61 | ControlPath ~/.ssh/master-%r@%h:%p 62 | ServerAliveCountMax 6 63 | ServerAliveInterval 10 64 | User arthur 65 | 66 | 67 | Please also see the `annotated ssh configuration example`_ with explanations. 68 | 69 | .. _`annotated ssh configuration example`: annotated_config.rst 70 | 71 | 72 | Workflow 73 | ======== 74 | 75 | 1. Establish control sessions at the start of your day/session/etc. 76 | 77 | - ``cmc -o bastion`` or ``cmc -O`` 78 | - This establishes a control master connection in the background. It will 79 | stay connected and available until it is closed or connectivity is lost. 80 | 81 | 2. Connect "directly" to hosts behind the firewall using bastion as a proxy 82 | 83 | - ``ssh prod`` 84 | - Uses the connection already in place when it proxies through bastion! 85 | 86 | 87 | References 88 | ========== 89 | 90 | - `ssh_config(5) OS X Manual Page`_ 91 | - `Ubuntu Manpage: ssh_config`_ 92 | 93 | .. _`ssh_config(5) OS X Manual Page`: 94 | https://developer.apple.com/library/mac/#documentation/Darwin/Reference/ManPages/man5/ssh_config.5.html 95 | .. _`Ubuntu Manpage: ssh_config`: 96 | http://manpages.ubuntu.com/manpages/precise/man5/ssh_config.5.html 97 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | cmc 2 | === 3 | 4 | ``cmc`` makes SSH ControlMaster sessions easy. SSH ControlMaster sessions have 5 | the following benefits: 6 | 7 | - When using a SSH Bastion, you will only be prompted for (two-factor) 8 | authentication once. 9 | 10 | - Hosts behind the SSH Bastion can be accessed "directly" by proxying through 11 | the SSH Bastion (see workflow_). 12 | 13 | - Sessions to or through ControlMaster hosts do not need to create a new 14 | connection (SSH will be faster for most tasks) 15 | 16 | 17 | Script Help 18 | ----------- 19 | 20 | :: 21 | 22 | Usage: cmc [ -c HOST | -o HOST | -x HOST ] 23 | cmc [ -L | -l | -O | -X ] 24 | cmc -h 25 | 26 | ControlMaster Controller - Eases management of SSH ControlMaster connections 27 | 28 | Options: 29 | -h show this help message and exit 30 | -c HOST check HOST ControlMaster connection status (maybe specified more 31 | than once) 32 | -L list ControlMasters defined in SSH_CONFIG 33 | -l list ControlMaster connection sockets matching ControlPath and 34 | check their connection status 35 | -O open all ControlMasters defined in SSH_CONFIG 36 | -o HOST open a ControlMaster session (maybe specified more than once) 37 | -x HOST exit ControlMaster session (maybe specified more than once) 38 | -X exit all ControlMaster connections with sockets matching 39 | ControlPath 40 | 41 | Notes: 42 | • Any unused sockets in ControlPath are removed with -l and -X 43 | • Only a single ControlPath should be specified 44 | 45 | 46 | Install 47 | ======= 48 | 49 | 1. `Install Homebrew`_ — The missing package manager for macOS 50 | 2. Add the cmc "tap" and install cmc:: 51 | 52 | brew tap ClockworkNet/cmc 53 | brew install cmc 54 | 55 | Alternatively, since ``cmc`` is a bash script without esoteric dependencies, 56 | you can simply download it and ensure it is in your ``PATH``. 57 | 58 | If this utility is helpful for you, please star this project so that it can 59 | eventually be included in Homebrew proper. Thank you! 60 | 61 | .. _`Install Homebrew`: http://brew.sh/#install 62 | 63 | 64 | Documentation 65 | ============= 66 | 67 | 68 | ControlPersist 69 | -------------- 70 | 71 | **Note:** This utility is largely deprecated by the addition of ControlPersist 72 | in `OpenSSH 5.6`_. It allows ControlMaster sessions to be configured to timeout 73 | instead of relying on a utility like ``cmc`` to manage them. See 74 | `ssh_config(5)`_ for more details. 75 | 76 | .. _`OpenSSH 5.6`: https://www.openssh.com/txt/release-5.6 77 | .. _`ssh_config(5)`: http://man.openbsd.org/OpenBSD-current/man5/ssh_config.5 78 | 79 | 80 | Workflow 81 | -------- 82 | 83 | **See** |workflow|_. 84 | 85 | .. |workflow| replace:: **docs/workflow.rst** 86 | .. _workflow: docs/workflow.rst 87 | 88 | 89 | Annotated SSH Configuration 90 | --------------------------- 91 | 92 | **See** |annotated_config|_. 93 | 94 | .. |annotated_config| replace:: **docs/annotated_config.rst** 95 | .. _annotated_config: docs/annotated_config.rst 96 | 97 | 98 | Related 99 | ======= 100 | 101 | - mac-ssh-confirm_: Protect against SSH Agent Hijacking on macOS with the 102 | ability to confirm agent identities prior to each use 103 | - gacli_: Mac CLI Google Authenticator client (ex. for use with SSH Bastions 104 | that utilize Google Authenticator) 105 | 106 | .. _mac-ssh-confirm: https://github.com/TimZehta/mac-ssh-confirm 107 | .. _gacli: https://github.com/ClockworkNet/gacli 108 | 109 | 110 | Requirements 111 | ============ 112 | 113 | - \*nix Operating System with 114 | 115 | - core utilities (``awk``, ``find``, ``grep``, ``ps``, and ``sed``) 116 | - GNU Bourne-Again Shell 3.0 or later (``bash``) 117 | - OpenSSH 4.0 or later (``ssh``) 118 | 119 | 120 | Supported By 121 | ============ 122 | 123 | Development of this project has been supported by `Clockwork`_ 124 | (`ClockworkNet`_). Thank you! 125 | 126 | .. _Clockwork: https://www.clockwork.com/ 127 | .. _ClockworkNet: https://github.com/ClockworkNet 128 | 129 | 130 | License 131 | ======= 132 | 133 | .. image:: https://img.shields.io/github/license/ClockworkNet/cmc.svg 134 | :alt: badge: GitHub license (MIT) 135 | :align: right 136 | :target: `MIT License`_ 137 | - ``_ (Expat License/`MIT License`_) 138 | 139 | .. _`MIT License`: http://www.opensource.org/licenses/MIT 140 | -------------------------------------------------------------------------------- /docs/annotated_config.rst: -------------------------------------------------------------------------------- 1 | *************************** 2 | Annotated SSH Configuration 3 | *************************** 4 | 5 | 6 | Example SSH Config 7 | ================== 8 | 9 | :: 10 | 11 | # insecure 12 | Host insecure insecure.example.com 13 | HostName insecure.example.com 14 | 15 | # bastion 16 | Host bastion bastion.example.com 17 | HostName bastion.example.com 18 | ForwardAgent yes 19 | ControlMaster auto 20 | 21 | # production 22 | Host prod production prod*.example.com 23 | HostName production.example.com 24 | ForwardAgent yes 25 | ProxyCommand ssh -q bastion nc -w30 %h %p 26 | 27 | # global defaults 28 | Host * 29 | ControlPath ~/.ssh/master-%r@%h:%p 30 | ServerAliveCountMax 18 31 | ServerAliveInterval 5 32 | TCPKeepAlive no 33 | User arthur 34 | 35 | 36 | Section: # insecure 37 | ------------------- 38 | 39 | This section is for a server on the Internet that we think is insecure (we 40 | do not trust the administrators--those with root access). :: 41 | 42 | # insecure 43 | Host insecure insecure.example.com 44 | HostName insecure.example.com 45 | 46 | 1. ``# insecure`` is a comment. It helps provide context for for the 47 | line that follows it. 48 | 2. ``Host insecure insecure.example.com`` indicates the host patterns that the 49 | subsequent parameters apply to. All of the following will work to connect 50 | to the configured HostName: 51 | 52 | - ``ssh insecure`` 53 | - ``ssh insecure.example.com`` 54 | 55 | 3. ``HostName insecure.example.com`` specifies the real host name to log into. 56 | 57 | Additionally, the following defaults are important. The parameter is not in 58 | this section because the default value is appropriate. It should be 59 | acknowledged so that it is not unintentionally superseded by a configured 60 | parameter: 61 | 62 | 4. ``ForwardAgent no`` specifies that the authentication agent will **not** be 63 | forwarded. This prevents administrators on untrusted remote servers from 64 | masquerading as you on *any* system on which you have your SSH public key. 65 | See `SSH Agent Hijacking`_ for more information. 66 | 67 | 68 | Section: # bastion 69 | ------------------ 70 | 71 | This section is for a server on the Internet that acts as a SSH bastion. It 72 | provides access to servers behind a firewall. :: 73 | 74 | # bastion 75 | Host bastion bastion.example.com 76 | HostName bastion.example.com 77 | ControlMaster auto 78 | ControlPath ~/.ssh/master-%r@%h:%p 79 | 80 | 1. ``# bastion`` is a comment. It helps provide context for for the 81 | line that follows it. 82 | 2. ``Host bastion bastion.example.com`` indicates the host patterns that the 83 | subsequent paramters apply to. All of the following will work to connect 84 | to the configured HostName: 85 | 86 | - ``ssh bastion`` 87 | - ``ssh bastion.example.com`` 88 | 89 | 3. ``HostName bastion.example.com`` specifies the real host name to log into. 90 | 4. ``ForwardAgent yes`` specifies that the authentication agent will be 91 | forwarded to the remote server. 92 | 93 | - This is important for the bastion server as it allows public key sessions 94 | from the bastion to other servers (especially those behind the firewall). 95 | This means you will be able to connect to those servers without a 96 | password. 97 | 98 | 5. ``ControlMaster auto`` indicates SSH should listen for connections on a 99 | control socket. Additional sessions can connect to this socket and reuse 100 | the master instances (bastion's) network connection rather than initiating 101 | a new one. 102 | 103 | 104 | Section: # production 105 | --------------------- 106 | 107 | This section is for a server on the Internet that acts as a SSH production. It 108 | provides access to servers behind a firewall. 109 | 110 | :: 111 | 112 | # production 113 | Host prod production prod*.example.com 114 | HostName production.example.com 115 | ForwardAgent yes 116 | ProxyCommand ssh -q bastion nc -w30 %h %p 117 | 118 | 1. ``# production`` is a comment. It helps provide context for for the 119 | line that follows it. 120 | 2. ``Host prod production prod*.example.com`` indicates the host patterns that 121 | the subsequent parameters apply to. All of the following will work to connect 122 | to the configured HostName: 123 | 124 | - ``ssh prod`` 125 | - ``ssh production`` 126 | - ``ssh prod.example.com`` 127 | - ``ssh production.example.com`` 128 | 129 | 3. ``HostName production.example.com`` specifies the real host name to log into. 130 | 4. ``ForwardAgent yes`` specifies that the authentication agent will be 131 | forwarded to the remote server. 132 | 133 | - This is important for the production server as it allows public key 134 | sessions from the production server to other servers (especially source 135 | code repository servers). 136 | 137 | 5. ``ProxyCommand ssh -q bastion nc -w30 %h %p`` specifies the command to use 138 | to connect to the server. 139 | 140 | - This allows the connections to servers behind the firewall using the 141 | bastion server as a proxy. Any SSH client (ex. ssh command line, svn, 142 | Transmit) will see the production session as a single connection. It 143 | just works! 144 | 145 | 146 | Section: # global defaults 147 | -------------------------- 148 | 149 | The global defaults for all hosts is specified last. Its parameters apply if 150 | they are not previously defined (which is why it should be the *last* section 151 | of your SSH config). :: 152 | 153 | # global defaults 154 | Host * 155 | ForwardAgent no 156 | ServerAliveCountMax 18 157 | ServerAliveInterval 5 158 | TCPKeepAlive no 159 | User arthur 160 | 161 | 1. ``# global defaults`` is a comment. It helps provide context for for the 162 | line that follows it. 163 | 2. ``Host *`` indicates this is the global defaults section. 164 | 3. ``ControlPath ~/.ssh/master-%r@%h:%p`` supports the ControlMaster parameter. 165 | See `ssh_config(5) OS X Manual Page`_ if you are really curious. 166 | 4. ``ServerAliveCountMax 18`` helps ensure robust proxied sessions. See 167 | `ssh_config(5) OS X Manual Page`_ if you are really curious. 168 | 5. ``ServerAliveInterval 5`` helps ensure robust proxied sessions. See 169 | `ssh_config(5) OS X Manual Page`_ if you are really curious. 170 | 6. ``TCPKeepAlive no`` allows connections to weather short network outages 171 | (especially useful when connected via WiFi). 172 | 7. ``User arthur`` specifies the user to log in as (remember, in our example 173 | the local username is arthurdent). 174 | 175 | Additionally, the following defaults are important. The parameter is not in 176 | this section because the default value is appropriate. It should be 177 | acknowledged so that it is not unintentionally superseded by a configured 178 | parameter: 179 | 180 | 7. ``ForwardAgent no`` specifies that the authentication agent will **not** be 181 | forwarded. This prevents administrators on untrusted remote servers from 182 | masquerading as you on *any* system on which you have your SSH public key. 183 | See `SSH Agent Hijacking`_ for more information. 184 | 185 | 186 | References 187 | ========== 188 | 189 | - `ssh_config(5) OS X Manual Page`_ 190 | - `Ubuntu Manpage: ssh_config`_ 191 | - `SSH Agent Hijacking`_ 192 | 193 | .. _`ssh_config(5) OS X Manual Page`: 194 | https://developer.apple.com/library/mac/#documentation/Darwin/Reference/ManPages/man5/ssh_config.5.html 195 | .. _`Ubuntu Manpage: ssh_config`: 196 | http://manpages.ubuntu.com/manpages/trusty/man5/ssh_config.5.html 197 | .. _SSH Agent Hijacking: 198 | http://www.clockwork.net/blog/2012/09/28/602/ssh_agent_hijacking 199 | -------------------------------------------------------------------------------- /cmc: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # ControlMaster Controller - Eases management of SSH ControlMaster connections 4 | # 5 | # Notes 6 | # • `kill -sigalrm SESSION_PID` will result in SSH leaving a stale socket 7 | # 8 | #### SETUP #################################################################### 9 | set -o errexit 10 | set -o errtrace 11 | set -o nounset 12 | 13 | #### Variables 14 | prog="${0##*/}" 15 | ssh_config=~/.ssh/config 16 | usage="\ 17 | Usage: ${prog} [ -c HOST | -o HOST | -x HOST ] 18 | ${prog} [ -L | -l | -O | -X ] 19 | ${prog} -h 20 | 21 | ControlMaster Controller - Eases management of SSH ControlMaster connections 22 | 23 | Options: 24 | -h show this help message and exit 25 | -c HOST check HOST ControlMaster connection status (may be specified more 26 | than once) 27 | -L list ControlMasters defined in ${ssh_config} 28 | -l list ControlMaster connection sockets matching ControlPath and 29 | check their connection status 30 | -O open all ControlMasters defined in ${ssh_config} 31 | -o HOST open a ControlMaster session (may be specified more than once) 32 | -x HOST exit ControlMaster session (may be specified more than once) 33 | -X exit all ControlMaster connections with sockets matching 34 | ControlPath 35 | 36 | Notes: 37 | • Any unused sockets in ControlPath are removed with -l and -X 38 | • Only a single ControlPath should be specified 39 | " 40 | syslog_linux=/var/log/syslog 41 | syslog_darwin=/private/var/log/system.log 42 | if [[ -f ${syslog_linux} ]] 43 | then 44 | syslog=${syslog_linux} 45 | log_opt='-y ' 46 | elif [[ -f ${syslog_darwin} ]] 47 | then 48 | syslog=${syslog_darwin} 49 | log_opt='-y ' 50 | fi 51 | 52 | 53 | #### FUNCTIONS ################################################################ 54 | 55 | 56 | action_check_set() { 57 | # check action and set provided there is no conflict 58 | local _action _msg 59 | _action=${1} 60 | if [[ -n "${action:-}" ]] && [[ "${action}" != "${_action}" ]] 61 | then 62 | _msg="more than one options ('-${Option}' and '-${opt:-}')" 63 | _msg="${_msg} specified" 64 | error_exit "${_msg}" 2 65 | else 66 | action=${_action} 67 | opt=${Option} 68 | fi 69 | return 0 70 | } 71 | 72 | 73 | config_hosts_list() { 74 | # Extract ControlMaster hosts from SSH config 75 | local _result=$(awk \ 76 | 'tolower($1) ~ /^host$/ { 77 | host=$2; next } 78 | tolower($0) ~ /^[[:space:]]*controlmaster[[:space:]]+(yes|auto)/ { 79 | print host; next } 80 | ' ${ssh_config}) 81 | if [[ -n "${_result}" ]] 82 | then 83 | echo "${_result}" 84 | return 0 85 | else 86 | error_exit "No ControlMaster hosts defined in '${ssh_config}'" 87 | fi 88 | } 89 | 90 | 91 | config_control_path() { 92 | # Extract ControlPath from SSH config 93 | local _result _result_count 94 | _result=$(awk \ 95 | 'tolower($1) ~ /^controlpath$/ { 96 | if ( tolower($2) !~ /^none$/ ) { 97 | print $2 98 | } 99 | }' ${ssh_config}) 100 | if [[ -n "${_result}" ]] 101 | then 102 | # Only a single ControlPath should be specified 103 | _result_count=$(echo "${_result}" | sort -u | wc -l) 104 | if (( _result_count > 1 )) 105 | then 106 | error_exit "Multiple ControlPaths specified: ${_result}" 107 | fi 108 | # Create Path glob from ControlPath 109 | echo ${_result} 110 | return 0 111 | else 112 | error_exit "No ControlPath defined in '${ssh_config}'" 113 | fi 114 | } 115 | 116 | 117 | error_exit() { 118 | # Display error message and exit 119 | local _es _msg 120 | _msg=${1} 121 | _es=${2:-1} 122 | echo "ERROR: ${_msg}" 1>&2 123 | exit ${_es} 124 | } 125 | 126 | 127 | help_print() { 128 | # Print help/usage, then exit (incorrect usage should exit 2) 129 | local _es=${1:-0} 130 | echo "${usage}" 131 | exit ${_es} 132 | } 133 | 134 | 135 | help_request_check() { 136 | # Print Help/Usage if requested 137 | local _arg 138 | shopt -s nocasematch 139 | # only accept help "action" in 1st position 140 | [[ "${1:-}" == 'help' ]] && help_print 141 | # evaulate all positional parameters for help options 142 | for _arg in "${@}" 143 | do 144 | case "${_arg}" in 145 | -h | -help | --help ) help_print ;; 146 | esac 147 | done 148 | shopt -u nocasematch 149 | return 0 150 | } 151 | 152 | 153 | host_from_socket() { 154 | # extract host from socket name 155 | local _host _hostname _socket 156 | _socket="${1}" 157 | if [[ "${_socket}" =~ ${host_regex} ]] 158 | then 159 | _hostname="${BASH_REMATCH[1]}" 160 | _host="$(awk \ 161 | "tolower(\$0) ~ /^[[:space:]]*host[[:space:]]+/ { 162 | host=\$2 163 | } 164 | tolower(\$0) ~ /^[[:space:]]*hostname[[:space:]]+${_hostname}/ { 165 | print host 166 | }" ${ssh_config})" 167 | if [[ -z "${_host}" ]] 168 | then 169 | _host="${_hostname}" 170 | fi 171 | echo "${_host}" 172 | else 173 | { 174 | echo "WARNING: cannot match host in socket: ${_socket}" 175 | echo " regex pattern: ${host_regex}" 176 | } 1>&2 177 | echo 'HOSTNAME' 178 | fi 179 | return 0 180 | } 181 | 182 | 183 | strip_clean() { 184 | # Strip 185 | # - all control characters 186 | # - leading whitespace 187 | # - trailing whitespace 188 | echo "${1}" | sed \ 189 | -e's/[[:cntrl:]]*//g' \ 190 | -e's/^[[:space:]]*//' \ 191 | -e's/[[:space:]]*$//' 192 | } 193 | 194 | 195 | session_check() { 196 | # Check ControlMaster connection status 197 | local _cmd _es _host _opt_socket _pid _result _socket _start 198 | _host=${1} 199 | _opt_socket='' 200 | _socket=${2:-} 201 | [[ -n "${_socket}" ]] && _opt_socket="-S ${_socket}" 202 | echo "${_host} - Checking ControlMaster connection status" 203 | _result=$(ssh ${_opt_socket} -O check ${_host} 2>&1 || exit ${?}) \ 204 | || _es=${?} 205 | _result=$(strip_clean "${_result}") 206 | if (( ${_es:-0} != 0 )) 207 | then 208 | echo " ${_result}" 209 | return ${_es} 210 | fi 211 | _pid=$(echo "${_result}" | sed -e 's/[^0-9]*//g') 212 | _cmd=$(ps -p ${_pid} -o command=) 213 | _cmd=$(strip_clean "${_cmd}") 214 | _start=$(ps -p ${_pid} -o start=) 215 | _start=${_start# } 216 | _result=${_result%)} 217 | echo " ${_result}, cmd=${_cmd}, start=${_start})" 218 | return 0 219 | } 220 | 221 | 222 | session_exit() { 223 | # Exit an existing ControlMaster process 224 | local _es _host _opt_socket _result _socket 225 | _host=${1} 226 | _opt_socket='' 227 | _socket=${2:-} 228 | [[ -n "${_socket}" ]] && _opt_socket="-S ${_socket}" 229 | echo "${_host} - Closing ControlMaster connection" 230 | _result=$(ssh ${_opt_socket} -O exit ${_host} 2>&1 || exit ${?}) \ 231 | || _es=${?} 232 | _result=$(strip_clean "${_result}") 233 | echo " ${_result}" 234 | return ${_es:-0} 235 | } 236 | 237 | 238 | session_exit_all() { 239 | # Exit ALL ControlMaster with connection sockets in ~/.ssh/ 240 | local _es _host _i _socket _sockets 241 | _sockets=$(sockets_list) 242 | if [[ -z "${_sockets}" ]] 243 | then 244 | error_exit 'No ControlMaster connection sharing sockets found' 245 | else 246 | _i=0 247 | for _socket in ${_sockets} 248 | do 249 | (( _i > 0 )) && echo 250 | _host=$(host_from_socket "${_socket}") 251 | if session_check ${_host} "${_socket}" &> /dev/null 252 | then 253 | session_exit ${_host} "${_socket}" 254 | else 255 | _es=4 256 | socket_clean ${_socket} 257 | fi 258 | _i=$(( ++_i )) 259 | done 260 | fi 261 | return ${_es:-0} 262 | } 263 | 264 | 265 | session_open() { 266 | # Open a new ControlMaster 267 | local _error _es _host 268 | _host=${1} 269 | if session_check ${_host} &> /dev/null 270 | then 271 | { 272 | echo "${_host} - ControlMaster connection already open" 273 | session_check ${_host} | tail -n 1 274 | } 1>&2 275 | else 276 | echo "${_host} - Opening ControlMaster connection" 277 | # ssh -f option does not work in subshell or list :( 278 | # ssh -y option used (if in ${log_opt}) so that misc connection errors 279 | # do not clutter terminals 280 | if ssh -f -N ${log_opt:-}${_host} 281 | then 282 | session_check ${_host} | tail -n 1 283 | else 284 | _es=${?} 285 | if [[ -n "${syslog:-}" ]] 286 | then 287 | _error=$(grep -F ' ssh: ' ${syslog} | tail -n 1) 288 | echo "ERROR: ${_error#*error:}" 1>&2 289 | fi 290 | return ${_es} 291 | fi 292 | fi 293 | return 0 294 | } 295 | 296 | 297 | session_open_all() { 298 | # open ALL ControlMaster hosts from SSH config 299 | local _host _i 300 | _i=0 301 | for _host in ${configured_hosts} 302 | do 303 | (( _i > 0 )) && echo 304 | session_open ${_host} || true 305 | _i=$(( ++_i )) 306 | done 307 | return 0 308 | } 309 | 310 | 311 | socket_clean() { 312 | # Remove stale socket 313 | local _socket="${1}" 314 | { 315 | echo 'WARNING: Stale ControlMaster connection socket' 316 | echo " Removing: ${_socket}" 317 | rm -f "${_socket}" 318 | } 1>&2 319 | return 0 320 | } 321 | 322 | 323 | sockets_display() { 324 | # List ControlMaster connections sockets found in ~/.ssh 325 | local _check _es _first _host _socket _sockets 326 | # find sockets 327 | _sockets=$(sockets_list) 328 | if [[ -z "${_sockets}" ]] 329 | then 330 | echo 'No ControlMaster connection sockets found.' 1>&2 331 | exit 0 332 | else 333 | _i=0 334 | for _socket in ${_sockets} 335 | do 336 | _host=$(host_from_socket "${_socket}") 337 | if session_check "${_host}" "${_socket}" &> /dev/null 338 | then 339 | if (( _i > 0 )) 340 | then 341 | echo 342 | fi 343 | _check=$(session_check "${_host}" "${_socket}" | tail -n 1) 344 | echo "${_host}" 345 | echo "${_check}" 346 | echo " Socket: ${_socket}" 347 | else 348 | socket_clean ${_socket} 349 | _es=3 350 | fi 351 | _i=$(( _i + 1 )) 352 | done 353 | fi 354 | exit ${_es:-0} 355 | } 356 | 357 | 358 | sockets_list() { 359 | # list ControlMaster connection sockets in ControlPath 360 | local _socket 361 | for _socket in $(eval echo ${control_glob}) 362 | do 363 | [[ -S "${_socket}" ]] || continue 364 | echo "${_socket}" 365 | done 366 | return 0 367 | } 368 | 369 | 370 | #### MAIN ##################################################################### 371 | 372 | 373 | #### Parse options 374 | help_request_check "${@:-}" 375 | (( ${#} == 0 )) && help_print 2 376 | while getopts ':Ac:LlOo:Xx:' Option 377 | do 378 | case ${Option} in 379 | c ) action_check_set 'check' 380 | hosts="${hosts:-} ${OPTARG}" 381 | ;; 382 | L ) action_check_set 'conf_list' ;; 383 | l ) action_check_set 'sockets' ;; 384 | O | A ) action_check_set 'open_all' ;; 385 | o ) action_check_set 'open' 386 | hosts="${hosts:-} ${OPTARG}" 387 | ;; 388 | X ) action_check_set 'exit_all' ;; 389 | x ) action_check_set 'exit' 390 | hosts="${hosts:-} ${OPTARG}" 391 | ;; 392 | * ) 393 | if [[ "${OPTARG}" =~ ^[cox]$ ]] 394 | then 395 | error_exit "option '-${OPTARG}' requires HOST argument" 2 396 | fi 397 | error_exit "Invalid option: -${OPTARG}" 2 398 | ;; 399 | esac 400 | done 401 | shift $((${OPTIND} - 1)) 402 | 403 | configured_hosts=$(config_hosts_list) 404 | control_path="$(config_control_path)" 405 | control_glob=${control_path//%?/*} 406 | host_regex=${control_path//%h/(.*)} 407 | host_regex=${host_regex//%?/.*} 408 | host_regex=${host_regex//\~/~} 409 | case "${action:-}" in 410 | # Check HOST ControlMaster connection status 411 | 'check' ) 412 | for _host in ${hosts} 413 | do 414 | session_check ${_host} 415 | done 416 | ;; 417 | # Exit HOST ControlMaster connection 418 | 'exit' ) 419 | for _host in ${hosts} 420 | do 421 | if session_check ${_host} &> /dev/null 422 | then 423 | session_exit ${_host} 424 | fi 425 | done 426 | ;; 427 | # Exit all ControlMaster connections with sockets in ~/.ssh/ 428 | 'exit_all' ) 429 | session_exit_all || exit ${?} 430 | ;; 431 | # List ControlMasters defined in ${ssh_config} 432 | 'conf_list') 433 | echo "${configured_hosts}" 434 | ;; 435 | # Open HOST ControlMaster connection (Default) 436 | 'open' ) 437 | for _host in ${hosts} 438 | do 439 | session_open ${_host} 440 | done 441 | ;; 442 | # Open all ControlMasters defined in ${ssh_config} 443 | 'open_all' ) 444 | session_open_all 445 | ;; 446 | # List ControlMaster connection sockets in ~/.ssh/ 447 | 'sockets' ) 448 | sockets_display || exit ${?} 449 | ;; 450 | *) 451 | error_exit 'orphaned argument(s)--no options were specified' 2 452 | ;; 453 | esac 454 | --------------------------------------------------------------------------------