Определить динамически выделенный порт для OpenSSH RemoteForward

13

Вопрос (TL; DR)

При динамическом назначении портов для удаленной переадресации (иначе -R), как скрипт на удаленном компьютере (например, из источника .bashrc) может определить, какие порты были выбраны OpenSSH?


Фон

Я использую OpenSSH (на обоих концах) для подключения к нашему центральному серверу, которым я делюсь с несколькими другими пользователями. Для моего удаленного сеанса (на данный момент) я хотел бы переслать X, чашки и pulseaudio.

Наиболее тривиальным является пересылка X с использованием -Xопции. Выделенный X-адрес хранится в переменной среды, DISPLAYи из этого я могу определить соответствующий порт TCP, в большинстве случаев в любом случае. Но мне вряд ли когда-нибудь понадобится, потому что Xlib чтит DISPLAY.

Мне нужен аналогичный механизм для чашек и pulaudio. Основы для обоих сервисов существуют в форме переменных среды CUPS_SERVERи PULSE_SERVER, соответственно. Вот примеры использования:

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

Проблема в настройке CUPS_SERVERи PULSE_SERVERправильности.

Мы часто используем переадресацию портов, поэтому мне нужно динамическое распределение портов. Статическое распределение портов не вариант.

OpenSSH имеет механизм динамического распределения портов на удаленном сервере, определяя 0как bind-порт для удаленной пересылки ( -Rопция). Используя следующую команду, OpenSSH будет динамически распределять порты для чашек и пересылки импульсов.

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

Когда я использую эту команду, sshвыведите следующее 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

Есть информация, которую я хочу! В конечном итоге я хочу создать:

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

Однако сообщения «Выделенный порт ...» создаются на моем локальном компьютере и отправляются на него STDERR, к которому я не могу получить доступ на удаленном компьютере. Как ни странно, OpenSSH, похоже, не имеет средств для получения информации о переадресации портов.

Как мне получить эту информацию, чтобы поместить ее в сценарий оболочки для соответствующей установки CUPS_SERVERи PULSE_SERVERна удаленном хосте?


Мертвые концы

Единственной легкой вещью, которую я мог найти, было увеличение детализации, sshdпока эта информация не будет прочитана из журналов. Это нежизнеспособно, поскольку эта информация раскрывает гораздо больше информации, чем разумно сделать доступной для пользователей без полномочий root.

Я думал о исправлении OpenSSH для поддержки дополнительной escape-последовательности, которая печатает хорошее представление внутренней структуры permitted_opens, но даже если это то, что я хочу, я все еще не могу написать сценарий доступа к escape-последовательностям клиента со стороны сервера.


Должен быть лучший способ

Следующий подход кажется очень нестабильным и ограничивается одним таким сеансом SSH на пользователя. Однако мне нужно как минимум два одновременных таких сеанса, а другим пользователям - еще больше. Но я пытался ...

Когда звезды выровнены должным образом, пожертвовав курицей или двумя, я могу злоупотреблять тем, что sshdне запускается как мой пользователь, но удаляет привилегии после успешного входа в систему, чтобы сделать это:

  • получить список номеров портов для всех прослушивающих сокетов, которые принадлежат моему пользователю

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

  • получить список номеров портов для всех прослушивающих сокетов, которые принадлежат процессам, запущенным моим пользователем

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

  • Все порты, которые находятся в первом наборе, но не во втором наборе, имеют высокую вероятность быть моими портами переадресации и действительно вычитать выходы наборов 41273, 55710и 6010; чашки, пульс и Х соответственно.

  • 6010определяется как использование X-порта DISPLAY.

  • 41273порт чашки, потому что lpstat -h localhost:41273 -aвозвращается 0.
  • 55710это импульсный порт, потому что pactl -s localhost:55710 statвозвращается 0. (Он даже печатает имя хоста моего клиента!)

(Чтобы сделать заданное вычитание I sort -uи сохранить выходные данные из приведенных выше командных строк и использовать commдля выполнения вычитания.)

Pulseaudio позволяет мне идентифицировать клиента, и, для всех целей и задач, это может служить якорем для отдельных сеансов SSH, которые необходимо разделить. Тем не менее, я не нашел способ связать 41273, 55710и 6010к тому же sshdпроцессу. netstatне будет раскрывать эту информацию пользователям без прав root. Я получаю только -в PID/Program nameстолбце, где я хотел бы прочитать 2339/54(в данном конкретном случае). Так близко ...

Bananguin
источник
Кстати, точнее будет сказать, что netstatPID не будет отображаться для процессов, которыми вы не владеете или которые являются пространством ядра. Например
Братчли
Самый надежный способ - это исправить sshd ... Быстрое и грязное исправление будет состоять из нескольких строк в том месте, где сервер получает свой локальный порт из ОС, записывая номер порта в файл, имя, генерируемое пользователем, удаленным хостом и порт. Предполагая, что сервер знает порт на стороне клиента, который не уверен, возможно даже не вероятен (иначе функция уже существовала бы).
Хайд
@hyde: точно. Удаленный сервер не знает о перенаправленных портах. Он просто создает несколько сокетов прослушивания и данные пересылаются через соединение ssh. Он не знает о локальных портах назначения.
Bananguin

