Determinare la porta allocata dinamicamente per OpenSSH RemoteForward


13

Domanda (TL; DR)

Quando si assegnano le porte in modo dinamico per l'inoltro remoto ( -Ropzione aka ), come può uno script sul computer remoto (ad esempio proveniente da .bashrc) determinare quali porte sono state scelte da OpenSSH?


sfondo

Uso OpenSSH (su entrambe le estremità) per connettermi al nostro server centrale, che condivido con più altri utenti. Per la mia sessione remota (per ora) vorrei inoltrare X, tazze e pulseaudio.

Il più banale è l'inoltro di X, usando l' -Xopzione. L'indirizzo X allocato viene memorizzato nella variabile ambientale DISPLAYe da questo posso determinare la porta TCP corrispondente, nella maggior parte dei casi comunque. Ma non ne ho quasi mai bisogno, perché Xlib onora DISPLAY.

Ho bisogno di un meccanismo simile per tazze e pulseaudio. Esistono le basi per entrambi i servizi, rispettivamente sotto forma di variabili ambientali CUPS_SERVERe PULSE_SERVER. Ecco alcuni esempi di utilizzo:

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

Il problema si sta configurando CUPS_SERVERe PULSE_SERVERcorrettamente.

Usiamo molto il port forwarding e quindi ho bisogno di allocazioni di porte dinamiche. Le allocazioni di porte statiche non sono un'opzione.

