Determinar a porta alocada dinamicamente para o OpenSSH RemoteForward

11

Pergunta (TL; DR)

Ao atribuir portas dinamicamente para encaminhamento remoto (também com a opção -R ), como um script na máquina remota (por exemplo, originado de .bashrc ) determina quais portas foram escolhidas pelo OpenSSH?

Antecedentes

Eu uso o OpenSSH (em ambas as extremidades) para conectar 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 o X, usando a opção -X . O endereço X alocado é armazenado na variável de ambiente DISPLAY e a partir disso eu posso determinar a porta TCP correspondente, na maioria dos casos de qualquer maneira. Mas eu quase nunca preciso, porque o Xlib honra DISPLAY .

Eu preciso de um mecanismo semelhante para copos e pulseaudio. Os fundamentos para os dois serviços existem, na forma das variáveis ambientais CUPS_SERVER e PULSE_SERVER , respectivamente. Aqui estão alguns 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 é definir CUPS_SERVER e PULSE_SERVER corretamente.

Usamos muito os reenvios de porta e, portanto, precisamos de alocações de porta dinâmicas. As alocações de porta estática não são uma opção.

O OpenSSH possui um mecanismo para alocação dinâmica de portas no servidor remoto, especificando 0 como porta de ligação para encaminhamento remoto (a opção -R ). Usando o seguinte comando, o OpenSSH alocará dinamicamente portas para copos e pulsos de encaminhamento.

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

Quando eu uso esse comando, ssh imprimirá 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

Existe a informação que eu quero! Por fim, 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. Por incrível que pareça, o OpenSSH parece não ter meios para recuperar informações sobre encaminhamentos de porta.

Como obtenho essas informações para colocá-las em um script de shell para definir adequadamente CUPS_SERVER e PULSE_SERVER no host remoto?

Dead Ends

A única coisa fácil que consegui encontrar foi o aumento da verbosidade do sshd até que essa informação pudesse ser lida a partir dos logs. Isso não é viável, pois essas informações revelam muito mais informações do que é sensato para serem acessadas por usuários não-root.

Eu estava pensando em remendar o OpenSSH para suportar uma seqüência de escape adicional que imprime uma boa representação da estrutura interna permitted_opens , mas mesmo que seja isso que eu quero, ainda não consigo script acessando 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 é limitada a uma sessão SSH por usuário. No entanto, preciso de pelo menos duas sessões simultâneas e 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 que sshd não foi iniciado como meu usuário, mas descarta privilégios após o login bem-sucedido, para fazer isso:

  • obtenha uma lista de números de porta para todos os sockets de escuta que pertencem ao meu usuário

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

  • obtenha uma lista de números de porta para todos os sockets que pertencem aos processos que o meu usuário iniciou

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

  • Todas as portas que estão no primeiro conjunto, mas não no segundo conjunto, têm alta probabilidade de serem minhas portas de encaminhamento e, de fato, subtrair os conjuntos gera 41273 , 55710 e 6010 ; copos, pulso e X, respectivamente.

  • 6010 é identificado como a porta X usando DISPLAY .

  • 41273 é a porta cups, porque lpstat -h localhost:41273 -a retorna 0 .
  • 55710 é a porta de pulso, porque pactl -s localhost:55710 stat retorna 0 . (Até imprime o nome do host do meu cliente!)

(Para fazer a subtração set I sort -u e armazenar a saída das linhas de comando acima e usar comm para fazer a subtração.)

O Pulseaudio permite que eu identifique o cliente e, para todos os efeitos, isso pode servir como uma âncora para separar as sessões SSH que precisam ser separadas. No entanto, não encontrei uma maneira de vincular 41273 , 55710 e 6010 ao mesmo processo sshd . netstat não divulgará essas informações para usuários não-root. Eu só recebo um - na coluna PID/Program name onde eu gostaria de ler 2339/54 (neste caso particular). Tão perto ...

    
por Bananguin 05.05.2014 / 15:03

4 respostas

1

Pegue dois (veja o histórico de uma versão que faz scp do lado do servidor e é um pouco mais simples), isto deve ser feito. A essência disso é esta:

  1. transmita 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, em seguida, obtém e usa essas variáveis.
  2. uma vez que as informações de porta estão disponíveis, copie-as de cliente para servidor, permitindo que o servidor as obtenha (com ajuda da parte 1 acima) e use-as

Primeiro, configure no lado remoto, você precisa habilitar o envio de uma variável env na configuração sshd :

sudo yourfavouriteeditor /etc/ssh/sshd_config

Localize a linha com AcceptEnv e adicione MY_PORT_FILE a ela (ou adicione a linha abaixo da seção direita Host se ainda não houver uma). Para mim, a linha se tornou isso:

AcceptEnv LANG LC_* MY_PORT_FILE

Lembre-se também de reiniciar sshd para que isso tenha efeito.

Além disso, para que os scripts abaixo funcionem, faça mkdir ~/portfiles no lado remoto!

Em seguida, no lado local, um trecho de script que será

  1. crie um nome de arquivo temporário para o redirecionamento stderr
  2. deixe um trabalho em segundo plano para aguardar que o arquivo tenha conteúdo
  3. passa o nome do arquivo para o servidor como variável env, enquanto redireciona ssh stderr para o arquivo
  4. trabalho de plano de fundo continua a copiar o arquivo temporário stderr para o lado do servidor usando scp separado
  5. O trabalho de plano de fundo
  6. também copia um arquivo de sinalizador para o servidor para indicar que o arquivo stderr está pronto

