Pregunta (TL;DR)
Al asignar puertos dinámicamente para el reenvío remoto (también conocido como -R
opción), ¿cómo puede un script en la máquina remota (por ejemplo, procedente de .bashrc
) determinar qué puertos fueron elegidos por OpenSSH?
Fondo
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.
El más trivial es reenviar X, usando la -X
opción. La dirección X asignada se almacena en la variable ambiental DISPLAY
y a partir de ahí puedo determinar el puerto TCP correspondiente, en la mayoría de los casos de todos modos. Pero casi nunca lo necesito, porque Xlib respeta DISPLAY
.
Necesito un mecanismo similar para tazas y pulseaudio. Los conceptos básicos para ambos servicios existen, en forma de variables ambientales CUPS_SERVER
y PULSE_SERVER
, respectivamente. A continuación se muestran 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 es configurarlo CUPS_SERVER
correctamente PULSE_SERVER
.
Usamos mucho el reenvío 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, especificando 0
como puerto de enlace para el reenvío remoto (la -R
opción). Al utilizar 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, ssh
imprimiré lo siguiente en 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 última instancia quiero generar:
export CUPS_SERVER=localhost:41273
export PULSE_SERVER=localhost:55710
Sin embargo, los mensajes "Puerto asignado ..." se crean en mi máquina local y se envían a STDERR
, a la que no puedo acceder en la máquina remota. Por extraño que parezca, OpenSSH no parece tener medios para recuperar información sobre reenvíos de puertos.
¿Cómo obtengo esa información para colocarla en un script de shell para configurarla adecuadamente CUPS_SERVER
en PULSE_SERVER
el host remoto?
Callejones sin salida
Lo único fácil que pude encontrar fue aumentar la detalle sshd
hasta que esa información pueda leerse en los registros. Esto no es viable ya que esa información revela mucha más información de la que sería sensato hacer accesible a 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 uno o dos pollos, puedo abusar del hecho de que sshd
no se inicia como mi usuario, pero pierde privilegios después de iniciar sesión exitosamente, para hacer esto:
obtener una lista de números de puerto para todos los sockets 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 los procesos que inició mi usuario
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, tienen una alta probabilidad de ser mis puertos de reenvío y, de hecho, al restar los conjuntos se obtiene
41273
,55710
y6010
; tazas, pulso y X, respectivamente.6010
se identifica como el puerto X usandoDISPLAY
.41273
es el puerto de tazas, porquelpstat -h localhost:41273 -a
regresa0
.55710
es el puerto de pulso, porquepactl -s localhost:55710 stat
devuelve0
. (¡Incluso imprime el nombre de host de mi cliente!)
(Para hacer la resta establecida I sort -u
y almacenar el resultado de las líneas de comando anteriores y usar comm
para hacer la resta).
Pulseaudio me permite identificar al cliente y, para todos los efectos, esto puede servir como ancla para separar las sesiones SSH que necesitan separarse. Sin embargo, no he encontrado una manera de vincularlo 41273
al 55710
mismo 6010
proceso sshd
. netstat
no revelará esa información a usuarios que no sean root. Solo aparece un -
en la PID/Program name
columna donde me gustaría leer 2339/54
(en este caso particular). Tan cerca ...
Respuesta1
Lamentablemente no encontré tu pregunta antes, pero acabo de recibir una muy buena respuesta de kamil-maciorowski:
https://unix.stackexchange.com/a/584505/251179
En resumen, primero establezca una conexión maestra, manténgala en segundo plano y luego emita un segundo comando con -O *ctl_cmd*
set to forward
para solicitar/configurar el reenvío de puertos:
ssh -fNMS /path/to/socket user@server
port="$(ssh -S /path/to/socket -O forward -R 0:localhost:22 placeholder)"
Esto le permitirá acceder $port
a su máquina local y a una conexión en segundo plano.
Luego puedes usarlo $port
localmente; o utilícelo ssh
nuevamente para ejecutar un comando en el servidor remoto, donde puede usar el mismo socket de control.
En cuanto a las banderas, estas son:
- -F= Solicita ssh para pasar al fondo.
- -NORTE= No ejecutar un comando remoto.
- -METRO= Coloca al cliente en modo "maestro", para compartir conexión.
- -S= Ubicación de una toma de control para compartir conexión.
- -O= Controlar un proceso maestro de multiplexación de conexión activa.
En mi caso he añadido un poco más para seguir comprobando la conexión:
#!/bin/bash
#--------------------------------------------------
# Setup
#--------------------------------------------------
set -u;
tunnel_user="user";
tunnel_host="1.1.1.1";
local_port="22";
local_name="my-name";
path_key="$HOME/.ssh/tunnel_ed25519";
path_lock="/tmp/tunnel.${tunnel_host}.pid"
path_port="/tmp/tunnel.${tunnel_host}.port"
path_log="/tmp/tunnel.${tunnel_host}.log"
path_socket="/tmp/tunnel.${tunnel_host}.socket"
#--------------------------------------------------
# Key file
#--------------------------------------------------
if [ ! -f "${path_key}" ]; then
ssh-keygen -q -t ed25519 -f "${path_key}" -N "";
/usr/local/bin/tunnel-client-key.sh
# Sends the public key to a central server, also run via cron, so it can be added to ~/.ssh/authorized_keys
# curl -s --form-string "pass=${pass}" --form-string "name=$(local_name)" -F "key=@${path_key}.pub" "https://example.com/key/";
fi
#--------------------------------------------------
# Lock
#--------------------------------------------------
if [ -e "${path_lock}" ]; then
c=$(pgrep -F "${path_lock}" 2>/dev/null | wc -l);
# MacOS 10.15.4 does not support "-c" to count processes, or the full "--pidfile" flag.
else
c=0;
fi
if [[ "${c}" -gt 0 ]]; then
if tty -s; then
echo "Already running";
fi;
exit;
fi;
echo "$$" > "${path_lock}";
#--------------------------------------------------
# Port forward
#--------------------------------------------------
retry=0;
while true; do
#--------------------------------------------------
# Log cleanup
#--------------------------------------------------
if [ ! -f "${path_log}" ]; then
touch "${path_log}";
fi
tail -n 30 "${path_log}" > "${path_log}.tmp";
mv "${path_log}.tmp" "${path_log}";
#--------------------------------------------------
# Exit old sockets
#--------------------------------------------------
if [ -S "${path_socket}" ]; then
echo "$(date) : Exit" >> "${path_log}";
ssh -S "${path_socket}" -O exit placeholder;
fi
#--------------------------------------------------
# Lost lock
#--------------------------------------------------
if [ ! -e "${path_lock}" ] || ! grep -q "$$" "${path_lock}"; then
echo "$(date) : Lost Lock" >> "${path_log}";
exit;
fi
#--------------------------------------------------
# Master connection
#--------------------------------------------------
echo "$(date) : Connect ${retry}" >> "${path_log}";
ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 -o ServerAliveInterval=30 -o ExitOnForwardFailure=yes -fNTMS "${path_socket}" -i "${path_key}" "${tunnel_user}@${tunnel_host}" >> "${path_log}" 2>&1;
#--------------------------------------------------
# Setup and keep checking the port forwarding
#--------------------------------------------------
old_port=0;
while ssh -S "${path_socket}" -O check placeholder 2>/dev/null; do
new_port=$(ssh -S "${path_socket}" -O forward -R "0:localhost:${local_port}" placeholder 2>&1);
if [[ "${new_port}" -gt 0 ]]; then
retry=0;
if [[ "${new_port}" -ne "${old_port}" ]]; then
ssh -i "${path_key}" "${tunnel_user}@${tunnel_host}" "tunnel.port.sh '${new_port}' '${local_name}'" >> "${path_log}" 2>&1;
# Tell remote server what the port is, and local_name.
# Don't use socket, it used "-N"; if done, a lost connection keeps sshd running on the remote host, even with ClientAliveInterval/ClientAliveCountMax.
echo "$(date) : ${new_port}" >> "${path_log}";
echo "${new_port}" > "${path_port}";
old_port="${new_port}";
sleep 1;
else
sleep 300; # Looks good, check again in 5 minutes.
fi
else # Not a valid port number (0, empty string, number followed by an error message, etc?)
ssh -S "${path_socket}" -O exit placeholder 2>/dev/null;
fi
done
#--------------------------------------------------
# Cleanup
#--------------------------------------------------
if [ ! -f "${path_port}" ]; then
rm "${path_port}";
fi
echo "$(date) : Disconnected" >> "${path_log}";
#--------------------------------------------------
# Delay before next re-try
#--------------------------------------------------
retry=$((retry+1));
if [[ $retry -gt 10 ]]; then
sleep 180; # Too many connection failures, try again in 3 minutes
else
sleep 5;
fi
done
Respuesta2
Logré lo mismo creando una tubería en el cliente local y luego redirigiendo stderr a la tubería que también se redirige a la entrada de ssh. No se requieren múltiples conexiones ssh para suponer que hay un puerto conocido libre que podría fallar. De esta manera, el banner de inicio de sesión y el texto "Puerto asignado ### ..." se redirigen al host remoto.
Tengo un script simple en el host getsshport.sh
que se ejecuta en el host remoto y que lee la entrada redirigida y analiza el puerto. Mientras este script no finalice, el reenvío 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
Primero intenté recibir grep
el mensaje de "puerto asignado" en el lado local antes de enviarlo a través de ssh, pero parece que ssh se bloqueará esperando a que se abra la tubería en la entrada estándar. grep no abre la tubería para escribir hasta que recibe algo, por lo que básicamente esto se bloquea. cat
sin embargo, no parece tener el mismo comportamiento y abre la tubería para escritura inmediatamente, permitiendo que ssh abra la conexión.
este es el mismo problema en el lado remoto, y por qué read
línea por línea en lugar de simplemente grep desde stdin; de lo contrario, `/tmp/allocatedports' no se escribe hasta que se cierra el túnel ssh, lo que anula todo el propósito.
Se prefiere canalizar el stderr de ssh en un comando como ~/getsshport.sh
, ya que sin especificar un comando, el texto del banner o cualquier otra cosa que esté en la canalización se ejecuta en el shell remoto.
Respuesta3
Tome dos (consulte el historial para ver una versión que noscpdesde el lado del servidor y es un poco más simple), esto debería ser suficiente. La esencia es esta:
- pasar una variable de entorno del cliente al servidor, diciéndole al servidor cómo puede detectar cuándo la información del puerto está disponible y luego obtenerla y usarla.
- Una vez que la información del puerto esté disponible, cópiela del cliente al servidor, permitiendo que el servidor la obtenga (con la ayuda de la parte 1 anterior) y úsela.
Primero, configure en el lado remoto, debe habilitar el envío de una variable env ensshdconfiguración:
sudo yourfavouriteeditor /etc/ssh/sshd_config
Busque la línea con AcceptEnv
y agréguela MY_PORT_FILE
(o agregue la línea en Host
la sección derecha si aún no hay una). Para mí la línea se convirtió en esta:
AcceptEnv LANG LC_* MY_PORT_FILE
Recuerda también reiniciarsshdpara que esto surta efecto.
Además, para que los siguientes scripts funcionen, ¡hágalo mkdir ~/portfiles
en el lado remoto!
Luego, en el lado local, un fragmento de guión que
- crear un nombre de archivo temporal para la redirección stderr
- dejar un trabajo en segundo plano para esperar a que el archivo tenga contenido
- pasar el nombre del archivo al servidor como variable env, mientras se redirigesshstderr al archivo
- El trabajo en segundo plano procede a copiar el archivo temporal stderr al lado del servidor utilizando un método separado.scp
- El trabajo en segundo plano también copia un archivo de bandera al servidor para indicar que el archivo stderr está listo
El fragmento de guión:
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 del 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: El código anterior, por supuesto, no se ha probado muy exhaustivamente y podría contener todo tipo de errores, errores de copiar y pegar, etc. Es mejor que cualquiera que lo use también lo comprenda.¡Úselo bajo su propio riesgo!Lo probé usando solo una conexión de host local y funcionó para mí en mi entorno de prueba. YMMV.
Respuesta4
Esto es complicado, un manejo adicional del lado del servidor similar a SSH_CONNECTION
o DISPLAY
sería excelente, pero no es fácil de agregar: parte del problema es que solo el ssh
cliente conoce el destino local, el paquete de solicitud (al servidor) contiene sólo la dirección remota y el puerto.
Las otras respuestas aquí tienen varias soluciones poco atractivas para capturar este lado del cliente y enviarlo al servidor. Aquí hay un enfoque alternativo que, para ser honesto, no es mucho más bonito, pero al menos esta fea fiesta se mantiene del lado del cliente ;-)
- lado del cliente, agregar/modificar
SendEnv
para que podamos enviar algunas variables de entorno de forma nativa a través de ssh (probablemente no por defecto) - lado del servidor, agregar/modificar
AcceptEnv
para aceptar lo mismo (probablemente no habilitado de forma predeterminada) - monitorear la
ssh
salida stderr del cliente con una biblioteca cargada dinámicamente y actualizar el entorno del cliente sshdurante el establecimiento de la conexión - seleccione las variables de entorno del lado del servidor en el script de perfil/inicio de sesión
Esto funciona (afortunadamente, al menos por ahora) porque los reenvíos remotos se configuran y registran antes de que se intercambie el entorno (confirme con ssh -vv ...
). La biblioteca cargada dinámicamente tiene que capturar la write()
función libc ( ssh_confirm_remote_forward()
→ logit()
→ do_log()
→ write()
). Redirigir o ajustar funciones en un binario ELF (sin recompilar) es mucho más complejo que hacer lo mismo con 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 raíz/administrativo)
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
Invocar 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);
}
(Existen algunas trampas para osos de glibc relacionadas con el control de versiones de símbolos con este enfoque, pero write()
no tienen este problema).
Si te sientes valiente, puedes tomar el setenv()
código relacionado y parchearlo en ssh.c
ssh_confirm_remote_forward()
la función de devolución de llamada.
Esto establece variables de entorno denominadas SSH_RFWD_nnn
, inspecciónelas 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
- cambiando el medio ambientepuedecausa problemas relacionados con subprocesos, PAM usa subprocesos, no espero problemas pero no lo he probado
ssh
actualmente no registra claramente el reenvío completo del formulario * local:puerto:remoto:puerto* (si es necesario, se requerirá un análisis adicional dedebug1
los mensajesssh -v
), pero no necesita esto para su caso de uso
Por extraño que parezca, OpenSSH no parece tener medios para recuperar información sobre reenvíos de puertos.
Puede hacer esto (en parte) de forma interactiva con 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 SSH_CHANNEL_PORT_LISTENER
las ranuras, pero eso solo le proporciona los reenvíos locales (canalesno son lo mismo que los realeshacia adelante). O puede parchearlo para volcar las dos tablas de reenvío de la options
estructura 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 salvedad de que el código del cliente no actualiza (aún, está marcado como XXX en el código 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 reenvío local en lugar de remoto. lo
es 127.0.0.1/8, en Linux puedesvincularse de forma transparente a cualquier dirección en 127/8, por lo que puede utilizar puertos fijos si utiliza 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 admite capacidades de Linux y tiene una verificación de UID codificada en la mayoría de las plataformas.
Los octetos sabiamente elegidos (mnemónicos ordinales ASCII en mi caso) ayudan a desenredar el lío al final del día.