Ответы:

1

Возьмите два (смотрите историю для версии, которая делает scp со стороны сервера и немного проще), это должно сделать это. Суть этого заключается в следующем:

  1. передать переменную среды от клиента к серверу, сообщив серверу, как он может определить, доступна ли информация о порте, а затем получить и использовать ее.
  2. как только информация о порте станет доступной, скопируйте ее с клиента на сервер, чтобы сервер мог ее получить (с помощью части 1 выше), и используйте ее

Во-первых, для настройки на удаленной стороне вам необходимо разрешить отправку переменной env в конфигурации sshd :

sudo yourfavouriteeditor /etc/ssh/sshd_config

Найдите строку с AcceptEnvи добавьте MY_PORT_FILEк ней (или добавьте строку в правой Hostчасти, если ее еще нет). Для меня линия стала такой:

AcceptEnv LANG LC_* MY_PORT_FILE

Также не забудьте перезапустить sshd, чтобы это вступило в силу.

Кроме того, чтобы нижеприведенные скрипты работали, делайте это mkdir ~/portfilesна удаленной стороне!


Затем на локальной стороне, фрагмент сценария, который будет

  1. создать имя временного файла для перенаправления stderr
  2. оставить фоновое задание, чтобы дождаться, пока файл будет иметь содержимое
  3. передать имя файла на сервер как переменную env, перенаправляя ssh stderr в файл
  4. Фоновое задание переходит к копированию временного файла stderr на сторону сервера, используя отдельный scp
  5. фоновое задание также копирует файл флага на сервер, чтобы указать, что файл stderr готов

Фрагмент скрипта:

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

Затем фрагмент для удаленной стороны, подходящий для .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

Примечание . Приведенный выше код, конечно, не очень тщательно протестирован и может содержать все виды ошибок, ошибок копирования и вставки и т. Д. Любой, кто его использует, также лучше это понимает, используйте на свой страх и риск! Я проверил это, используя только локальное соединение, и это сработало для меня, в моей тестовой среде. YMMV.

Хайд
источник
Что, конечно, требует, я могу scpс удаленной стороны на местную сторону, что я не могу. У меня был похожий подход, но я бы обернул его sshв фоновом режиме после установления соединения, затем отправил этот файл с локального на удаленный сервер, scpа затем вытащил sshклиента на передний план и запустил скрипт на удаленной стороне. Я не понял, как правильно создавать сценарии фоновой обработки и упорядочения локальных и удаленных процессов. Обертывание и интеграция локального sshклиента с некоторыми удаленными сценариями вроде этого не выглядит хорошим подходом.
Bananguin
Ах. Я думаю , вы должны фон на стороне клиента УПП только: (while [ ... ] ; do sleep 1 ; done ; scp ... )&. Затем дождитесь появления на сервере переднего плана .bashrc(при условии, что клиент отправит правильную переменную env), чтобы файл появился. Я обновлю ответ позже после некоторого тестирования (вероятно, нет времени до завтра).
Хайд
@Bananguin Новая версия готова. Кажется, работает для меня, поэтому должны быть адаптированы к вашему варианту использования. О "хорошем подходе", да, но я не думаю, что здесь действительно возможен хороший подход. Информацию нужно каким-то образом передавать, и это всегда будет взломом, если только вы не исправите ssh-клиент и сервер, чтобы сделать это чисто через одно соединение.
Хайд
И я все больше и больше думаю о патчинге openssh. Кажется, не имеет большого значения. Информация уже доступна. Мне просто нужно отправить его на сервер. Всякий раз, когда сервер получает такую ​​информацию, он записывает ее~/.ssh-${PID}-forwards
Bananguin
1

Фрагмент для локальной стороны, подходящий для .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
источник
0

Я добился того же, создав канал на локальном клиенте, затем перенаправив stderr на канал, который также перенаправляется на вход ssh. Не требуется нескольких соединений ssh, чтобы предположить свободный известный порт, который может дать сбой. Таким образом, баннер входа в систему и текст «Выделенный порт ### ...» перенаправляются на удаленный хост.

У меня есть простой скрипт на хосте, getsshport.shкоторый запускается на удаленном хосте, который читает перенаправленный ввод и анализирует порт. Пока этот сценарий не заканчивается, ssh remote forward остается открытым.

местная сторона

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 Это небольшая хитрость, чтобы поменять местами stderr и stdout, чтобы stderr был передан по каналу cat, а все обычные выходные данные ssh показаны на stderr.

удаленная сторона ~ / 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

Я попытался grepсначала отправить сообщение «выделенный порт» на локальной стороне перед отправкой через ssh, но кажется, что ssh заблокирует ожидание открытия канала на stdin. grep не открывает канал для записи, пока не получит что-то, так что это в основном тупики. catоднако, похоже, не имеет такого же поведения, и открывает канал для записи немедленно, позволяя ssh открыть соединение.

