Determine el puerto asignado dinámicamente para OpenSSH RemoteForward

13

Pregunta (TL; DR)

Al asignar puertos dinámicamente para el reenvío remoto (también conocido como -Ropción), ¿cómo puede un script en la máquina remota (por ejemplo, procedente de .bashrc) determinar qué puertos fueron elegidos por OpenSSH?


Antecedentes

Utilizo OpenSSH (en ambos extremos) para conectarme a nuestro servidor central, que comparto con muchos otros usuarios. Para mi sesión remota (por ahora) me gustaría reenviar X, cups y pulseaudio.

Lo más trivial es reenviar X, usando la -Xopción. La dirección X asignada se almacena en la variable de entorno DISPLAYy a partir de eso puedo determinar el puerto TCP correspondiente, en la mayoría de los casos de todos modos. Pero casi nunca necesito hacerlo, porque Xlib lo honra DISPLAY.

Necesito un mecanismo similar para tazas y pulseaudio. Existen los conceptos básicos para ambos servicios, en forma de variables ambientales CUPS_SERVERy PULSE_SERVER, respectivamente. Aquí hay ejemplos de uso:

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

El problema está configurando CUPS_SERVERy PULSE_SERVERcorrectamente.

Usamos muchos reenvíos de puertos y, por lo tanto, necesito asignaciones de puertos dinámicas. Las asignaciones de puertos estáticos no son una opción.

OpenSSH tiene un mecanismo para la asignación dinámica de puertos en el servidor remoto, al especificar 0como puerto de enlace para el reenvío remoto (la -Ropción). Al usar el siguiente comando, OpenSSH asignará dinámicamente puertos para tazas y reenvío de pulsos.

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

Cuando uso ese comando, sshimprimiremos lo siguiente para 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

¡Ahí está la información que quiero! En definitiva quiero generar:

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

Sin embargo, los mensajes de "Puerto asignado ..." se crean en mi máquina local y se envían a los STDERRcuales no puedo acceder desde la máquina remota. Curiosamente, OpenSSH no parece tener medios para recuperar información sobre reenvíos de puertos.

¿Cómo obtengo esa información para ponerla en un script de shell para configurar adecuadamente CUPS_SERVERy PULSE_SERVERen el host remoto?


Callejones sin salida

Lo único fácil que pude encontrar fue aumentar la verbosidad de la sshdinformación hasta que se pueda leer la información de los registros. Esto no es viable ya que esa información revela mucha más información de la que es razonable hacer accesible para usuarios no root.

Estaba pensando en parchear OpenSSH para admitir una secuencia de escape adicional que imprima una buena representación de la estructura interna permitted_opens, pero incluso si eso es lo que quiero, todavía no puedo acceder a las secuencias de escape del cliente desde el lado del servidor.


Debe haber una mejor manera

El siguiente enfoque parece muy inestable y está limitado a una sesión SSH por usuario. Sin embargo, necesito al menos dos sesiones simultáneas y otros usuarios aún más. Pero lo intenté ...

Cuando las estrellas están alineadas correctamente, después de haber sacrificado una gallina o dos, puedo abusar del hecho de que sshdno se inicia como mi usuario, sino que elimina los privilegios después de iniciar sesión correctamente, para hacer esto:

  • obtener una lista de números de puerto para todos los enchufes de escucha que pertenecen a mi usuario

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

  • obtener una lista de números de puerto para todos los sockets de escucha que pertenecen a procesos que mi usuario inició

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

  • Todos los puertos que están en el primer conjunto, pero no en el segundo conjunto tienen una alta probabilidad de ser mis puertos de reenvío, y de hecho restando los rendimientos de los conjuntos 41273, 55710y 6010; tazas, pulso y X, respectivamente.

  • 6010se identifica como el puerto X usando DISPLAY.

  • 41273es el puerto de tazas, porque lpstat -h localhost:41273 -aregresa 0.
  • 55710es el puerto de pulso, porque pactl -s localhost:55710 statregresa 0. (¡Incluso imprime el nombre de host de mi cliente!)

(Para hacer la substracción del conjunto I sort -uy almacenar la salida de las líneas de comando anteriores y usar commpara hacer la substracción).

