Pergunta (TL; DR)
Ao atribuir portas dinamicamente para encaminhamento remoto (também conhecido como -R
opção), como um script na máquina remota (por exemplo, originado de .bashrc
) pode determinar quais portas foram escolhidas pelo OpenSSH?
Fundo
Eu uso OpenSSH (em ambas as extremidades) para conectar-me ao nosso servidor central, que compartilho com vários outros usuários. Para minha sessão remota (por enquanto), gostaria de encaminhar X, cups e pulseaudio.
O mais trivial é encaminhar X, usando a -X
opção. O endereço X alocado é armazenado na variável ambiental DISPLAY
e a partir disso posso determinar a porta TCP correspondente, na maioria dos casos, de qualquer maneira. Mas quase nunca preciso, porque o Xlib honra DISPLAY
.
Preciso de um mecanismo semelhante para cups e pulseaudio. Os fundamentos para ambos os serviços existem, na forma das variáveis ambientais CUPS_SERVER
e PULSE_SERVER
, respectivamente. Aqui estão exemplos 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
O problema está na configuração CUPS_SERVER
e PULSE_SERVER
corretamente.
Usamos muito encaminhamento de porta e, portanto, preciso de alocações dinâmicas de porta. As alocações de portas estáticas não são uma opção.
OpenSSH possui um mecanismo para alocação dinâmica de portas no servidor remoto, especificando 0
como bind-port para encaminhamento remoto (a -R
opção). Usando o comando a seguir, o OpenSSH alocará portas dinamicamente para cups e encaminhamento de pulso.
ssh -X -R0:localhost:631 -R0:localhost:4713 datserver
Quando eu uso esse comando, ssh
imprimirei o seguinte em 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
Aí está a informação que eu quero! Em última análise, quero gerar:
export CUPS_SERVER=localhost:41273
export PULSE_SERVER=localhost:55710
No entanto, as mensagens "Porta alocada ..." são criadas na minha máquina local e enviadas para STDERR
, que não consigo acessar na máquina remota. Curiosamente, o OpenSSH não parece ter meios de recuperar informações sobre encaminhamentos de porta.
Como faço para buscar essas informações para colocá-las em um script de shell para configurá-las adequadamente CUPS_SERVER
e PULSE_SERVER
no host remoto?
Becos-sem-saída
A única coisa fácil que consegui encontrar foi aumentar a verbosidade sshd
até que as informações pudessem ser lidas nos logs. Isso não é viável, pois essas informações divulgam muito mais informações do que seria sensato tornar acessíveis a usuários não-root.
Eu estava pensando em corrigir o OpenSSH para suportar uma sequência de escape adicional que imprime uma boa representação do interno struct permitted_opens
, mas mesmo que seja isso que eu quero, ainda não consigo criar scripts para acessar as sequências de escape do cliente do lado do servidor.
Deve haver uma maneira melhor
A abordagem a seguir parece muito instável e está limitada a uma sessão SSH por usuário. No entanto, preciso de pelo menos duas sessões simultâneas e de outros usuários ainda mais. Mas eu tentei...
Quando as estrelas estão alinhadas corretamente, tendo sacrificado uma ou duas galinhas, posso abusar do fato de sshd
não ser iniciado como meu usuário, mas descartar privilégios após login bem-sucedido, para fazer isso:
obter uma lista de números de porta para todos os soquetes de escuta que pertencem ao meu usuário
netstat -tlpen | grep ${UID} | sed -e 's/^.*:\([0-9]\+\) .*$/\1/'
obter uma lista de números de porta para todos os soquetes de escuta que pertencem aos processos iniciados pelo meu usuário
lsof -u ${UID} 2>/dev/null | grep LISTEN | sed -e 's/.*:\([0-9]\+\) (LISTEN).*$/\1/'
Todas as portas que estão no primeiro conjunto, mas não no segundo conjunto, têm uma grande probabilidade de serem minhas portas de encaminhamento e, de fato, subtrair os rendimentos dos conjuntos
41273
e ; xícaras, pulso e X, respectivamente.55710
6010
6010
é identificado como a porta X usandoDISPLAY
.41273
é a porta cups, porquelpstat -h localhost:41273 -a
retorna0
.55710
é a porta de pulso, porquepactl -s localhost:55710 stat
retorna0
. (Até imprime o nome do host do meu cliente!)
(Para fazer o conjunto de subtração I sort -u
e armazenar a saída das linhas de comando acima e usar comm
para fazer a subtração.)
O Pulseaudio me permite identificar o cliente e, para todos os efeitos, isso pode servir como uma âncora para separar sessões SSH que precisam ser separadas. No entanto, não encontrei uma maneira de vincular 41273
e 55710
ao 6010
mesmo sshd
processo. netstat
não divulgará essas informações para usuários não-root. Só recebo um -
na PID/Program name
coluna onde gostaria de ler 2339/54
(neste caso específico). Tão perto ...
Responder1
Infelizmente não encontrei sua pergunta antes, mas acabei de receber uma resposta muito boa de Kamil-maciorowski:
https://unix.stackexchange.com/a/584505/251179
Em resumo, estabeleça primeiro uma conexão mestre, mantenha-a em segundo plano e, em seguida, emita um segundo comando -O *ctl_cmd*
definido como forward
para solicitar/configurar o encaminhamento de porta:
ssh -fNMS /path/to/socket user@server
port="$(ssh -S /path/to/socket -O forward -R 0:localhost:22 placeholder)"
Isso fornecerá a você $port
sua máquina local e uma conexão em segundo plano.
Você pode então usar $port
localmente; ou use ssh
novamente para executar um comando no servidor remoto, onde você pode usar o mesmo soquete de controle.
Quanto às bandeiras, são elas:
- -f= Solicita que o ssh vá para segundo plano.
- -N= Não execute um comando remoto.
- -M= Coloca o cliente no modo “master”, para compartilhamento de conexão.
- -S= Localização de um soquete de controle para compartilhamento de conexão.
- -O= Controlar um processo mestre de multiplexação de conexão ativa.
No meu caso, adicionei um pouco mais para continuar verificando a conexão:
#!/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
Responder2
Consegui o mesmo criando um pipe no cliente local e, em seguida, redirecionando o stderr para o pipe que também é redirecionado para a entrada do ssh. Não são necessárias múltiplas conexões ssh para presumir uma porta conhecida livre que pode falhar. Dessa forma, o banner de logon e o texto "Porta alocada ### ..." são redirecionados para o host remoto.
Eu tenho um script simples no host getsshport.sh
que é executado no host remoto, que lê a entrada redirecionada e analisa a porta. Enquanto esse script não terminar, o encaminhamento remoto do ssh permanecerá aberto.
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
é um pequeno truque para trocar stderr e stdout, de modo que stderr seja canalizado para cat e toda a saída normal do ssh seja mostrada em 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
Eu tentei grep
a mensagem "porta alocada" no lado local antes de enviá-la através do ssh, mas parece que o ssh bloqueará a espera pela abertura do canal no stdin. grep não abre o pipe para escrita até receber algo, então isso basicamente gera um impasse. cat
no entanto, não parece ter o mesmo comportamento e abre o canal para gravação imediatamente, permitindo que o ssh abra a conexão.
este é o mesmo problema no lado remoto, e por que read
linha por linha em vez de apenas grep from stdin - caso contrário `/tmp/allocatedports' não é escrito até que o túnel ssh seja fechado, o que anula todo o propósito
É preferível canalizar o stderr do ssh para um comando como ~/getsshport.sh
, pois sem especificar um comando, o texto do banner ou qualquer outra coisa que esteja no canal é executado no shell remoto.
Responder3
Pegue dois (veja o histórico para uma versão que nãoscpdo lado do servidor e é um pouco mais simples), isso deve bastar. A essência disso é esta:
- passar uma variável de ambiente do cliente para o servidor, informando ao servidor como ele pode detectar quando as informações da porta estão disponíveis e então obtê-las e usá-las.
- assim que as informações da porta estiverem disponíveis, copie-as do cliente para o servidor, permitindo que o servidor as obtenha (com a ajuda da parte 1 acima) e use-as
Primeiro, configure no lado remoto, você precisa habilitar o envio de uma variável env emsshdconfiguração:
sudo yourfavouriteeditor /etc/ssh/sshd_config
Encontre a linha AcceptEnv
e adicione MY_PORT_FILE
-a (ou adicione a linha na Host
seção direita, se ainda não houver). Para mim a linha ficou assim:
AcceptEnv LANG LC_* MY_PORT_FILE
Lembre-se também de reiniciarsshdpara que isso tenha efeito.
Além disso, para que os scripts abaixo funcionem, faça- mkdir ~/portfiles
o remotamente!
Então, no lado local, um trecho de script que irá
- crie um nome de arquivo temporário para redirecionamento stderr
- deixe um trabalho em segundo plano para esperar que o arquivo tenha conteúdo
- passe o nome do arquivo para o servidor como variável env, enquanto redirecionasshstderr para o arquivo
- trabalho em segundo plano continua copiando o arquivo temporário stderr para o lado do servidor usando separadoscp
- o trabalho em segundo plano também copia um arquivo de sinalização para o servidor para indicar que o arquivo stderr está pronto
O trecho do 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
Em seguida, um trecho para o lado remoto, adequado 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
Observação: O código acima, obviamente, não foi testado exaustivamente e pode conter todos os tipos de bugs, erros de copiar e colar, etc.Use por sua conta e risco!Eu testei usando apenas a conexão localhost e funcionou para mim, no meu ambiente de teste. YMMV.
Responder4
Este é complicado, o tratamento extra do lado do servidor nos moldes de SSH_CONNECTION
or DISPLAY
seria ótimo, mas não é fácil de adicionar: parte do problema é que apenas o ssh
cliente conhece o destino local, o pacote de solicitação (para o servidor) contém apenas o endereço remoto e a porta.
As outras respostas aqui possuem várias soluções pouco bonitas para capturar esse lado do cliente e enviá-lo ao servidor. Aqui está uma abordagem alternativa que não é muito mais bonita para ser honesto, mas pelo menos essa parte feia é mantida do lado do cliente ;-)
- lado do cliente, adicione/altere
SendEnv
para que possamos enviar algumas variáveis de ambiente nativamente por ssh (provavelmente não padrão) - lado do servidor, adicione/altere
AcceptEnv
para aceitar o mesmo (provavelmente não habilitado por padrão) - monitore a
ssh
saída stderr do cliente com uma biblioteca carregada dinamicamente e atualize o ambiente do cliente sshdurante a configuração da conexão - pegue as variáveis de ambiente do lado do servidor no script de perfil/login
Isso funciona (felizmente, pelo menos por enquanto) porque os encaminhamentos remotos são configurados e gravados antes da troca do ambiente (confirme com ssh -vv ...
). A biblioteca carregada dinamicamente deve capturar a write()
função libc ( ssh_confirm_remote_forward()
→ logit()
→ do_log()
→ write()
). Redirecionar ou agrupar funções em um binário ELF (sem recompilar) é muito mais complexo do que fazer o mesmo para uma função em uma biblioteca dinâmica.
No cliente .ssh/config
(ou linha de comando -o SendEnv ...
)
Host somehost
user whatever
SendEnv SSH_RFWD_*
No servidor sshd_config
(alteração raiz/administrativa necessária)
AcceptEnv LC_* SSH_RFWD_*
Esta abordagem funciona para clientes Linux e não requer nada de especial no servidor; deve funcionar para outros *nix com alguns pequenos ajustes. Funciona pelo menos OpenSSH 5.8p1 até 7.5p1.
Compilar com gcc -Wall -shared -ldl -Wl,-soname,rfwd -o rfwd.so rfwd.c
Invocar com:
LD_PRELOAD=./rfwd.so ssh -R0:127.0.0.1:4713 -R0:localhost:631 somehost
O 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);
}
(Existem algumas armadilhas de urso glibc relacionadas ao controle de versão de símbolos com esta abordagem, mas write()
não apresentam esse problema.)
Se você estiver com coragem, poderá pegar o setenv()
código relacionado e corrigi-lo na ssh.c
ssh_confirm_remote_forward()
função de retorno de chamada.
Isso define variáveis de ambiente denominadas SSH_RFWD_nnn
, inspecione-as em seu perfil, por exemplo, embash
for fwd in ${!SSH_RFWD_*}; do
IFS=" :" read lport rip rport <<< ${!fwd}
[[ $rport -eq "631" ]] && export CUPS_SERVER=localhost:$lport
# ...
done
Ressalvas:
- não há muitos erros na verificação do código
- mudando o ambientepoderiacausar problemas relacionados ao thread, o PAM usa threads, não espero problemas, mas não testei isso
ssh
atualmente não registra claramente o encaminhamento completo no formato * local:port:remote:port* (se necessário, seria necessária uma análise adicional dasdebug1
mensagensssh -v
), mas você não precisa disso para o seu caso de uso
Curiosamente, o OpenSSH não parece ter meios de recuperar informações sobre encaminhamentos de porta.
Você pode (parcialmente) fazer isso interativamente com o escape ~#
, estranhamente a implementação pula os canais que estão escutando, lista apenas os abertos (ou seja, TCP ESTABLISHED) e não imprime os campos úteis em nenhum caso. Verchannels.c
channel_open_message()
Você pode corrigir essa função para imprimir os detalhes dos SSH_CHANNEL_PORT_LISTENER
slots, mas isso só fornece os encaminhamentos locais (canaisnão são a mesma coisa que reaispara a frente). Ou você pode corrigi-lo para despejar as duas tabelas de encaminhamento da options
estrutura 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));
}
Isso funciona bem, embora não seja uma solução "programática", com a ressalva de que o código do cliente não (ainda está sinalizado como XXX na fonte) atualiza a lista quando você adiciona/remove encaminhamentos on-the-fly ( ~C
)
Se o(s) servidor(es) for(em) Linux você tem mais uma opção, esta é a que eu uso geralmente, embora para encaminhamento local em vez de remoto. lo
é 127.0.0.1/8, no Linux você podevincular-se de forma transparente a qualquer endereço em 127/8, então você pode usar portas fixas se usar endereços 127.xyz exclusivos, por exemplo:
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 *:*
Isso está sujeito à ligação de portas privilegiadas <1024, o OpenSSH não suporta recursos do Linux e possui uma verificação de UID codificada na maioria das plataformas.
Octetos sabiamente escolhidos (mnemônicos ordinais ASCII no meu caso) ajudam a desembaraçar a bagunça no final do dia.