O 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

Em seguida, um snippet 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 totalmente testado e pode conter todos os tipos de erros, copiar e colar erros, etc. Qualquer pessoa que o use também entende, use em seu risco próprio! Eu testei usando apenas conexão localhost, e funcionou para mim, no meu env de teste. YMMV.

    
por 13.05.2014 / 21:40
1

Um snippet para o lado local, adequado 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
    
por 30.10.2014 / 10:47
0

Eu consegui o mesmo criando um pipe no cliente local e redirecionando o stderr para o pipe, que também é redirecionado para a entrada do ssh. Ele não requer várias conexões ssh para presumir uma porta conhecida gratuita 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 este script não terminar, o remetente 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, para que stderr seja canalizado para o cat, e toda a saída normal do ssh é mostrada no 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  is on local port /p" >> /tmp/allocatedports
done

Eu tentei grep a mensagem "alocado port" no lado local primeiro antes de enviá-lo através do ssh, mas parece que o ssh irá bloquear esperando o pipe abrir no stdin. O grep não abre o pipe para escrever até receber algo, então isso basicamente trava. No entanto, o cat parece não ter esse mesmo comportamento e abre o pipe 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 de stdin - caso contrário, '/ tmp / allocatedports' não é gravado até que o túnel ssh seja fechado, o que anula todo o propósito

Piping o stderr do ssh em um comando como ~/getsshport.sh é o preferido, pois sem especificar um comando, o texto do banner, ou qualquer outra coisa no pipe é executada no shell remoto.

    
por 03.03.2017 / 08:55
0

Isso é complicado, lidar com o lado do servidor ao longo das linhas de SSH_CONNECTION ou DISPLAY seria ótimo, mas não é fácil adicionar: parte do problema é que somente o cliente ssh conhece o destino local, o pacote de pedidos (para o servidor) contém apenas o endereço remoto e a porta.

As outras respostas aqui têm várias soluções despretensiosas para capturar esse cliente e enviá-lo para o servidor. Aqui está uma abordagem alternativa que não é muito mais bonita para ser honesta, mas pelo menos essa parte feia é mantida no lado do cliente;)

  • lado do cliente, adicionar / alterar SendEnv para que possamos enviar algumas variáveis de ambiente nativamente por ssh (provavelmente não padrão)
  • do lado do servidor, adicione / altere AcceptEnv para aceitar o mesmo (provavelmente não ativado por padrão)
  • monitora a saída ssh client stderr com uma biblioteca carregada dinamicamente e atualiza o ambiente do cliente ssh durante a configuração da conexão
  • ative o lado do servidor de variáveis de ambiente no perfil / login script

Isso funciona (felizmente, por enquanto) porque os remotos são configurados e gravados antes do ambiente ser trocado (confirme com ssh -vv ... ). A biblioteca carregada dinamicamente tem que capturar a função write() 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 (raiz / alteração 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 de pelo menos OpenSSH 5.8p1 até 7.5p1.

Compile 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 relacionadas a versões de símbolos com esta abordagem, mas write() não tem esse problema.)

Se você estiver se sentindo corajoso, poderá usar o código setenv() e corrigi-lo na função de retorno de chamada ssh.c ssh_confirm_remote_forward() .

Isso define variáveis de ambiente denominadas SSH_RFWD_nnn , inspecione-as em seu perfil, por exemplo, em bash

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

Advertências:

  • não há muita verificação de erros no código
  • alterando o ambiente may causa problemas relacionados ao encadeamento, o PAM usa encadeamentos, não espero problemas, mas não testei isso
  • ssh atualmente não registra claramente o encaminhamento completo do formulário * local: port: remote: port * (se necessário uma análise adicional de debug1 mensagens com ssh -v seria necessária), mas você não precisa disso para o seu caso de uso

Oddly enough OpenSSH does not seem to have means to retrieve Information about port forwardings.

Você pode (parcialmente) fazer isso interativamente com o escape ~# , estranhamente a implementação pula os canais que estão ouvindo, ele apenas lista os abertos (ou seja, TCP ESTABLISHED), e ele não imprime os campos úteis em qualquer caso. Consulte channels.c channel_open_message()

Você pode corrigir essa função para imprimir os detalhes para SSH_CHANNEL_PORT_LISTENER slots, mas isso só lhe dá os encaminhamentos locais ( canais não são a mesma coisa que os encaminhamentos ). Ou, você pode aplicá-lo para despejar as duas tabelas de encaminhamento do global options struct:

#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 atualiza a lista quando você adiciona / remove encaminhamentos on-the-fly ( ~C )

Se o (s) servidor (es) são Linux você tem mais uma opção, esta é a que eu geralmente uso, embora para encaminhamento local ao invés de remoto. lo é 127.0.0.1/8, no Linux você pode se ligar de forma transparente a qualquer endereço em 127/8, então você pode usar portas fixas se você usar endereços 127.xyz únicos, 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 à vinculação de portas com privilégios < 1024, o OpenSSH não suporta os recursos do Linux e possui uma verificação UID codificada na maioria das plataformas.

Octetos inteligentemente escolhidos (mnemônicos ordinais ASCII no meu caso) ajudam a desvendar a bagunça no final do dia.

    
por 12.04.2017 / 20:26