Pulseaudio me permite identificar al cliente y, para todos los efectos, esto puede servir como un ancla para separar las sesiones SSH que necesitan separarse. Sin embargo, no he encontrado una manera de vincular 41273, 55710y 6010al mismo sshdproceso. netstatno divulgará esa información a usuarios no root. Solo recibo un -en la PID/Program namecolumna donde me gustaría leer 2339/54(en este caso particular). Tan cerca ...

Bananguin
fuente
fwiw, es más exacto decir que netstatno le mostrará el PID para los procesos que no posee o que son kernel-space. Por ejemplo
Bratchley,
La forma más sólida sería parchear el sshd ... Un parche rápido y sucio sería solo unas pocas líneas en el lugar donde el servidor obtiene su puerto local del sistema operativo, escribiendo el número de puerto en un archivo, el nombre generado por el usuario, el host remoto y Puerto. Suponiendo que el servidor conoce el puerto en el lado del cliente, lo cual no es seguro, tal vez ni siquiera sea probable (de lo contrario, la característica ya existiría).
hyde
@hyde: exactamente. El servidor remoto no conoce los puertos reenviados. Simplemente crea algunos enchufes de escucha y los datos se reenvían a través de la conexión ssh. No conoce los puertos de destino locales.
Bananguin

Respuestas:

1

Tome dos (vea el historial para una versión que hace scp desde el lado del servidor y es un poco más simple), esto debería hacerlo. La esencia de esto es esto:

  1. pasar una variable de entorno del cliente al servidor, diciéndole al servidor cómo puede detectar cuándo está disponible la información del puerto y luego obtenerla y usarla.
  2. una vez que la información del puerto esté disponible, cópiela del cliente al servidor, permitiendo que el servidor la obtenga (con ayuda de la parte 1 anterior) y úsela

Primero, la configuración en el lado remoto, debe habilitar el envío de una variable env en la configuración sshd :

sudo yourfavouriteeditor /etc/ssh/sshd_config

Busque la línea AcceptEnvy agréguela MY_PORT_FILE(o agregue la línea debajo de la Hostsección derecha si aún no hay una) Para mí, la línea se convirtió en esto:

AcceptEnv LANG LC_* MY_PORT_FILE

Asimismo, recuerda a reiniciar sshd para que esto tenga efecto.

Además, para que funcionen las siguientes secuencias de comandos, ¡hazlo mkdir ~/portfilesen el lado remoto!


Luego, en el lado local, un fragmento de script que

  1. crear nombre de archivo temporal para la redirección de stderr
  2. dejar un trabajo en segundo plano para esperar que el archivo tenga contenido
  3. pasar el nombre de archivo de servidor como variable de entorno, mientras que la reorientación de ssh stderr al archivo
  4. el trabajo en segundo plano procede a copiar el archivo temporal stderr al lado del servidor usando un scp separado
  5. el trabajo en segundo plano también copia un archivo de marca al servidor para indicar que el archivo stderr está listo

El fragmento de script:

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

Luego un fragmento para el lado remoto, adecuado para .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

Nota : Por supuesto, el código anterior no se ha probado exhaustivamente y puede contener todo tipo de errores, errores de copiar y pegar, etc. Cualquiera que lo use mejor también lo entiende, ¡ úselo bajo su propio riesgo! Lo probé usando solo la conexión localhost, y funcionó para mí, en mi entorno de prueba. YMMV.

