Как стать автором
Обновить

Его величество Пайп, или как заставить ssh tunnel открыть RDP на другом конце через альтернативный IP

Уровень сложностиСредний
Время на прочтение5 мин
Количество просмотров16K

Немного теории

Для начала, вспомним некоторые базовые вещи ОС Unix.

Любой процесс в Unix имеет три открытых файла по умолчанию, (но он может их потом закрыть/переоткрыть, например перенаправляя вывод в log file):

  • 0 (stdin)

  • 1 (stdout)

  • 2 (stderr)

примечание: stderr, отличается от stdout тем, что он не использует буфер для вывода.

Процессы unix могут выполняться асинхронно, как параллельно, так и последовательно, например:

$ cmd1 & cmd2;(cmd3 && cmd4) & (cmd5 || cmd6)

Разберем приведенную вязь команд по косточкам:

cmd1 & cmd2; — вначале параллельно запустятся cmd1 и cmd2

(cmd3 && cmd4) & (cmd5 || cmd6) — после завершения cmd1,параллельно запустятся условные цепочки cmd3,cmd4 и cmd5,cmd6. При этом:

cmd4 запуститься если cmd3 завершиться успешно, то есть ее код возврата будет 0,

cmd6, только в том случае, если cmd5 вернёт ненулевой код возврата.

Мне нравится термин конвейер для таких цепочек команд.

Pipe

Конвейер, помимо асинхронного выполнения, может синхронизировать выполнение процессов по вводу/выводу, используя специальный тип файла: pipe, или по русски канал.
Pipe — специальный тип файла, реализующий очередь FIFO (First Input, First Output) в Unix. Pipe может быть именованным или нет. Проще всего перенаправлять стандартные открытые файлы.

Неименованные каналы

Используя различные комбинацию конвейера и каналов, можно решить самые разные задачи администратора. Приведу примеры из собственной практики синхронизации через неименованные каналы:

## вывод размера файлов и числовая сортировка (про ключи -sS команды ls в курсе)
$ ls | awk '{printf("%25d %s\n",$5,$9)}'|sort -n
## найти пакеты postgresql
$ apt list | grep postgresql
## сформировать SQL команды для переноса файлов oracle
##  из одной файловой системы в другую.
$ (echo "set line 1000 pages 0 feedback off";echo "select 'alter database rename file '''||name||''' to '''||replace(name,'/u/','/u00/')||''';' as cmd from v\$datafile where name like '/u/%';")|sqlplus -s -l / as sysdba
## пример пробрасывания канала с одного host на другой из разных сессий
## префикс приглашения показывает hostname сервера
## на котором запускается команда
host2$ nc -l -p 44665|tar xvf -
host1$ tar cvf - /home/mydata | nc host2 44665

Прокомментирую последний пример: Тут используется команда netcat (или nc), которая открывает сетевой канал для проброса канала tar/untar.

При копировании больших объёмов, это может дать в разы большую скорость, чем стандартное scp. Например, у меня при копировании 1500G backup database c одной VM на другую, скорость выросла в пять раз без сильной нагрузки на CPU.

Понятно, что при копировании между ЦОД, нужно учитывать открытость такого трафика для перехвата.

Именованные каналы

Когда команды запускаются из под одной учетной записи, и команды работают с нужной информацией через stdin/stdout, особых проблем нет. Но вот реальный пример, когда oracle imp, умеет писать dump последовательно только в файл, которые указан параметром FILE=.
У вас не хватает пространства, чтобы выгрузить нужные данные, но можно сжать полученные данные используя Named Pipe и потоковую компрессию данных.

## Для начала создадим  канал как элемент файловой системы 
## используя команды mkfifo или mknod
$ mknod exp.dmp p
## запускаем в фоновом режиме gzip с чтением из именованного файла
$ gzip -c < exp.dmp > exp.dmp.gz &
## запускаем oracle exp с выводом в Name Pipe
$ exp file=exp.dmp userid=/ tables="(DROPME1,DROPME2)"

Как сделать imp из полученного архива, предлагаю подумать самим в качестве задания.

Разумеется, с именованными каналами, можно использовать netcat, для организации межсерверной коммуникации.

SSH туннели

ssh — универсальный инструмент администратора. В стандартной реализации ssh, существует возможность создать специальный шифрованный канал - ssh tunnel.

Пример использования ssh tunnel для проброса VNC:2 удаленного порта 5902 доступного только для 127.0.0.1 на vm2 на локальный порт 5902 машины vm1. После входа на vm2 командой:

vm1$ ssh -L localhost:5902:localhost:5902 user@vm2

вы можете запустить на vm1 локальный vncviwer :2 и попасть на vnc:2 на машине vm2.


vm1$ vncviwer  localhost:2

Схематично это можно показать так:

Здесь через локальный порт (-L) подключались к удаленному через ssh tunnel.