OpenSSH ha un meccanismo per l'allocazione dinamica delle porte sul server remoto, specificando 0come bind-port per l'inoltro remoto (l' -Ropzione). Usando il comando seguente, OpenSSH alloca dinamicamente le porte per le tazze e l'inoltro degli impulsi.

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

Quando uso quel comando, sshstampa quanto segue su 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

Ci sono le informazioni che voglio! Alla fine voglio generare:

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

Tuttavia, i messaggi "Porta assegnata ..." vengono creati sul mio computer locale e inviati a STDERRcui non riesco ad accedere sul computer remoto. Stranamente OpenSSH non sembra avere i mezzi per recuperare le informazioni sul port forwarding.

Come posso recuperare queste informazioni per inserirle in uno script shell per impostarle adeguatamente CUPS_SERVERe PULSE_SERVERsull'host remoto?


Vicoli ciechi

L'unica cosa facile che ho trovato è stata la prolissità crescente sshdfino a quando le informazioni non possono essere lette dai registri. Ciò non è fattibile in quanto tali informazioni rivelano molte più informazioni di quante sia sensato rendere accessibili agli utenti non root.

Stavo pensando di patchare OpenSSH per supportare un'ulteriore sequenza di escape che stampa una bella rappresentazione della struttura interna permitted_opens, ma anche se è quello che voglio, non riesco ancora a accedere alle sequenze di escape del client dal lato server.


Deve esserci un modo migliore

Il seguente approccio sembra molto instabile ed è limitato a una di queste sessioni SSH per utente. Tuttavia, ho bisogno di almeno due sessioni simili e altri utenti ancora di più. Ma ho provato ...

Quando le stelle sono allineate correttamente, dopo aver sacrificato un pollo o due, posso abusare del fatto che sshdnon è iniziato come il mio utente, ma abbandona i privilegi dopo il login riuscito, per fare questo:

  • ottenere un elenco di numeri di porta per tutte le prese di ascolto che appartengono al mio utente

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

  • ottenere un elenco di numeri di porta per tutti i socket di ascolto che appartengono ai processi avviati dal mio utente

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

  • Tutte le porte che si trovano nel primo set, ma non nel secondo set hanno un'alta probabilità di essere le mie porte di inoltro e sottrarre i rendimenti degli insiemi 41273, 55710e 6010; tazze, impulso e X, rispettivamente.

  • 6010viene identificato come la porta X utilizzando DISPLAY.

  • 41273è la porta della coppa, perché lpstat -h localhost:41273 -aritorna 0.
  • 55710è la porta del polso, perché pactl -s localhost:55710 statritorna 0. (Stampa persino il nome host del mio client!)

(Per eseguire la sottostrazione impostata I sort -ue memorizzare l'output dalle righe di comando sopra e utilizzare commper eseguire la sottostrazione.)

Pulseaudio mi consente di identificare il client e, a tutti gli effetti, questo può servire da ancoraggio per separare le sessioni SSH che devono essere separate. Tuttavia, non ho trovato un modo per legare 41273, 55710e 6010allo stesso sshdprocesso. netstatnon divulgherà tali informazioni a utenti non root. Ottengo solo un -nella PID/Program namecolonna in cui vorrei leggere 2339/54(in questo caso particolare). Così vicino ...


prima di tutto, è più preciso affermare che netstatnon ti mostrerà il PID per i processi che non possiedi o che sono spazio del kernel. Ad esempio
Bratchley

Il modo più affidabile sarebbe quello di patchare l'sshd ... Una patch veloce e sporca sarebbe solo poche righe nel punto in cui il server ottiene la sua porta locale dal sistema operativo, scrivendo il numero di porta su un file, il nome generato dall'utente, l'host remoto e porta. Supponendo che il server conosca la porta sul lato client, il che non è certo, forse nemmeno probabile (altrimenti la funzionalità esisterebbe già).
hyde,

@hyde: esattamente. Il server remoto non è a conoscenza delle porte inoltrate. Crea solo alcuni socket di ascolto e i dati vengono inoltrati attraverso la connessione SSH. Non conosce le porte di destinazione locali.
Bananguin,

Risposte:


1

Prendi due (vedi la cronologia di una versione che fa scp dal lato server ed è un po 'più semplice), questo dovrebbe farlo. L'essenza è questa:

  1. passare una variabile di ambiente da client a server, indicando al server come rilevare quando sono disponibili le informazioni sulla porta e quindi ottenerle e utilizzarle.
  2. una volta che le informazioni sulla porta sono disponibili, copiarle dal client al server, consentendo al server di ottenerle (con l'aiuto della parte 1 sopra) e usarle

Innanzitutto, l'installazione sul lato remoto, è necessario abilitare l'invio di una variabile env nella configurazione sshd :

sudo yourfavouriteeditor /etc/ssh/sshd_config

Trova la linea con AcceptEnve aggiungila MY_PORT_FILE(o aggiungi la linea nella Hostsezione destra se non ce n'è ancora una). Per me la linea è diventata questa:

AcceptEnv LANG LC_* MY_PORT_FILE

Ricorda anche di riavviare sshd affinché questo abbia effetto.

Inoltre, affinché gli script seguenti funzionino, esegui mkdir ~/portfilessul lato remoto!


Poi sul lato locale, un frammento di script che lo farà

  1. crea il nome del file temporaneo per il reindirizzamento stderr
  2. lasciare un processo in background per attendere che il file contenga contenuto
  3. passa il nome del file al server come variabile env, reindirizzando ssh stderr al file
  4. il processo in background procede a copiare il file temp stderr sul lato server usando scp separato
  5. il processo in background copia anche un file flag sul server per indicare che il file stderr è pronto

Lo snippet di 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

Quindi uno snippet per il lato remoto, adatto per .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 : il codice sopra riportato non è ovviamente molto testato e potrebbe contenere tutti i tipi di bug, errori di copia e incolla, ecc. Chiunque lo utilizzi meglio lo capisce, lo usa a proprio rischio! L'ho provato usando solo la connessione localhost, e ha funzionato per me, nel mio ambiente di prova. YMMV.


Il che ovviamente richiede scpche io possa farlo dal lato remoto al lato locale, cosa che non posso. Avevo un approccio simile, ma lo avrei avvolto sshin background dopo aver stabilito la connessione, quindi avrei inviato quel file da locale a remoto tramite scpe quindi tirato il sshclient in primo piano ed eseguendo uno script sul lato remoto. Non ho capito bene come eseguire lo scripting di background e in primo piano dei processi locali e remoti. Avvolgere e integrare il sshclient locale con alcuni script remoti del genere non sembra un buon approccio.
Bananguin,

Ah. Penso che si dovrebbe sfondo lato client SCP solo: (while [ ... ] ; do sleep 1 ; done ; scp ... )&. Quindi attendere in primo piano nel server .bashrc(supponendo che il client invii la variabile env corretta) affinché appaia il file. Aggiornerò la risposta più tardi dopo alcuni test (probabilmente non c'è tempo fino a domani).
hyde,

@Bananguin Nuova versione completata. Sembra funzionare per me, quindi dovrebbe essere adattabile al tuo caso d'uso. A proposito di "bel approccio", sì, ma non credo che ci sia davvero un buon approccio possibile qui. Le informazioni devono essere passate in qualche modo, e sarà sempre un hack, a meno che non si corregga il client e il server ssh per farlo in modo pulito sulla singola connessione.
hyde,

E sto pensando sempre più al patching openssh. Non sembra essere un grosso problema. Le informazioni sono già disponibili. Devo solo inviarlo al server. Ogni volta che il server riceve tali informazioni, le scrive a~/.ssh-${PID}-forwards
Bananguin,

1

Uno snippet per il lato locale, adatto per .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

0

Ho ottenuto lo stesso risultato creando una pipe sul client locale, quindi reindirizzando stderr alla pipe che viene anche reindirizzata all'input di ssh. Non richiede più connessioni SSH per presumere una porta nota libera che potrebbe non riuscire. In questo modo il banner di accesso e il testo "Allocated port ### ..." vengono reindirizzati all'host remoto.

Ho un semplice script sull'host getsshport.shche viene eseguito sull'host remoto che legge l'input reindirizzato e analizza la porta. Finché questo script non termina, l'inoltro remoto ssh rimane aperto.

lato locale

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 è un piccolo trucco per scambiare stderr e stdout, in modo che stderr venga reindirizzato a cat, e tutto l'output normale di ssh viene mostrato su stderr.

lato 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

Ho provato al grepmessaggio "porta allocata" sul lato locale prima di inviarlo tramite ssh, ma sembra che ssh si blocchi in attesa che la pipe si apra su stdin. grep non apre la pipa per la scrittura fino a quando non riceve qualcosa, quindi sostanzialmente questo si blocca. cattuttavia non sembra avere lo stesso comportamento e apre immediatamente la pipe per scrivere consentendo a ssh di aprire la connessione.

questo è lo stesso problema sul lato remoto, e perché readriga per riga invece che solo grep da stdin - altrimenti `/ tmp / allocports 'non viene scritto fino a quando il tunnel SSH non viene chiuso, il che sconfigge l'intero scopo

~/getsshport.shÈ preferibile eseguire il piping dello stderr di ssh in un comando come , poiché senza specificare un comando, il testo del banner o qualsiasi altra cosa presente nella pipa viene eseguita sulla shell remota.


simpatico. ho aggiunto renice +10 $$; exec catprima il doneper risparmiare risorse.
Spongman,

0

È complicato, una gestione extra sul lato server sulla falsariga SSH_CONNECTIONo DISPLAYsarebbe grandiosa, ma non è facile aggiungere: parte del problema è che solo il sshclient conosce la destinazione locale, il pacchetto di richiesta (al server) contiene solo l'indirizzo e la porta remoti.

Le altre risposte qui hanno varie soluzioni semplici per catturare questo lato client e inviarlo al server. Ecco un approccio alternativo che non è molto più bello per essere onesti, ma almeno questa brutta festa è tenuta sul lato client ;-)

  • lato client, aggiungi / modifica in SendEnvmodo da poter inviare alcune variabili di ambiente in modo nativo su ssh (probabilmente non predefinito)
  • lato server, aggiungi / modifica AcceptEnvper accettare lo stesso (probabilmente non abilitato per impostazione predefinita)
  • monitorare l' sshoutput stderr del client con una libreria caricata dinamicamente e aggiornare l'ambiente client ssh durante l'impostazione della connessione
  • raccogliere il lato server delle variabili di ambiente nello script profilo / login

Funziona (fortunatamente, per ora comunque) perché i forward remoti vengono impostati e registrati prima che l'ambiente venga scambiato (confermare con ssh -vv ...). La libreria caricata dinamicamente deve catturare la write()funzione libc ( ssh_confirm_remote_forward()logit()do_log()write()). Il reindirizzamento o il wrapping di funzioni in un file binario ELF (senza ricompilazione) è un ordine di grandezza più complesso del fare lo stesso per una funzione in una libreria dinamica.

Sul client .ssh/config(o sulla riga di comando -o SendEnv ...)

Host somehost
  user whatever
  SendEnv SSH_RFWD_*

Sul server sshd_config(è richiesta la modifica root / amministrativa)

AcceptEnv LC_* SSH_RFWD_*

Questo approccio funziona per i client Linux e non richiede nulla di speciale sul server, dovrebbe funzionare per altri * nix con alcune piccole modifiche. Funziona da almeno OpenSSH 5.8p1 fino a 7.5p1.

Compila 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

Il codice:

#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);
}

(Esistono alcune trappole per orsi glibc relative alla versione dei simboli con questo approccio, ma write()non presenta questo problema.)

Se ti senti coraggioso, potresti prendere il setenv()codice relativo e modificarlo nella ssh.c ssh_confirm_remote_forward()funzione di richiamata.

Questo imposta le variabili di ambiente denominate SSH_RFWD_nnn, ispezionale nel tuo profilo, ad esbash

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

Avvertenze:

  • non c'è molto errore nel controllo del codice
  • la modifica dell'ambiente può causare problemi relativi ai thread, PAM utilizza i thread, non mi aspetto problemi ma non l'ho verificato
  • sshal momento non registra chiaramente l'inoltro completo del modulo * local: port: remote: port * (se necessario sarebbe necessario un ulteriore analisi dei debug1messaggi ssh -v), ma non è necessario per questo caso d'uso

Stranamente OpenSSH non sembra avere i mezzi per recuperare le informazioni sul port forwarding.

Puoi (parzialmente) farlo in modo interattivo con l'escape ~#, stranamente l'implementazione salta i canali in ascolto, elenca solo quelli aperti (cioè TCP STABILITO) e non stampa i campi utili in ogni caso. Vederechannels.c channel_open_message()

Puoi applicare una patch a quella funzione per stampare i dettagli per gli SSH_CHANNEL_PORT_LISTENERslot, ma ciò ti procura solo gli inoltri locali (i canali non sono la stessa cosa dei forward effettivi ). In alternativa, è possibile correggerlo per scaricare le due tabelle di inoltro dalla optionsstruttura globale :

#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));
}

Funziona bene, anche se non è una soluzione "programmatica", con l'avvertenza che il codice client non (ancora, è contrassegnato XXX nell'origine) aggiorna l'elenco quando aggiungi / rimuovi gli inoltri al volo ( ~C)


Se i server sono Linux hai un'altra opzione, questa è quella che uso generalmente, anche se per l'inoltro locale piuttosto che remoto. loè 127.0.0.1/8, su Linux puoi associare in modo trasparente a qualsiasi indirizzo in 127/8 , quindi puoi usare porte fisse se usi indirizzi 127.xyz univoci, ad esempio:

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                    *:*    

Questo è soggetto a porte privilegiate vincolanti <1024, OpenSSH non supporta le capacità di Linux e ha un controllo UID codificato sulla maggior parte delle piattaforme.

Gli ottetti saggiamente scelti (nel mio caso mnemonici ordinali ASCII) aiutano a districare il caos alla fine della giornata.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.