Determinar porta alocada dinamicamente para OpenSSH RemoteForward

13

Pergunta (TL; DR)

Ao atribuir portas dinamicamente para encaminhamento remoto ( -Ropção aka ), como um script na máquina remota (por exemplo, proveniente de .bashrc) determina quais portas foram escolhidas pelo OpenSSH?


fundo

Eu uso o OpenSSH (nas duas extremidades) para conectar-se ao nosso servidor central, que eu compartilho com vários outros usuários. Para a minha sessão remota (por enquanto), gostaria de encaminhar X, cups e pulseaudio.

O mais trivial é encaminhar o X, usando a -Xopção O endereço X alocado é armazenado na variável ambiental DISPLAYe, a partir disso, posso determinar a porta TCP correspondente, na maioria dos casos. Mas quase nunca preciso, porque Xlib honra DISPLAY.

Eu preciso de um mecanismo semelhante para copos e pulseaudio. O básico para ambos os serviços existe, na forma de variáveis ​​ambientais CUPS_SERVERe 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 está definido CUPS_SERVERe PULSE_SERVERcorretamente.

Usamos muito o encaminhamento de portas e, portanto, preciso de alocações dinâmicas de portas. Alocações de porta estática não são uma opção.

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

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

Quando eu uso esse comando, sshimprimo o seguinte 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

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 as STDERRquais não consigo acessar na máquina remota. Curiosamente, o OpenSSH parece não ter meios de recuperar informações sobre encaminhamentos de portas.

Como obtenho essas informações para colocá-las em um script shell para definir adequadamente CUPS_SERVERe PULSE_SERVERno host remoto?


Becos-sem-saída

A única coisa fácil que pude encontrar foi aumentar a verbosidade sshdaté que essas informações possam ser lidas nos logs. Isso não é viável, pois essas informações divulgam muito mais informações do que é sensato tornar acessível por usuários não-root.

Eu estava pensando em aplicar um patch ao OpenSSH para oferecer suporte a uma sequência de escape adicional que imprime uma boa representação da estrutura interna permitted_opens, mas mesmo se é isso que eu quero, ainda não consigo script acessando as seqüências de escape do cliente pelo 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 outros usuários ainda mais. Mas eu tentei ...

Quando as estrelas estão alinhadas corretamente, depois de sacrificar uma galinha ou duas, posso abusar do fato de sshdnão ter sido iniciado como meu usuário, mas eliminado privilégios após o 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 que meu usuário iniciou

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

  • Todas as portas que estão no primeiro set, mas não no segundo set têm uma alta probabilidade de ser meus portos de encaminhamento e, na verdade subtrair os rendimentos conjuntos 41273, 55710e 6010; copos, pulso e X, respectivamente.

  • 6010é identificado como a porta X usando DISPLAY.

  • 41273é a porta de copos, porque lpstat -h localhost:41273 -aretorna 0.
  • 55710é a porta de pulso, porque pactl -s localhost:55710 statretorna 0. (Ele até imprime o nome do host do meu cliente!)

(Para fazer a subtração definida I sort -ue armazenar a saída das linhas de comando acima e use commpara fazer a subtração.)

O Pulseaudio permite identificar o cliente e, para todos os efeitos, pode servir de âncora para separar as sessões SSH que precisam ser separadas. No entanto, eu não encontrei uma maneira de amarrar 41273, 55710e 6010ao mesmo sshdprocesso. netstatnão divulgará essas informações para usuários não raiz. Eu só recebo um -na PID/Program namecoluna em que gostaria de ler 2339/54(neste exemplo em particular). Tão perto ...

Bananguin
fonte
fwiw, é mais preciso dizer que netstatnão mostrará o PID para processos que você não possui ou que são espaço no kernel. Por exemplo
Bratchley
A maneira mais robusta seria corrigir o sshd ... Um patch rápido e sujo seria apenas algumas linhas no local em que o servidor obtém sua porta local do SO, gravando o número da porta em um arquivo, nome gerado pelo usuário, host remoto e porta. Supondo que o servidor conheça a porta no lado do cliente, o que não é certo, talvez nem provável (caso contrário, o recurso já existiria).
Hyde 14/05
@ hyde: exatamente. O servidor remoto não conhece as portas encaminhadas. Ele apenas cria alguns soquetes de escuta e os dados são encaminhados através da conexão ssh. Não conhece as portas de destino locais.
Bananguin 14/05