Для проброса удаленного порта (-R) на пользовательский комп, поступают аналогично. Например надо подключится к домашней postgresql базе с рабочего компа подключившись из дома к рабочей машине:

my1$ ssh -R localhost:5432:localhost:5432 user@vm1

vm1$ psql -h localhost -p 5432

Здесь мы свой домашний 5432 порт пробрасываем на удаленную машину.

Более сложный пример (начало):

По проекту, потребовалось RDP подключение с рабочего notebook через VPN и цепочку hosts на windows машину. При этом, напрямую подключиться к каждой из цепочки машин невозможно, только последовательно.

В целом, подключение выглядит так :

Идем стандартным путем, используем туннель + ssh jump:

## команда, которой я пользовался для подключения: 
my$ ssh -i ~/.ssh/vm1.key -J gateuser@gatehost -L 127.0.0.1:3390:win1:3389 user@vm1 

Где:

-i vm1.key — файл private key пользователя user@vm1

-J (Jump), подключится к user@vm1 через  gateuser@gatehost, само подключение к gateuser@gatehost, так как постоянно работаю через него, прописано у меня в ~/.ssh/config

-L 127.0.0.1:3390:win1:3389 — создание сквозного ssh tunnel между my (127.0.0.1:3390) и (win1:3389) через машину user@vm1. То есть, пробрасывается открытый на vm1 конец канала vm1:3389 — win1:3389

## осталось подключится по RDP:
my$ xfreerdp /scale:140 /u:winuser /smart-sizing:1900x1000 /size:1900x1000 /bpp:32 /network:lan /p:**** /v:127.0.0.1:3390

Пример (продолжение):

Все работало отлично, но в один из черных дней погибает vm1...

Сервера расположены не у нас, процедура перенастройки требует согласования и времени, вот только доступ на win1 понадобился вот прямо сейчас!

Рядом с vm1 расположен еще один сервер vm2, но доступа с его IP на win1 нет.

Было принято решение (с получением «добра» от начальника), поднять второй IP от первого vm1 на втором vm2 и ходить через него.

Сеть наша, лишних в ней нет, поэтому: сказано — сделано:

# добавляем второй IP
vm2$ sudo ip a add 192.168.45.56/24 dev eth0
## проверяем
vm2$ ip a
...
2: eth0:  mtu 1500 qdisc mq state UP group default qlen 1000
...
  inet 192.168.45.57/24 brd 192.168.56.255 scope global eth0
...
  inet 192.168.45.56/24 scope global secondary eth0 
...

Вот только засада, ssh не умеет указывать какой IP использовать при подключении через ssh tunnel.

То есть при попытке подключения предыдущей командой окончилось неудачей, так как соединение к win1 идет через default IP ( для этого сервера 192.168.45.57),  а «волшебного ключа», для указание ssh tunnel через какой удаленный IP ему подключаться дальше — не существует в текущей версии ssh. Ключа --remote-bind-address - не существует:

## таких не бывает!
$ ssh -J ... -L localhost:3390:win1:3389 --remote-bind-address 192.168.45.56 user@vm2

Начались поиски решения, найденные варианты не подошли по разным причинам:

  • route add --host ...

  • iptables -t nat -I POSTROUTING ...

Но все же решение Было найдено.

Помог netcat + named pipe. Итак вот оно, решение!:

## создаем на vm2 uniх name pipe
vm2$ mkfifo /tmp/rdp.pipe
## запускаем двухстороннее перенаправление
## localhost:3389<->win1:3389 через созданный rdp.pipe
vm2$ nc -v -n -s 192.168.23.1 192.168.45.56 3389 < /tmp/rdp.pipe |nc -lkv -n -s 127.0.0.1 -p 3389 1> /tmp/rdp.pipe

## проверяем 
vm2$ sudo netstat -anp | grep 3389
tcp    0  0 127.0.0.1:3389      0.0.0.0:*           LISTEN  15460/nc         
tcp    0  0 192.168.45.56:42175 192.168.45.111:3389  ESTABLISHED 15459/nc
## Чуток модифицируем предыдущую команду для ssh tunnel
## так как теперь RDP у нас на localhost:3389 сервера vm2
## и только потом через nc + rdp.pipe идет подключение на win1:3389 
my$ ssh -i ~/.ssh/linuxhostkey -J gateuser@gatehost -L localhost:3390:localhost:3389 user@vm2
## на my notebook подключаемся через RDP
my$ xfreerdp /scale:140 /u:winuser  /smart-sizing:1900x1000 /size:1900x1000 /bpp:32 /network:lan /p:***** /v:127.0.0.1:3390

Как заключение

Я в принципе знал сам принцип, но вот использовать такой трюк для проброса RDP между локальными IP не додумался, да и на удивление, такое решение нашлось далеко не сразу.

Надеюсь, данная статья поможет многим администраторам разобраться с каналами, и их использованием в своей работе.

Теги:
Хабы:
Всего голосов 28: ↑28 и ↓0+28
Комментарии11

Публикации

Истории

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань