Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

Question (TL;DR)

When assigning ports dynamically for remote forwarding (a.k.a. -R option), how can a script on the remote machine (for instance sourced from .bashrc) determine which ports were chosen by OpenSSH?


Background

I use OpenSSH (on both ends) to connect to our central server, that I share with multiple other users. For my remote session (for now) I would like to forward X, cups and pulseaudio.

The most trivial is forwarding X, using the -X option. The allocated X address is stored in the environmental variable DISPLAY and from that I can determine the corresponding TCP port, in most cases anyways. But I hardly ever need to, because Xlib honors DISPLAY.

I need a similar mechanism for cups and pulseaudio. The basics for both services exist, in the form of the environmental variables CUPS_SERVER and PULSE_SERVER, respectively. Here are usage examples:

ssh -X -R12345:localhost:631 -R54321:localhost:4713 datserver

export CUPS_SERVER=localhost:12345
lowriter #and I can print using my local printer
lpr -P default -o Duplex=DuplexNoTumble minutes.pdf #printing through the tunnel
lpr -H localhost:631 -P default -o Duplex=DuplexNoTumble minutes.pdf #printing remotely

mpg123 mp3s/van_halen/jump.mp3 #annoy co-workers
PULSE_SERVER=localhost:54321 mpg123 mp3s/van_halen/jump.mp3 #listen to music through the tunnel

The problem is setting CUPS_SERVER and PULSE_SERVER correctly.

We use port forwardings a lot and therefore I need dynamic port allocations. Static port allocations are not an option.

OpenSSH has a mechanism for dynamic port allocation on the remote server, by specifying 0 as bind-port for remote forwarding (the -R option). By using the following command, OpenSSH will dynamically allocate ports for cups and pulse forwarding.

ssh -X -R0:localhost:631 -R0:localhost:4713 datserver

When I use that command, ssh will print the following to STDERR:

Allocated port 55710 for remote forward to 127.0.0.1:4713
Allocated port 41273 for remote forward to 127.0.0.1:631

There is the information I want! Ultimately I want to generate:

export CUPS_SERVER=localhost:41273
export PULSE_SERVER=localhost:55710

However the "Allocated port ..." messages are created on my local machine and sent to STDERR, which I can't access on the remote machine. Oddly enough OpenSSH does not seem to have means to retrieve Information about port forwardings.

How do I fetch that information to put it into a shell script to adequately set CUPS_SERVER and PULSE_SERVER on the remote host?


Dead Ends

The only easy thing I could find was increasing verbosity of the sshd until that information can be read from the logs. This is not viable as that information discloses a lot more information than is sensible to make accessible by non-root users.

I was thinking about patching OpenSSH to support an additional escape sequence which prints a nice representation of the internal struct permitted_opens, but even if that is what I want, I still can't script accessing the client escape sequences from the server side.


There must be a better way

The following approach seems very unstable and is limited to one such SSH session per user. However, I need at least two concurrent such sessions and other users even more. But I tried ...

When the stars are aligned properly, having sacrificed a chicken or two, I can abuse the fact that sshd is not started as my user, but drops privileges after successful login, to do this:

  • get a list of port numbers for all listening sockets that belong to my user

    netstat -tlpen | grep ${UID} | sed -e 's/^.*:\([0-9]\+\) .*$/\1/'

  • get a list of port numbers for all listening sockets that belong to processes my user started

    lsof -u ${UID} 2>/dev/null | grep LISTEN | sed -e 's/.*:\([0-9]\+\) (LISTEN).*$/\1/'

  • All ports that are in the first set, but not in the second set have a high likelyhood to be my forwarding ports, and indeed subtracting the sets yields 41273, 55710 and 6010; cups, pulse and X, respectively.

  • 6010 is identified as the X port using DISPLAY.

  • 41273 is the cups port, because lpstat -h localhost:41273 -a returns 0.
  • 55710 is the pulse port, because pactl -s localhost:55710 stat returns 0. (It even prints the hostname of my client!)

(To do the set substraction I sort -u and store the output from the above command lines and use comm to do the substraction.)

Pulseaudio lets me identify the client and, for all intents and purposes, this may serve as an anchor to separate SSH sessions that need separating. However, I haven't found a way to tie 41273, 55710 and 6010 to the same sshd process. netstat won't disclose that information to non-root users. I only get a - in the PID/Program name column where I would like to read 2339/54 (in this particular instance). So close ...

share|improve this question
    
fwiw, it's more accurate to say that netstat won't show you the PID for processes you don't own or that are kernel-space. For example –  Joel Davis May 5 at 13:37
    
The most robust way would be to patch the sshd... A quick&dirty patch would be just a few lines at the place where server gets its local port from OS, writing the port number to a file, name generated from user, remote host and port. Assuming server knows the port on client side, which isn't certain, maybe not even likely (otherwise feature would exist already). –  hyde May 14 at 5:43
    
@hyde: exactly. The remote server does not know about the forwarded ports. It just creates a few listening sockets and data is forwarded through the ssh connection. It does not know about the local destination ports. –  Bananguin May 14 at 7:31

