Déterminer le port alloué dynamiquement pour OpenSSH RemoteForward

13

Question (TL; DR)

Lors de l'attribution dynamique de ports pour le transfert à distance ( -Roption aka ), comment un script sur la machine distante (provenant par exemple de .bashrc) peut-il déterminer quels ports ont été choisis par OpenSSH?


Contexte

J'utilise OpenSSH (aux deux extrémités) pour me connecter à notre serveur central, que je partage avec plusieurs autres utilisateurs. Pour ma session à distance (pour l'instant), je voudrais transmettre X, cups et pulseaudio.

Le plus trivial est le transfert de X, en utilisant l' -Xoption. L'adresse X allouée est stockée dans la variable d'environnement DISPLAYet à partir de là, je peux déterminer le port TCP correspondant, dans la plupart des cas de toute façon. Mais je n'en ai presque jamais besoin, car Xlib fait honneur DISPLAY.

J'ai besoin d'un mécanisme similaire pour les tasses et pulseaudio. Les bases pour les deux services existent, sous la forme des variables environnementales CUPS_SERVERet PULSE_SERVER, respectivement. Voici des exemples d'utilisation:

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

Le problème est réglé CUPS_SERVERet PULSE_SERVERcorrectement.

Nous utilisons beaucoup les transferts de ports et j'ai donc besoin d'allocations de ports dynamiques. Les allocations de ports statiques ne sont pas une option.

OpenSSH dispose d'un mécanisme d'allocation dynamique des ports sur le serveur distant, en spécifiant 0comme port de liaison pour le transfert à distance (l' -Roption). En utilisant la commande suivante, OpenSSH allouera dynamiquement des ports pour les coupes et le transfert d'impulsions.

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

Lorsque j'utilise cette commande, sshimprimera ce qui suit pour 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

Voilà les informations que je veux! Finalement, je veux générer:

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

Cependant, les messages "Port alloué ..." sont créés sur ma machine locale et envoyés à STDERR, auxquels je ne peux pas accéder sur la machine distante. Curieusement, OpenSSH ne semble pas avoir les moyens de récupérer des informations sur les transferts de port.

Comment puis-je récupérer ces informations pour les mettre dans un script shell pour définir correctement CUPS_SERVERet PULSE_SERVERsur l'hôte distant?


Impasses

La seule chose facile que j'ai pu trouver était d'augmenter la verbosité du sshdjusqu'à ce que ces informations puissent être lues dans les journaux. Ce n'est pas viable car ces informations révèlent beaucoup plus d'informations qu'il n'est raisonnable de rendre accessibles aux utilisateurs non root.

Je pensais à patcher OpenSSH pour prendre en charge une séquence d'échappement supplémentaire qui affiche une belle représentation de la structure interne permitted_opens, mais même si c'est ce que je veux, je ne peux toujours pas accéder aux séquences d'échappement client depuis le côté serveur.


Il doit y avoir une meilleure façon

L'approche suivante semble très instable et se limite à une session SSH par utilisateur. Cependant, j'ai besoin d'au moins deux sessions simultanées et d'autres utilisateurs encore plus. Mais j'ai essayé ...

Lorsque les étoiles sont alignées correctement, après avoir sacrifié un poulet ou deux, je peux abuser du fait que ce sshdn'est pas démarré en tant qu'utilisateur, mais abandonne les privilèges après une connexion réussie, pour ce faire:

  • obtenir une liste des numéros de port pour toutes les sockets d'écoute qui appartiennent à mon utilisateur

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

  • obtenir une liste des numéros de port pour toutes les sockets d'écoute qui appartiennent aux processus démarrés par mon utilisateur

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

  • Tous les ports qui se trouvent dans le premier ensemble, mais pas dans le deuxième ensemble ont une forte probabilité d'être mes ports de transfert, et en effet de soustraire les rendements des ensembles 41273, 55710et 6010; tasses, pouls et X, respectivement.

  • 6010est identifié comme le port X utilisant DISPLAY.

  • 41273est le port des tasses, car lpstat -h localhost:41273 -aretourne 0.
  • 55710est le port d'impulsion, car pactl -s localhost:55710 statretourne 0. (Il imprime même le nom d'hôte de mon client!)

