tunneling through stdin/stdout, source routing

Alan Barrett apb at cequrux.com
Wed Nov 22 02:54:45 EST 2006


On Sat, 11 Nov 2006, Simon Richter wrote:
> quite often I find myself using commands like
> 
> $ ssh foo nc bar 12345
  [...]
> I wonder whether it would make sense to have an option in the ssh
> client that told it to connect to the server, then open a tunneled
> connection and connect that to stdin/stdout.

That would be nice.  Syntax wise, I suggest an extension of the "-L"
or "LocalForward" option to let it interpret "-" in the local port number
position as a request to forward stdin/stdout instead of a TCP port.
So this:

    ssh -N -L -:bar:12345 foo

would make an ssh connection to foo, ask foo to forward a channel to
bar:12345, amd connect the local stdin/stdout to the forwarded channel.

I append a proof of concept wrapper script that can be used as

    sshconnect foo bar 12345

to do much the same thing, through a combination of "ssh -N -L
localhost:localport:bar:12345 foo" and "connect localhost localport",
where localport is a dynamically allocated port.  It works without any
support from the server, but requires perl and connect on the client.

> As an extension, there could also be a source routing option in
> the config file that would take care of setting up a chain of ssh
> connections if I need multiple hops.

I'd suggest using "ProxyCommand some_clever_script %h %p".  Let the
clever script have knowledge of which hosts can see which other hosts,
which firewall you are behind today, etc.

--apb (Alan Barrett)

#!/usr/bin/perl -w
#
# sshconnect - connect stdin/stdout to a distant host and port,
#              via ssh port forwarding through a nearby host.
#
# Placed in the public domain by A P Barrett, November 2006.
#
# usage: sshconnect [-sshoptions] [user@]nearby distanthost distantport
#
# usage in .ssh/config file:
#	ProxyCommand sshconnect [-sshoptions] [user@]nearby %h %p
#
# What it does:
#  1.  Find an available local port number;
#  2.  Use ssh to connect to the nearby host, and set up
#      port forwarding from the local port allocated above to the
#      desired distant host and port.
#  3.  Use the "connect" program to connect stdin/stdout to the
#      local port allocated above.  This will cause the local
#      ssh client to ask the nearby ssh server to forward the connection
#      to the distant destination.
#

# returns a local port number that is not currently in use.
# XXX: There is a race between choosing the port here,
# and using the port elsewhere.
sub choose_port
{
    my (@netstat_lines) = `netstat -an -f inet`;
    foreach my $port (2048..3000) {
	if (! grep /\.\Q${port}\E\s/, @netstat_lines) {
	    #print STDERR "Chose port $port\n";
	    return $port;
	}
    }
    return undef;
}

# sleep for a specified (possibly fractional) number of seconds
sub fracsleep
{
    my ($delay) = @_;
    select(undef, undef, undef, $delay);
}

# really kill a process, by sending SIGTERM immediately,
# and SIGKILL after a short delay if the process doesn't exit by itelf.
sub really_kill
{
    my ($pid) = @_;
    kill TERM, $pid;
    foreach my $delay (((0.1) x 5, (0.5) x 4)) {
	return unless kill 0, $pid;
	fracsleep $delay;
    }
    kill KILL, $pid;
}

sub main
{
    my (@ARGV) = @_;
    my $localhost = "127.0.0.1";
    my $localport = choose_port();
    my (@delays) = ((0.2) x 5, (0.5) x 6, (1) x 10, (2) x 60);
    my $connect_command = "connect";
    my $connect_status = undef;
    my $childpid = undef;
    my $sighandler = sub {
		    #print STDERR "CAUGHT SIGNAL\n";
		    really_kill($childpid) if defined $childpid;
		    exit(1);
		};
    local $SIG{INT} = $sighandler;

    die "Can't assign local port\n" unless defined $localport;

    $childpid = fork();

    die "Can't fork\n" unless defined $childpid;

    if ($childpid == 0) {
	# This is the child.
	# Run ssh, asking it to set up port forwarding.

	my ($distant_port) = pop(@ARGV);
	my ($distant_host) = pop(@ARGV);
	my ($user_at_nearby_host) = pop(@ARGV);
	my (@ssh_options) = @ARGV;
	push @ssh_options, (
		# no login or command
		"-N",
		# port forwarding does not play nicely with
		# session multiplexing, so disable session multiplexing.
		"-o", "ControlPath=none",
		"-o", "ControlMaster=no",
		# forward the local port to the distant port
		"-o", "ExitOnForwardFailure=yes",
		"-L", "localhost:${localport}:${distant_host}:${distant_port}",
		);

	exec ("ssh", @ssh_options, $user_at_nearby_host);

	exit(0);

    } else {
	# This is the parent.
	# Connect sdin/stdout to the TCP port that ssh will forward.

	# Wait until netstat shows that $localport is listening,
	# or the child dies.
	$ok = 0;
	foreach my $delay (@delays) {
	    @netstat_lines = `netstat -an -f inet`;
	    $ok = 1 if (grep /\s\Q${localhost}.${localport}\E\s.*LISTEN/,
		    @netstat_lines);

	    if ($ok) {
		#print STDERR "OK\n";
		last;
	    }

	    if (kill(0, $childpid) != 1) {
		#print STDERR "ssh died\n";
		last;
	    }

	    #print STDERR "SLEEP $delay\n";
	    fracsleep $delay;
	}

	if (! $ok) {
	    #print STDERR "Could not forward port\n";
	}
	
	# Connect.  This will run until EOF or network error.
	if ($ok) {
	    #print STDERR "CONNECTING to port $localport\n";
	    $connect_status = system($connect_command, $localhost, $localport);
	    #print STDERR "CONNECT STATUS $connect_status\n";
	}

	# Kill the child and exit.
	#print STDERR "KILL $childpid\n";
	really_kill($childpid);

	if (defined($connect_status)) {
	    exit($connect_status);
	} else {
	    exit(1);
	}
    }
}

main @ARGV;


More information about the openssh-unix-dev mailing list