Hyde
fuente
Lo que, por supuesto, requiere que pueda scpdesde el lado remoto al lado local, lo que no puedo. Tuve un enfoque similar, pero lo envolví sshen segundo plano después de establecer la conexión, luego envié ese archivo de local a remoto a través de scpy luego empujé al sshcliente a primer plano y ejecuté un script en el lado remoto. No he descubierto cómo hacer un script de fondo y poner en primer plano procesos locales y remotos muy bien. Ajustar e integrar el sshcliente local con algunos scripts remotos como ese no parece un buen enfoque.
Bananguin
Ah Creo que deberías fondo del lado del cliente SCP solamente: (while [ ... ] ; do sleep 1 ; done ; scp ... )&. Luego, espere en primer plano en el servidor .bashrc(suponiendo que el cliente envíe la variable env correcta) para que aparezca el archivo. Actualizaré la respuesta más tarde después de algunas pruebas (probablemente no habrá tiempo hasta mañana).
hyde
@Bananguin Nueva versión hecha. Parece funcionar para mí, por lo que debe ser adaptable a su caso de uso. Sobre "buen enfoque", sí, pero no creo que haya un buen enfoque posible aquí. La información debe pasarse de alguna manera, y siempre será un truco, a menos que parchee tanto el cliente ssh como el servidor para hacerlo limpiamente a través de la conexión única.
hyde
Y estoy pensando cada vez más en parchar openssh. No parece ser un gran problema. La información ya está disponible. Solo necesito enviarlo al servidor. Cada vez que el servidor recibe dicha información, la escribe~/.ssh-${PID}-forwards
Bananguin
1

Un fragmento para el lado local, adecuado para .bashrc:

#!/bin/bash

user=$1
host=$2

sshr() {
# 1. connect, get dynamic port, disconnect  
port=`echo "exit" | ssh -R '*:0:127.0.0.1:52698' -t $1 2>&1 | grep 'Allocated port' | awk '/port/ {print $3;}'`
# 2. reconnect with this port and set remote variable
cmds="ssh -R $port:127.0.0.1:52698 -t $1 bash -c \"export RMATE_PORT=$port; bash\""
($cmds)
}

sshr $user@$host
ToxeH
fuente
0

He logrado lo mismo al crear una tubería en el cliente local, luego redirigir stderr a la tubería que también se redirige a la entrada de ssh. No requiere múltiples conexiones ssh para suponer un puerto libre conocido que podría fallar. De esta manera, el banner de inicio de sesión y el texto "Puerto asignado ### ..." se redirige al host remoto.

Tengo un script simple en el host getsshport.shque se ejecuta en el host remoto que lee la entrada redirigida y analiza el puerto. Mientras este script no finalice, el avance remoto ssh permanece abierto.

lado local

mkfifo pipe
ssh -R "*:0:localhost:22" user@remotehost "~/getsshport.sh" 3>&1 1>&2 2>&3 < pipe | cat > pipe

3>&1 1>&2 2>&3 es un pequeño truco para intercambiar stderr y stdout, de modo que stderr se canalice a cat y toda la salida normal de ssh se muestre en stderr.

lado remoto ~ / getsshport.sh

#!/bin/sh
echo "Connection from $SSH_CLIENT"
while read line
do
    echo "$line" # echos everything sent back to the client
    echo "$line" | sed -n "s/Allocated port \([0-9]*\) for remote forward to \(.*\)\:\([0-9]*\).*/client port \3 is on local port \1/p" >> /tmp/allocatedports
done

Yo probé grepal mensaje "puerto asignado" en el lado local en primer lugar antes de enviarlo a través de SSH, pero parece que ssh bloqueará la espera de la tubería para abrir la entrada estándar. grep no abre la tubería para escribir hasta que recibe algo, por lo que esto básicamente se interrumpe. catsin embargo, no parece tener este mismo comportamiento, y abre la tubería para escribir inmediatamente permitiendo que ssh abra la conexión.