это та же проблема на удаленной стороне, и почему readстрока за строкой, а не просто grep из stdin - в противном случае `/ tmp / allocports 'не будет записан до тех пор, пока не будет закрыт туннель ssh, что наносит ущерб всей цели

~/getsshport.shПредпочтительной является передача stderr из ssh в подобную команду , так как без указания команды, текста баннера или чего-либо еще в канале выполняется на удаленной оболочке.

JesseMcL
источник
красивый. Я добавил renice +10 $$; exec catперед тем, doneчтобы сэкономить ресурсы.
Spongman
0

Это сложная задача , дополнительная обработка на стороне сервера по линии SSH_CONNECTIONили DISPLAYбыла бы хорошей, но добавить ее нелегко: отчасти проблема в том, что только sshклиент знает локальное назначение, пакет запроса (к серверу) содержит только удаленный адрес и порт.

У других ответов здесь есть различные не очень подходящие решения для захвата этой клиентской стороны и отправки ее на сервер. Вот альтернативный подход, который не очень красив, если честно, но по крайней мере эта уродливая вечеринка держится на стороне клиента ;-)

  • на стороне клиента, добавьте / измените, SendEnvчтобы мы могли отправлять некоторые переменные среды через ssh (вероятно, не по умолчанию)
  • на стороне сервера, добавьте / измените, AcceptEnvчтобы принять то же самое (вероятно, не включено по умолчанию)
  • контролировать sshвывод stderr клиента с помощью динамически загружаемой библиотеки и обновлять среду клиента ssh во время установки соединения
  • подобрать переменные окружения на стороне сервера в профиле / скрипте входа

Это работает (к счастью, пока что в любом случае), потому что удаленные пересылки устанавливаются и записываются до обмена окружением (подтвердите с помощью ssh -vv ...). Динамически загружаемая библиотека должна захватывать write()функцию libc ( ssh_confirm_remote_forward()logit()do_log()write()). Перенаправление или перенос функций в двоичный файл ELF (без перекомпиляции) на несколько порядков сложнее, чем при выполнении функции в динамической библиотеке.

На клиенте .ssh/config(или командной строке -o SendEnv ...)

Host somehost
  user whatever
  SendEnv SSH_RFWD_*

На сервере sshd_config(требуется root / административное изменение)

AcceptEnv LC_* SSH_RFWD_*

Этот подход работает для клиентов Linux и не требует ничего особенного на сервере, он должен работать для других * nix с некоторыми незначительными изменениями. Работает как минимум с OpenSSH 5.8p1 до 7.5p1.

Компилировать с gcc -Wall -shared -ldl -Wl,-soname,rfwd -o rfwd.so rfwd.c Invoke с:

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

Код:

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

(Существуют некоторые ловушки для медведей-глибков, связанные с версионированием символов с помощью этого подхода, но у write()него нет этой проблемы.)

Если вы чувствуете себя смелым, вы можете взять setenv()соответствующий код и добавить его в ssh.c ssh_confirm_remote_forward()функцию обратного вызова.

Это устанавливает переменные среды с именами SSH_RFWD_nnn, проверьте их в своем профиле, например, вbash

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

Предостережения:

  • в коде не так много проверок на ошибки
  • изменение среды может вызвать проблемы, связанные с потоками, PAM использует потоки, я не ожидаю проблем, но я не проверял это
  • sshв настоящее время явно не регистрируется полная переадресация формы * local: port: remote: port * (при необходимости потребуется дальнейший анализ debug1сообщений с помощью ssh -v), но это не требуется для вашего варианта использования

Как ни странно, OpenSSH, похоже, не имеет средств для получения информации о переадресации портов.

Вы можете (частично) сделать это в интерактивном режиме с помощью escape ~#, как ни странно, реализация пропускает каналы, которые прослушивают, она только перечисляет открытые (т. Е. TCP ESTABLISHED) каналы и ни в коем случае не печатает полезные поля. Видетьchannels.c channel_open_message()

Вы можете исправить эту функцию, чтобы напечатать детали для SSH_CHANNEL_PORT_LISTENERслотов, но это только даст вам локальные переадресации ( каналы не то же самое, что фактические пересылки ). Или вы можете исправить это, чтобы вывести две таблицы пересылки из глобальной optionsструктуры:

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

Это прекрасно работает, хотя это и не «программное» решение, с оговоркой, что клиентский код не (пока он помечен как XXX в источнике) обновляет список при добавлении / удалении переадресаций на лету ( ~C)


Если сервер (ы) - Linux, у вас есть еще один вариант, который я обычно использую, хотя для локальной пересылки, а не удаленной. lo127.0.0.1/8, в Linux вы можете прозрачно привязать любой адрес в 127/8 , поэтому вы можете использовать фиксированные порты, если используете уникальные адреса 127.xyz, например:

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

Это зависит от привязки привилегированных портов <1024, OpenSSH не поддерживает возможности Linux и имеет жестко проверенную проверку UID на большинстве платформ.

Мудро выбранные октеты (в моем случае порядковая мнемоника ASCII) помогают распутать беспорядок в конце дня.

mr.spuratic
источник