Respostas:

1

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

  1. passe 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, obtenha e use-as.
  2. quando 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 na configuração do sshd :

sudo yourfavouriteeditor /etc/ssh/sshd_config

Encontre a linha com AcceptEnve adicione MY_PORT_FILEa ela (ou adicione a linha na Hostseção direita , se ainda não houver uma). Para mim, a linha tornou-se esta:

AcceptEnv LANG LC_* MY_PORT_FILE

Lembre-se também de reiniciar o sshd para que isso entre em vigor.

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


No lado local, um trecho de script que será

  1. criar nome do arquivo temporário para redirecionamento stderr
  2. deixe um trabalho em segundo plano para aguardar o arquivo ter conteúdo
  3. passe o nome do arquivo para o servidor como variável env, enquanto redireciona o ssh stderr para o arquivo
  4. o trabalho em segundo plano prossegue para copiar o arquivo temporário stderr para o lado do servidor usando scp separado
  5. 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 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 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

Nota : É claro que o código acima não é exaustivamente testado e pode conter todos os tipos de bugs, erros de copiar e colar, etc. Qualquer pessoa que o utilize melhor também o entenderá, use por sua conta e risco! Testei-o usando apenas a conexão localhost e funcionou para mim no meu ambiente de teste. YMMV.

hyde
fonte
O que, é claro, exige que eu possa scpdo lado remoto para o local, o que não posso. Eu tinha uma abordagem semelhante, mas seria envolvida sshem segundo plano depois de estabelecer a conexão, depois enviá-lo do local para o remoto via scpe, em seguida, puxar o sshcliente para o primeiro plano e executar um script no lado remoto. Não descobri como criar scripts de plano de fundo e primeiro plano para processos locais e remotos. Embrulhar e integrar o sshcliente local a alguns scripts remotos como esse não parece ser uma boa abordagem.
Bananguin 13/05
Ah Eu acho que você deveria fundo do lado do cliente scp apenas: (while [ ... ] ; do sleep 1 ; done ; scp ... )&. Em seguida, aguarde em primeiro plano no servidor .bashrc(supondo que o cliente envie a variável env correta) para o arquivo aparecer. Atualizarei a resposta mais tarde após alguns testes (provavelmente não haverá tempo até amanhã).
Hyde 14/05
@Banguanguin Nova versão concluída. Parece funcionar para mim, portanto, deve ser adaptável ao seu caso de uso. Sobre "boa abordagem", sim, mas não acho que seja realmente possível uma boa abordagem aqui. As informações precisam ser passadas de alguma maneira e sempre serão um hack, a menos que você faça o patch do cliente e do servidor ssh para fazê-lo de maneira limpa na conexão única.
Hyde 14/05
E eu estou pensando cada vez mais sobre o patch openssh. Não parece ser grande coisa. A informação já está disponível. Eu só preciso enviá-lo para o servidor. Sempre que o servidor recebe essa informação ele grava-lo para~/.ssh-${PID}-forwards
Bananguin
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
ToxeH
fonte
0

Consegui o mesmo criando um canal no cliente local e redirecionando o stderr para o canal que também é redirecionado para a entrada do ssh. Não requer várias conexões ssh para presumir uma porta conhecida livre que possa 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.shque é executado no host remoto que lê a entrada redirecionada e analisa a porta. Enquanto esse script não terminar, o encaminhamento remoto 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 o stderr seja canalizado para cat e toda a saída normal do ssh seja 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 \3 is on local port \1/p" >> /tmp/allocatedports
done

