├── LICENSE ├── README.md └── ssh-chain /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Ryan Castellucci 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | DESCRIPTION 2 | ----------- 3 | ssh-chain - ssh via a chain of intermediary hosts 4 | 5 | NOTE 6 | ---- 7 | This functionality is built into OpenSSH via the -J option as of version 8 | 7.3, therefore **this tool will no longer be maintained**. 9 | 10 | INSTALL 11 | ------- 12 | 13 | Copy the ssh-chain script to somewhere that's in your path. Append the 14 | following to ~/.ssh/config or /etc/ssh/ssh_config: 15 | 16 | ``` 17 | # This should be the last entry 18 | Host *^* 19 | ProxyCommand ssh-chain %h %p 20 | ``` 21 | 22 | and you're done. 23 | 24 | USAGE 25 | ----- 26 | 27 | ssh-chain can act as a wrapper to ssh in order to avoid filling your 28 | known_hosts file with garbage - just run ssh-chain instead of ssh. 29 | 30 | The simple use case is this: 31 | 32 | ssh final.example^second.example^first.example 33 | 34 | The connection is built right to left, so you'll end up with a set of 35 | connections that looks like this: 36 | 37 | you -> first.example -> second.example -> final.example 38 | 39 | This will also work with scp/sftp and hopefully any other tool that invokes 40 | ssh as a backend (e.g. rsync, git, svn, etc.) and all the standard features 41 | such as port forwarding should work. 42 | 43 | ADVANCED USAGE 44 | -------------- 45 | 46 | Sometimes you'll have need to specify a username or port for an 47 | intermediary host. Since ssh will normally consume these, different (and 48 | sort of weird) syntax is used. Ports are specified by appending an underscore 49 | (e.g. foo.example_2222) and usernames use a plus instead of an at symbol (e.g. 50 | jdoe+foo.example). The far left host still needs to be specified using an 51 | at symbol since this doesn't get fed to the ProxyCommand. Example: 52 | 53 | jdoe@final.example^johnd+second.example_2222^john+first.example_443 54 | 55 | HOST-SPECIFIC OPTIONS 56 | --------------------- 57 | 58 | To make host-specific options for hosts other than the first one in the chain 59 | work, you need to change lines like this 60 | 61 | ``` 62 | Host *.foo.example bar.example 63 | User john 64 | Port 2222 65 | ``` 66 | 67 | to 68 | 69 | ``` 70 | Host *.foo.example *.foo.example^* bar.example bar.example^* 71 | User john 72 | Port 2222 73 | ``` 74 | 75 | NOTES 76 | ----- 77 | 78 | It's preferable to use OpenSSH 5.4 or newer with ssh-chain. 'netcat mode' (-W) 79 | was added then and this is faster then exec'ing netcat on the remote host. 80 | ssh-chain auto-detects if -W is available and will remote exec netcat 81 | otherwise. 82 | -------------------------------------------------------------------------------- /ssh-chain: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | use strict; 3 | use English; 4 | 5 | use IPC::Open3; 6 | 7 | my $FORCE_NETCAT = $ENV{'FORCE_NETCAT'} || 0; 8 | my $SSH_BIN = $ENV{'SSH_BIN'} || 'ssh'; 9 | my $DEBUG = $ENV{'DEBUG'} || 0; 10 | my $SSH_OPTS = ssh_cli_opts(); 11 | 12 | ################ 13 | ### ### 14 | ### Main ### 15 | ### ### 16 | ################ 17 | 18 | # TODO: robust error handling 19 | # Attempt to detect if we were called by SSH 20 | my $PPID = getppid; # Get parent process id 21 | if ($PPID > 0 && open(my $fh, '<', "/proc/$PPID/cmdline")) { 22 | my @cmdline = split(/\0/, join("\n", <$fh>)); 23 | close($fh); 24 | # If not invoked by ssh, operate as a wrapper 25 | if ($cmdline[0] !~ /(?:\A|\/)ssh\z/) { 26 | unless (grep { /HostKeyAlias=/i } @cmdline) { 27 | my @SSH_ARGS = @ARGV; 28 | # Remove all options leaving the hostname 29 | while (scalar(@SSH_ARGS) && $SSH_ARGS[0] =~ /\A-[^-]*([^-])\Z/) { 30 | shift(@SSH_ARGS); 31 | # If an argument was required for the option, remove that too 32 | shift(@SSH_ARGS) if ($SSH_OPTS->{args}->{$1}); 33 | } 34 | my $hostname = $SSH_ARGS[0]; # hostname should be left 35 | @SSH_ARGS = @ARGV; # Repopulate argument 36 | if ($hostname) { 37 | $hostname =~ s/.*[\@\+]//; # Remove username 38 | # Since we're overriding the hostname, the Host *^* in the ssh config 39 | # file will no longer match, so we need to set the ProxyCommand here 40 | unshift(@SSH_ARGS, '-o', "ProxyCommand=$0 $hostname \%p"); 41 | $hostname =~ s/\^.*//; # Remove caretpath 42 | $hostname =~ s/_.*//; # Remove port spec 43 | # Prevent an unknown host key warning for the final host 44 | unshift(@SSH_ARGS, '-o', "HostKeyAlias=$hostname"); 45 | } 46 | print STDERR join(' ', $SSH_BIN, @SSH_ARGS) . "\n" if ($DEBUG); 47 | exec($SSH_BIN, @SSH_ARGS); # Script stops here 48 | } 49 | } 50 | } 51 | 52 | # If we get to here, we're acting as a ProxyCommand 53 | 54 | my $host_arg = $ARGV[0] || die "No host passed!"; 55 | my $port_arg = $ARGV[1] || die "No port passed!"; 56 | 57 | my ($dest_host, $bounce_host); 58 | if ($host_arg =~ /\A([^\^]+)\^([^\^].*)/) { 59 | ($dest_host, $bounce_host) = ($1, $2); 60 | } else { 61 | die "Invalid arguments!"; 62 | } 63 | 64 | # Deal with usernames 65 | $bounce_host =~ s/\A([^\^\+]+)\+/$1\@/; 66 | # Strip username+ syntax from the dest host 67 | $dest_host =~ s/\A.*\+//; 68 | 69 | if ($dest_host =~ /\A([^\^]+)_(\d+)\z/) { 70 | $dest_host = $1; 71 | $port_arg = $2; 72 | } 73 | 74 | print STDERR "Connecting to $dest_host:$port_arg via $bounce_host\n" if ($DEBUG); 75 | 76 | # See if the local version of ssh supports -W ('netcat mode') 77 | if (!$FORCE_NETCAT && $SSH_OPTS->{args}->{W}) { 78 | ssh_bounce_native($dest_host, $port_arg, $bounce_host); 79 | } else { 80 | ssh_bounce_netcat($dest_host, $port_arg, $bounce_host); 81 | } 82 | 83 | # Shouldn't make it this far 84 | exit 1; 85 | 86 | ##################### 87 | ### ### 88 | ### Functions ### 89 | ### ### 90 | ##################### 91 | 92 | # Parse supported SSH command line options 93 | sub ssh_cli_opts { 94 | my $data = {}; 95 | my @tmp; 96 | # 97 | my $pid = open3(undef, undef, \*FH, $SSH_BIN); 98 | while(my $line = ) { 99 | $line =~ s/\A[^\[]+\[(.*)\]\Z/$1/; # Trim beginning and end of line 100 | my @chunks = split(/\]\s+\[/, $line); 101 | foreach my $chunk (@chunks) { 102 | # If SSH adds long options this will need to be updated 103 | if ($chunk =~ /\A-([^-]\S+)\z/) { 104 | foreach my $flag (split(//, $1)) { $data->{flags}->{$flag}++; } 105 | } elsif ($chunk =~ /\A-(\S)\s+/) { 106 | $data->{args}->{$1}++; 107 | } 108 | } 109 | } 110 | close(FH); 111 | waitpid($pid, 0); 112 | return $data; 113 | } 114 | 115 | sub ssh_bounce_native { 116 | my $dest_host = shift; 117 | my $port_arg = shift; 118 | my $bounce_host = shift; 119 | 120 | print STDERR "Using ssh -W\n" if ($DEBUG); 121 | ssh_bounce_generic($dest_host, $port_arg, $bounce_host, [$SSH_BIN, '-W', "$dest_host:$port_arg"]); 122 | } 123 | 124 | sub ssh_bounce_netcat { 125 | my $dest_host = shift; 126 | my $port_arg = shift; 127 | my $bounce_host = shift; 128 | 129 | print STDERR "Using ssh exec netcat\n" if ($DEBUG); 130 | ssh_bounce_generic($dest_host, $port_arg, $bounce_host, [$SSH_BIN], ['exec', 'nc', $dest_host, $port_arg]); 131 | } 132 | 133 | sub ssh_bounce_generic { 134 | my $dest_host = shift; 135 | my $port_arg = shift; 136 | my $bounce_host = shift; 137 | my $local_cmd = shift || []; # needs an arrayref 138 | my $remote_cmd = shift || []; # needs an arrayref 139 | 140 | my @SSH_CMD = @$local_cmd; # dereference 141 | if ($bounce_host =~ /\A([^\^]+)_(\d+)\z/) { 142 | $bounce_host = $1; 143 | push(@SSH_CMD, '-p', $2); 144 | } 145 | # reduce key warnings and known_hosts pollution by correcting ssh's idea of 146 | # what target host should be used when matching known fingerprints. This does 147 | # not prevent the whole initial caretpath from being logged/checked, though 148 | # that can be prevented by specifying -o HostKeyAlias=... yourself 149 | if ($bounce_host =~ /\A(?:[^\+\@]+[\@\+])?([A-Za-z0-9_\-\.]+)(?:_\d+)?\^/) { 150 | push(@SSH_CMD, '-o', "HostKeyAlias=$1"); 151 | } 152 | push(@SSH_CMD, $bounce_host); 153 | push(@SSH_CMD, @$remote_cmd); 154 | print STDERR join(' ', @SSH_CMD) . "\n" if ($DEBUG); 155 | exec(@SSH_CMD); # Script stops here 156 | } 157 | # vim: ts=2 sw=2 et ai si 158 | --------------------------------------------------------------------------------