1 Answer 1

Take two (see history for a version which does scp from server side and is a bit simpler), this should do it. The gist of it is this:

  1. pass an environment variable from client to server, telling server how it can detect when port information is avaiable and then get and use it.
  2. once port information is available, copy it from client to server, allowing server to get it (with help of part 1 above), and use it

First, setup on remote side, you need to enable sending an env variable in sshd configuration:

sudo yourfavouriteeditor /etc/ssh/sshd_config

Find line with AcceptEnv and add MY_PORT_FILE to it (or add the line under right Host section if there isn't one yet). For me the line became this:

AcceptEnv LANG LC_* MY_PORT_FILE

Also remember to restart sshd for this to take effect.

Additionally, for below scripts to work, do mkdir ~/portfiles on remote side!


Then on local side, a script snippet which will

  1. create temp file name for stderr redirection
  2. leave a background job to wait for the file to have content
  3. pass file name to server as env variable, while redirecting ssh stderr to the file
  4. background job proceeds to copy the stderr temp file to server side using separate scp
  5. background job also copies a flag file to server to indicate stderr file is ready

The script snippet:

REMOTE=$USER@datserver

PORTFILE=`mktemp /tmp/sshdataserverports-$(hostname)-XXXXX`
test -e $PORTFILE && rm -v $PORTFILE

# EMPTYFLAG servers both as empty flag file for remote side,
# and safeguard for background job termination on this side
EMPTYFLAG=$PORTFILE-empty
cp /dev/null $EMPTYFLAG

# this variable has the file name sent over ssh connection
export MY_PORT_FILE=$(basename $PORTFILE)

# background job loop to wait for the temp file to have data
( while [ -f $EMPTYFLAG -a \! -s $PORTFILE ] ; do
     sleep 1 # check once per sec
  done
  sleep 1 # make sure temp file gets the port data

  # first copy temp file, ...
  scp  $PORTFILE $REMOTE:portfiles/$MY_PORT_FILE

  # ...then copy flag file telling temp file contents are up to date
  scp  $EMPTYFLAG $REMOTE:portfiles/$MY_PORT_FILE.flag
) &

# actual ssh terminal connection    
ssh -X -o "SendEnv MY_PORT_FILE" -R0:localhost:631 -R0:localhost:4713 $REMOTE 2> $PORTFILE

# remove files after connection is over
rm -v $PORTFILE $EMPTYFLAG

Then a snippet for remote side, suitable for .bashrc:

# only do this if subdir has been created and env variable set
if [ -d ~/portfiles -a "$MY_PORT_FILE" ] ; then

       PORTFILE=~/portfiles/$(basename "$MY_PORT_FILE")
       FLAGFILE=$PORTFILE.flag
       # wait for FLAGFILE to get copied,
       # after which PORTFILE should be complete
       while [ \! -f "$FLAGFILE" ] ; do 
           echo "Waiting for $FLAGFILE..."
           sleep 1
       done

       # use quite exact regexps and head to make this robust
       export CUPS_SERVER=localhost:$(grep '^Allocated port [0-9]\+ .* localhost:631[[:space:]]*$' "$PORTFILE" | head -1 | cut -d" " -f3)
       export PULSE_SERVER=localhost:$(grep '^Allocated port [0-9]\+ .* localhost:4713[[:space:]]*$' "$PORTFILE" | head -1 | cut -d" " -f3)
       echo "Set CUPS_SERVER and PULSE_SERVER"

       # copied files served their purpose, and can be removed right away
       rm -v -- "$PORTFILE" "$FLAGFILE"
fi

Note: Above code is of course not very thoroughly tested and could contain all kinds of bugs, copy-paste errors, etc. Anybody using it better also understand it, use at your own risk! I tested it using just localhost connection, and it worked for me, in my test env. YMMV.

share|improve this answer
    
Which of course requires I can scp from the remote side to the local side, which I can't. I had a similar approach, but I would wrap ssh to background it after establishing the connection, then send that file from local to remote via scp and then pull the ssh client to the foreground and run a script on the remote side. I haven't figured out how to script backgrounding and foregrounding local and remote processes nicely. Wrapping and integrating the local ssh client with some remote scripts like that does not seem like a good approach. –  Bananguin May 13 at 21:32
    
Ah. I think you should background the client side scp only: (while [ ... ] ; do sleep 1 ; done ; scp ... )& . Then wait on the foreground in server .bashrc (assuming client sends right env variable) for the file to appear. I'll update the answer later after some testing (probably no time until tomorrow). –  hyde May 14 at 5:29
    
@Bananguin New version done. Seems to work for me, so should be adaptible to your use case. About "nice approach", yeah, but I don't think there's really a nice approach possible here. The information needs to be passed somehow, and it's always going to be a hack, unless you patch both ssh client and server to do it cleanly over the single connection. –  hyde May 14 at 20:47
    
And I am thinking more and more about patching openssh. Doesn't seem to be a big deal. The information is already available. I just need to send it to the server. Whenever the server receives such information it writes it to ~/.ssh-${PID}-forwards –  Bananguin May 15 at 10:00

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.