este es el mismo problema en el lado remoto, y por qué readlínea por línea en lugar de solo grep de stdin; de lo contrario, `/ tmp / alloport 'no se escribe hasta que se cierra el túnel ssh, lo que frustra todo el propósito

~/getsshport.shSe prefiere canalizar el stderr de ssh en un comando como , ya que sin especificar un comando, el texto del banner o cualquier otra cosa en la tubería se ejecuta en el shell remoto.

JesseMcL
fuente
bonito. agregué renice +10 $$; exec catantes del donepara ahorrar recursos.
Spongman el
0

Esto es complicado, el manejo adicional del lado del servidor a lo largo de las líneas SSH_CONNECTIONo DISPLAYsería genial, pero no es fácil de agregar: parte del problema es que solo el sshcliente conoce el destino local, el paquete de solicitud (al servidor) contiene solo la dirección remota y el puerto.

Las otras respuestas aquí tienen varias soluciones sencillas para capturar este lado del cliente y enviarlo al servidor. Aquí hay un enfoque alternativo que no es mucho más bonito para ser honesto, pero al menos esta fiesta fea se mantiene del lado del cliente ;-)

  • lado del cliente, agregue / modifique SendEnvpara que podamos enviar algunas variables de entorno de forma nativa a través de ssh (probablemente no predeterminado)
  • lado del servidor, agregue / modifique AcceptEnvpara aceptar lo mismo (probablemente no esté habilitado de forma predeterminada)
  • supervise la sshsalida del cliente stderr con una biblioteca cargada dinámicamente y actualice el entorno del cliente ssh durante la configuración de la conexión
  • recoger las variables de entorno del lado del servidor en el perfil / script de inicio de sesión

Esto funciona (felizmente, por ahora de todos modos) porque los reenvíos remotos se configuran y graban antes de intercambiar el entorno (confirmar con ssh -vv ...). La biblioteca cargada dinámicamente tiene que capturar la write()función libc ( ssh_confirm_remote_forward()logit()do_log()write()). Las funciones de redireccionamiento o ajuste en un binario ELF (sin recompilar) son órdenes de magnitud más complejas que hacer lo mismo para una función en una biblioteca dinámica.

En el cliente .ssh/config(o línea de comando -o SendEnv ...)

Host somehost
  user whatever
  SendEnv SSH_RFWD_*

En el servidor sshd_config(se requiere cambio administrativo / raíz)

AcceptEnv LC_* SSH_RFWD_*

Este enfoque funciona para clientes Linux y no requiere nada especial en el servidor, debería funcionar para otros * nix con algunos ajustes menores. Funciona desde al menos OpenSSH 5.8p1 hasta 7.5p1.

Compilar con gcc -Wall -shared -ldl -Wl,-soname,rfwd -o rfwd.so rfwd.c Invoke con:

LD_PRELOAD=./rfwd.so ssh -R0:127.0.0.1:4713 -R0:localhost:631 somehost

El código:

#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h>
#include <string.h>
#include <stdlib.h>

// gcc -Wall -shared  -ldl -Wl,-soname,rfwd -o rfwd.so rfwd.c

#define DEBUG 0
#define dfprintf(fmt, ...) \
    do { if (DEBUG) fprintf(stderr, "[%14s#%04d:%8s()] " fmt, \
          __FILE__, __LINE__, __func__,##__VA_ARGS__); } while (0)

typedef ssize_t write_fp(int fd, const void *buf, size_t count);
static write_fp *real_write;

void myinit(void) __attribute__((constructor));
void myinit(void)
{
    void *dl;
    dfprintf("It's alive!\n");
    if ((dl=dlopen(NULL,RTLD_NOW))) {
        real_write=dlsym(RTLD_NEXT,"write");
        if (!real_write) dfprintf("error: %s\n",dlerror());
        dfprintf("found %p write()\n", (void *)real_write);
    } else {
        dfprintf(stderr,"dlopen() failed\n");
    }
}

ssize_t write(int fd, const void *buf, size_t count)
{
     static int nenv=0;

     // debug1: Remote connections from 192.168.0.1:0 forwarded to local address 127.0.0.1:1000
     //  Allocated port 44284 for remote forward to 127.0.0.1:1000
     // debug1: All remote forwarding requests processed
     if ( (fd==2) && (!strncmp(buf,"Allocated port ",15)) ) {
         char envbuf1[256],envbuf2[256];
         unsigned int rport;
         char lspec[256];
         int rc;

         rc=sscanf(buf,"Allocated port %u for remote forward to %256s",
          &rport,lspec);

         if ( (rc==2) && (nenv<32) ) {
             snprintf(envbuf1,sizeof(envbuf1),"SSH_RFWD_%i",nenv++);
             snprintf(envbuf2,sizeof(envbuf2),"%u %s",rport,lspec);
             setenv(envbuf1,envbuf2,1);
             dfprintf("setenv(%s,%s,1)\n",envbuf1,envbuf2);
         }
     }
     return real_write(fd,buf,count);
}

(Hay algunas trampas de oso glibc relacionadas con el control de versiones de símbolos con este enfoque, pero write()no tiene este problema).

Si te sientes valiente, puedes tomar el setenv()código relacionado y parcharlo en la ssh.c ssh_confirm_remote_forward()función de devolución de llamada.

Esto establece las variables de entorno nombradas SSH_RFWD_nnn, inspeccione estas en su perfil, por ejemplo, enbash

for fwd in ${!SSH_RFWD_*}; do
    IFS=" :" read lport rip rport <<< ${!fwd}
    [[ $rport -eq "631" ]] && export CUPS_SERVER=localhost:$lport
    # ...
done

Advertencias:

  • no hay muchos errores al verificar el código
  • cambiar el entorno puede causar problemas relacionados con subprocesos, PAM usa subprocesos, no espero problemas pero no lo he probado
  • sshactualmente no registra claramente el reenvío completo del formulario * local: puerto: remoto: puerto * (si fuera necesario, se requeriría un análisis adicional de los debug1mensajes ssh -v), pero no lo necesita para su caso de uso

Curiosamente, OpenSSH no parece tener medios para recuperar información sobre reenvíos de puertos.

Puede (en parte) hacer esto interactivamente con el escape ~#, curiosamente, la implementación omite los canales que están escuchando, solo enumera los abiertos (es decir, TCP ESTABLECIDO) y no imprime los campos útiles en ningún caso. Verchannels.c channel_open_message()

Puede parchear esa función para imprimir los detalles de las SSH_CHANNEL_PORT_LISTENERranuras, pero eso solo le proporciona los reenvíos locales (los canales no son lo mismo que los reenvíos reales ). O bien, puede parchearlo para volcar las dos tablas de reenvío de la optionsestructura global :

#include "readconf.h"
Options options;  /* extern */
[...]
snprintf(buf, sizeof buf, "Local forwards:\r\n");
buffer_append(&buffer, buf, strlen(buf));
for (i = 0; i < options.num_local_forwards; i++) {
     snprintf(buf, sizeof buf, "  #%d listen %s:%d connect %s:%d\r\n",i,
       options.local_forwards[i].listen_host,
       options.local_forwards[i].listen_port,
       options.local_forwards[i].connect_host,
       options.local_forwards[i].connect_port);
     buffer_append(&buffer, buf, strlen(buf));
}
snprintf(buf, sizeof buf, "Remote forwards:\r\n");
buffer_append(&buffer, buf, strlen(buf));
for (i = 0; i < options.num_remote_forwards; i++) {
     snprintf(buf, sizeof buf, "  #%d listen %s:%d connect %s:%d\r\n",i,
       options.remote_forwards[i].listen_host,
       options.remote_forwards[i].listen_port,
       options.remote_forwards[i].connect_host,
       options.remote_forwards[i].connect_port);
     buffer_append(&buffer, buf, strlen(buf));
}

Esto funciona bien, aunque no es una solución "programática", con la advertencia de que el código del cliente no actualiza (todavía, está marcado como XXX en la fuente) la lista cuando agrega / elimina reenvíos sobre la marcha ( ~C)


Si los servidores son Linux, tiene una opción más, esta es la que uso generalmente, aunque para el reenvío local en lugar de remoto. loes 127.0.0.1/8, en Linux puede vincularse de forma transparente a cualquier dirección en 127/8 , por lo que puede usar puertos fijos si usa direcciones 127.xyz únicas, por ejemplo:

mr@local:~$ ssh -R127.53.50.55:44284:127.0.0.1:44284 remote
[...]
mr@remote:~$ ss -atnp src 127.53.50.55
State      Recv-Q Send-Q        Local Address:Port          Peer Address:Port 
LISTEN     0      128            127.53.50.55:44284                    *:*    

Esto está sujeto a la vinculación de puertos privilegiados <1024, OpenSSH no es compatible con las capacidades de Linux y tiene una comprobación de UID codificada en la mayoría de las plataformas.

Los octetos sabiamente elegidos (mnemotécnicos ordinales ASCII en mi caso) ayudan a desenredar el desastre al final del día.

Sr. púrpura
fuente