(Pour effectuer la soustraction définie sort -u, enregistrer la sortie des lignes de commande ci-dessus et utiliser commpour effectuer la soustraction.)

Pulseaudio me permet d'identifier le client et, à toutes fins utiles, cela peut servir d'ancrage pour séparer les sessions SSH qui doivent être séparées. Cependant, je ne l' ai pas trouvé un moyen de lier 41273, 55710et 6010au même sshdprocessus. netstatne divulguera pas ces informations aux utilisateurs non root. Je reçois seulement un -dans la PID/Program namecolonne où je voudrais lire 2339/54(dans ce cas particulier). Si proche ...

Bananguin
la source
fwiw, il est plus précis de dire que netstatvous ne verrez pas le PID pour les processus que vous ne possédez pas ou qui sont de l'espace noyau. Par exemple
Bratchley
Le moyen le plus robuste serait de patcher le sshd ... Un patch rapide et sale serait juste quelques lignes à l'endroit où le serveur obtient son port local du système d'exploitation, en écrivant le numéro de port dans un fichier, le nom généré par l'utilisateur, l'hôte distant et Port. En supposant que le serveur connaît le port côté client, ce qui n'est pas certain, peut-être même pas probable (sinon la fonctionnalité existerait déjà).
hyde
@hyde: exactement. Le serveur distant ne connaît pas les ports redirigés. Il crée juste quelques sockets d'écoute et les données sont transmises via la connexion ssh. Il ne connaît pas les ports de destination locaux.
Bananguin

Réponses:

1