Eu tentei grepa mensagem "porta alocada" no lado local primeiro antes de enviá-lo através de ssh, mas parece que o ssh irá bloquear esperando o tubo para abrir em stdin. O grep não abre o pipe para escrever até receber algo, então isso basicamente gera um impasse. catno entanto, parece não ter esse 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 readlinha por linha, em vez de apenas grep do stdin - caso contrário, `/ tmp / alocados 'não será gravado até que o túnel ssh seja fechado, o que derrota todo o propósito

~/getsshport.shÉ preferível canalizar o stderr do ssh em um comando como , pois sem especificar um comando, o texto do banner ou o que mais estiver no pipe é executado no shell remoto.

JesseMcL
fonte
legais. eu adicionei renice +10 $$; exec catantes do donepara economizar recursos.
Spongman
0

Essa é uma manipulação complicada, do lado do servidor, semelhante SSH_CONNECTIONou DISPLAYseria ótima, mas não é fácil de acrescentar: parte do problema é que apenas o sshcliente conhece o destino local, o pacote de solicitação (para o servidor) contém somente o endereço e a porta remotos.

As outras respostas aqui têm várias soluções despretensiosas para capturar esse lado do cliente e enviá-lo ao 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 ;-)

  • do lado do cliente, adicione / corrija SendEnvpara que possamos enviar algumas variáveis ​​de ambiente nativamente pelo ssh (provavelmente não padrão)
  • servidor, adicione / altere AcceptEnvpara aceitar o mesmo (provavelmente não ativado por padrão)
  • monitore a sshsaída do stderr do cliente com uma biblioteca carregada dinamicamente e atualize o ambiente do cliente ssh durante a configuração da conexão
  • escolha o lado do servidor de variáveis ​​de ambiente no script de perfil / login

Isso funciona (felizmente, por enquanto, de qualquer maneira) porque os encaminhadores remotos são configurados e registrados antes da troca do ambiente (confirme com ssh -vv ...). A biblioteca carregada dinamicamente precisa 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) é uma ordem de magnitude mais complexa 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(é necessária alteração raiz / administrativa)

AcceptEnv LC_* SSH_RFWD_*

Essa 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 a 7.5p1.

Compilar com gcc -Wall -shared -ldl -Wl,-soname,rfwd -o rfwd.so rfwd.c Invoke 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 glibc bear relacionadas ao versionamento de símbolo com essa abordagem, mas write()não há esse problema.)

Se você se sente corajoso, pode 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 nomeadas SSH_RFWD_nnn, inspecione-as no 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á muito erro ao verificar o código
  • mudar o ambiente pode causar problemas relacionados a threads, o PAM usa threads, não espero problemas, mas não testei isso
  • sshno momento, não registra claramente o encaminhamento completo do formulário * local: port: remote: port * (se necessário, é necessária uma análise adicional de debug1mensagens ssh -v), mas você não precisa disso para o seu caso de uso

Curiosamente, o OpenSSH parece não ter meios de recuperar informações sobre encaminhamentos de portas.

Você pode (em parte) fazer isso de maneira interativa com a fuga ~#, estranhamente a implementação ignora os canais que estão ouvindo, apenas lista os abertos (por exemplo, TCP ESTABELECIDO) e, em nenhum caso, imprime os campos úteis. Vejochannels.c channel_open_message()

Você pode corrigir essa função para imprimir os detalhes dos SSH_CHANNEL_PORT_LISTENERslots, mas isso apenas fornece os encaminhamentos locais (os canais não são a mesma coisa que os encaminhamentos reais ). Ou você pode corrigi-lo para despejar as duas tabelas de encaminhamento da optionsestrutura 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 (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 Linux, você tem mais uma opção, essa é a que eu geralmente uso, embora seja para encaminhamento local e não remoto. loé 127.0.0.1/8, no Linux você pode vincular de forma transparente a qualquer endereço no 127/8 ; portanto, 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 Linux e possui uma verificação de UID codificada na maioria das plataformas.

Os octetos sabiamente escolhidos (mnemônicos ordinais ASCII no meu caso) ajudam a desembaraçar a bagunça no final do dia.

mr.spuratic
fonte