Prenez deux (voir l'historique pour une version qui fait scp du côté serveur et est un peu plus simple), cela devrait le faire. L'essentiel est le suivant:

  1. transmettre une variable d'environnement du client au serveur, en indiquant au serveur comment il peut détecter quand les informations de port sont disponibles, puis les obtenir et les utiliser.
  2. une fois que les informations de port sont disponibles, copiez-les du client vers le serveur, permettant au serveur de les obtenir (avec l'aide de la partie 1 ci-dessus), et utilisez-les

Tout d'abord, configurez le côté distant, vous devez activer l'envoi d'une variable env dans la configuration sshd :

sudo yourfavouriteeditor /etc/ssh/sshd_config

Trouvez la ligne avec AcceptEnvet ajoutez- MY_PORT_FILEy (ou ajoutez la ligne dans la Hostsection de droite s'il n'y en a pas encore). Pour moi, la ligne est devenue ceci:

AcceptEnv LANG LC_* MY_PORT_FILE

N'oubliez pas de redémarrer sshd pour que cela prenne effet.

De plus, pour que les scripts ci-dessous fonctionnent, faites mkdir ~/portfilesdu côté distant!


Ensuite, du côté local, un extrait de script qui

  1. créer un nom de fichier temporaire pour la redirection stderr
  2. laisser un travail en arrière-plan pour attendre que le fichier ait du contenu
  3. passer le nom du fichier au serveur en tant que variable env, tout en redirigeant ssh stderr vers le fichier
  4. le travail en arrière-plan procède à la copie du fichier temporaire stderr côté serveur à l'aide de scp séparé
  5. le travail en arrière-plan copie également un fichier indicateur sur le serveur pour indiquer que le fichier stderr est prêt

L'extrait 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

Ensuite, un extrait pour le côté distant, adapté pour .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

Remarque : Le code ci-dessus n'est bien sûr pas très testé et peut contenir toutes sortes de bugs, erreurs de copier-coller, etc. Quiconque l'utilise mieux le comprend également, utilisez-le à vos risques et périls! Je l'ai testé en utilisant juste une connexion localhost, et cela a fonctionné pour moi, dans mon test env. YMMV.

hyde
la source
Ce qui, bien sûr, nécessite que je puisse scppasser du côté distant au côté local, ce que je ne peux pas. J'avais une approche similaire, mais je terminais sshen arrière-plan après avoir établi la connexion, puis envoyais ce fichier de local à distant via scp, puis tirais le sshclient au premier plan et exécutais un script sur le côté distant. Je n'ai pas compris comment créer un script d'arrière-plan et de mise en avant des processus locaux et distants correctement. Envelopper et intégrer le sshclient local avec certains scripts distants comme celui-ci ne semble pas être une bonne approche.
Bananguin
Ah. Je pense que vous devriez fond du côté client scp seulement: (while [ ... ] ; do sleep 1 ; done ; scp ... )&. Attendez ensuite au premier plan dans le serveur .bashrc(en supposant que le client envoie la variable env droite) pour que le fichier apparaisse. Je mettrai à jour la réponse plus tard après quelques tests (probablement pas de temps avant demain).
hyde
@Bananguin Nouvelle version terminée. Semble fonctionner pour moi, devrait donc être adaptable à votre cas d'utilisation. A propos de "belle approche", oui, mais je ne pense pas qu'il y ait vraiment une belle approche possible ici. Les informations doivent être transmises d'une manière ou d'une autre, et ce sera toujours un hack, à moins que vous ne corrigiez à la fois le client et le serveur ssh pour le faire proprement via la connexion unique.
hyde
Et je pense de plus en plus à patcher openssh. Cela ne semble pas être un gros problème. L'information est déjà disponible. J'ai juste besoin de l'envoyer au serveur. Chaque fois que le serveur reçoit de telles informations, il les écrit~/.ssh-${PID}-forwards
Bananguin
1

Un extrait pour le côté local, adapté pour .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
la source
0

J'ai obtenu le même résultat en créant un canal sur le client local, puis en redirigeant stderr vers le canal qui est également redirigé vers l'entrée de ssh. Il ne nécessite pas plusieurs connexions ssh pour présumer un port connu libre qui pourrait échouer. De cette façon, la bannière de connexion et le texte "Port alloué ### ..." sont redirigés vers l'hôte distant.

J'ai un script simple sur l'hôte getsshport.shqui est exécuté sur l'hôte distant qui lit l'entrée redirigée et analyse le port. Tant que ce script ne se termine pas, le transfert à distance ssh reste ouvert.

côté 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 est une petite astuce pour échanger stderr et stdout, afin que stderr soit canalisé vers cat, et toute la sortie normale de ssh soit affichée sur stderr.

côté distant ~ / 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

J'ai essayé greple message "port alloué" du côté local avant de l'envoyer via ssh, mais il semble que ssh bloquera l'attente de l'ouverture du canal sur stdin. grep n'ouvre pas le canal d'écriture jusqu'à ce qu'il reçoive quelque chose, donc cela se bloque essentiellement. catcependant ne semble pas avoir ce même comportement, et ouvre immédiatement le canal d'écriture permettant à ssh d'ouvrir la connexion.

c'est le même problème du côté distant, et pourquoi readligne par ligne au lieu de simplement grep de stdin - sinon `/ tmp / allocationsports 'n'est pas écrit jusqu'à ce que le tunnel ssh soit fermé, ce qui va à l'encontre de l'objectif

Il ~/getsshport.shest préférable de canaliser le stderr de ssh dans une commande similaire , car sans spécifier de commande, le texte de la bannière ou tout ce qui se trouve dans le tuyau est exécuté sur le shell distant.

JesseMcL
la source
agréable. j'ai ajouté renice +10 $$; exec catavant le donepour économiser des ressources.
Spongman
0

Il s'agit d'une opération délicate, une gestion supplémentaire côté serveur le long de SSH_CONNECTIONou DISPLAYserait géniale, mais ce n'est pas facile à ajouter: une partie du problème est que seul le sshclient connaît la destination locale, le paquet de demande (au serveur) contient uniquement l'adresse et le port distants.

Les autres réponses ici ont diverses solutions peu judicieuses pour capturer ce côté client et l'envoyer au serveur. Voici une approche alternative qui n'est pas beaucoup plus jolie pour être honnête, mais au moins cette laideur est conservée côté client ;-)

  • côté client, ajoutez / modifiez SendEnvafin que nous puissions envoyer certaines variables d'environnement nativement via ssh (probablement pas par défaut)
  • côté serveur, ajouter / modifier AcceptEnvpour l'accepter (probablement pas activé par défaut)
  • surveiller la sshsortie du client stderr avec une bibliothèque chargée dynamiquement et mettre à jour l'environnement client ssh pendant la configuration de la connexion
  • ramasser les variables d'environnement côté serveur dans le script profil / login

Cela fonctionne (heureusement, pour l'instant de toute façon) parce que les renvois à distance sont configurés et enregistrés avant l'échange de l'environnement (confirmer avec ssh -vv ...). La bibliothèque chargée dynamiquement doit capturer la write()fonction libc ( ssh_confirm_remote_forward()logit()do_log()write()). La redirection ou l'encapsulation de fonctions dans un binaire ELF (sans recompilation) est des ordres de grandeur plus complexes que de faire de même pour une fonction dans une bibliothèque dynamique.

Sur le client .ssh/config(ou en ligne de commande -o SendEnv ...)

Host somehost
  user whatever
  SendEnv SSH_RFWD_*

Sur le serveur sshd_config(modification racine / administrative requise)

AcceptEnv LC_* SSH_RFWD_*

Cette approche fonctionne pour les clients Linux et ne nécessite rien de spécial sur le serveur, elle devrait fonctionner pour les autres * nix avec quelques ajustements mineurs. Fonctionne depuis au moins OpenSSH 5.8p1 jusqu'à 7.5p1.

Compiler avec gcc -Wall -shared -ldl -Wl,-soname,rfwd -o rfwd.so rfwd.c Invoke avec:

LD_PRELOAD=./rfwd.so ssh -R0:127.0.0.1:4713 -R0:localhost:631 somehost

Le code:

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

(Il existe des pièges à ours glibc liés à la gestion des versions des symboles avec cette approche, mais write()cela ne pose pas ce problème.)

Si vous vous sentez courageux, vous pouvez prendre le setenv()code associé et le patcher en ssh.c ssh_confirm_remote_forward()fonction de rappel.

Cela définit les variables d'environnement nommées SSH_RFWD_nnn, inspectez-les dans votre profil, par exemple dansbash

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

Mises en garde:

  • il n'y a pas beaucoup de vérification d'erreur dans le code
  • changer l'environnement peut provoquer des problèmes liés aux threads, PAM utilise des threads, je ne m'attends pas à des problèmes mais je n'ai pas testé cela
  • sshactuellement ne consigne pas clairement le transfert complet du formulaire * local: port: remote: port * (si nécessaire, une analyse plus approfondie des debug1messages avec ssh -vserait nécessaire), mais vous n'en avez pas besoin pour votre cas d'utilisation

Curieusement, OpenSSH ne semble pas avoir les moyens de récupérer des informations sur les transferts de port.

Vous pouvez (en partie) le faire de manière interactive avec l'échappement ~#, bizarrement l'implémentation saute les canaux qui écoutent, elle ne répertorie que les canaux ouverts (c'est-à-dire TCP ESTABLISHED), et elle n'imprime en aucun cas les champs utiles. Voirchannels.c channel_open_message()

Vous pouvez corriger cette fonction pour imprimer les détails des SSH_CHANNEL_PORT_LISTENERcréneaux horaires, mais cela ne vous donne que les renvois locaux (les canaux ne sont pas la même chose que les renvois réels ). Ou, vous pouvez le patcher pour vider les deux tables de transfert de la optionsstructure 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));
}

Cela fonctionne bien, bien que ce ne soit pas une solution "programmatique", avec la mise en garde que le code client ne met pas (encore, il est marqué XXX dans la source) la liste lorsque vous ajoutez / supprimez des renvois à la volée ( ~C)


Si le ou les serveurs sont Linux, vous avez une option de plus, c'est celle que j'utilise généralement, bien que pour le transfert local plutôt que distant. loest 127.0.0.1/8, sous Linux, vous pouvez vous lier de manière transparente à n'importe quelle adresse en 127/8 , vous pouvez donc utiliser des ports fixes si vous utilisez des adresses 127.xyz uniques, par exemple:

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

Ceci est soumis à la liaison de ports privilégiés <1024, OpenSSH ne prend pas en charge les capacités Linux et dispose d'une vérification UID codée en dur sur la plupart des plates-formes.

Des octets judicieusement choisis (mnémoniques ASCII dans mon cas) aident à démêler le désordre à la fin de la journée.

Mr Spuratic
la source