May 30, 2022

RDP over SSH. Как я писал клиент для удаленки под винду

Ког­да в кино показы­вают, как на Зем­ле раз­ража­ется эпи­демия страш­ного вируса, людям там нуж­ны в основном тушен­ка и пат­роны. Ког­да эта фан­тасти­ка воп­лотилась в реаль­нос­ти, ока­залось, что нуж­нее все­го — туалет­ная бумага и уда­лен­ные рабочие мес­та. И если с пер­вым все понят­но, вто­рой пункт вызыва­ет воп­росы с точ­ки зре­ния безопас­ности и выбора ПО. Ког­да готовое решение не под­ходит, нуж­но брать­ся за сбор­ку сво­его велоси­педа.

Сер­висы, с помощью которых мож­но орга­низо­вать под­клю­чение сот­рудни­ков на уда­лен­ке, обыч­но работа­ют через свои сер­веры. Поч­ти всег­да соеди­нение получа­ется мед­леннее пря­мых под­клю­чений, да и безопас­ность таких служб тоже под боль­шим воп­росом. Чаще все­го архи­тек­тура этих решений пос­тро­ена вок­руг реали­зации VNC (Virtual Network Computing). Сис­тема базиру­ется на про­токо­ле RFB (Remote FrameBuffer). Управле­ние устро­ено так: с одно­го компь­юте­ра на дру­гой переда­ются нажатия кла­виш и дви­жения мыши и содер­жимое экра­на рет­ран­сли­рует­ся через сеть. Сам VNC не шиф­рует переда­ваемые дан­ные. Если тре­бует­ся обес­печить повышен­ную безопас­ность, сес­сия может быть уста­нов­лена через SSL, SSH или VPN-тун­нели, что нес­коль­ко усложня­ет задачу.

На пуб­личных сер­висах все соеди­нения уста­нав­лива­ются через сер­вер, который выда­ет ID кли­ентов, под­клю­чая их либо через VPN нап­рямую друг к дру­гу, либо через собс­твен­ный канал. Конеч­но, мож­но под­нять свой VPN-сер­вер, нас­тро­ить мар­шру­тиза­цию VPN-сети и локаль­ной сети пред­при­ятия и под­клю­чать сот­рудни­ков по пря­мым IP-адре­сам. В этом слу­чае кли­енту нуж­но, помимо логина, пароля и адре­са под­клю­чения, передать еще кли­ент VPN и дан­ные для авто­риза­ции. Для некото­рых поль­зовате­лей все это слож­новато, а это, в свою оче­редь усложнит под­дер­жку.

Проб­росить пор­ты на локаль­ные ПК тоже не вари­ант. Это небезо­пас­но, да и нас­тра­ивать мар­шру­тиза­цию замуча­ешь­ся, ког­да кли­ентов ста­новит­ся боль­ше двух. В Linux есть замеча­тель­ный кли­ент Remmina, который поз­воля­ет проб­расывать сес­сии RDP/VNC через SSH-соеди­нение без допол­нитель­ных кли­ентов. В Windows мож­но орга­низо­вать SSH-тун­нели через кли­ент­ские при­ложе­ния, которые необ­ходимо нас­тра­ивать на уда­лен­ных поль­зователь­ских машинах. SSH-кли­ент «из короб­ки» есть толь­ко в Windows 10, но как быть с юзе­рами семер­ки и вось­мер­ки? Да и для Windows 10 при­дет­ся писать бат­ник, и не один. Все это не добав­ляет бал­лов стан­дар­тным решени­ям. Но всег­да мож­но при­думать нес­тандар­тное. Чем мы пря­мо сей­час и зай­мем­ся.

ПОСТАНОВКА ЗАДАЧИ

Итак, задача у нас будет сле­дующая. Под­клю­чать поль­зовате­ля по RDP (как выяс­нилось, это нам­ного при­выч­нее для боль­шинс­тва из них). Самое глав­ное пре­иму­щес­тво RDP по срав­нению с VNC — это ско­рость. RDP быс­трее потому, что этот про­токол перери­совы­вает на сто­роне кли­ента толь­ко изме­нен­ную часть экра­на, а зна­чит, дан­ных переда­ется мень­ше. Под­клю­чение дол­жно быть безопас­ным. Под­клю­чение дол­жно выпол­нять­ся с минималь­ными нас­трой­ками и не тре­бовать от поль­зовате­ля никаких допол­нитель­ных дей­ствий.

А ЧТО СКАЖЕТ GOOGLE?

В общем‑то, задача не новая, и реали­заций пос­тро­ения SSH-тун­неля сущес­тву­ет доволь­но мно­го. В Google мож­но с ходу най­ти ре­шения на базе PuTTY или ва­риан­ты для Windows 10. Мы сво­их поль­зовате­лей любим (и свои нер­вы тоже). А зна­чит, надо дать им такой инс­тру­мент, который не нуж­но нас­тра­ивать и который будет работать надеж­но.

Ины­ми сло­вами, решение дол­жно отве­чать сле­дующим тре­бова­ниям:

  • прос­тота для поль­зовате­ля;
  • лег­кость под­дер­жки;
  • прос­тая и понят­ная под­готов­ка и нас­трой­ка «сер­верных час­тей».

Ре­шение будет осно­вано на тех­нологии RDP over SSH. Тех­ничес­ки мы орга­низу­ем это так:

  • кли­ент SSH для орга­низа­ции тун­неля с проб­росом пор­та до целево­го ПК под­клю­чения;
  • ав­томати­чес­кий старт RDP-сес­сии без допол­нитель­ного вво­да парамет­ров под­клю­чения.

Сис­тема дол­жна быть лег­ко встра­иваемой, и дол­жен быть кли­ент для прод­винутых поль­зовате­лей (опци­ональ­но).

ГОТОВИМ СЕРВЕРНУЮ ЧАСТЬ

Прос­тые вещи вро­де нас­трой­ки RDP или SSH-сер­вера мы рас­смат­ривать не будем. Инс­трук­ций в интерне­те име­ется тьма, а у нас объ­ем огра­ничен, да и перег­ружать статью не хочет­ся. Так­же я не ста­ну под­робно рас­ска­зывать, как реали­зовать получе­ние дан­ных с Kerio Control: на стра­нице про­екта мож­но най­ти го­товый код.

Пер­вым делом нам нуж­но раз­решить RDP-под­клю­чения на кли­ент­ских машинах в локаль­ной сети. Если там реали­зова­но цен­тра­лизо­ван­ное управле­ние типа Active Directory, тебе повез­ло. Раз­реша­ем под­клю­чение к RDP в груп­повых полити­ках. Если нет, обхо­дим рабочие мес­та ногами и раз­реша­ем на целевых локаль­ных машинах RDP. Не забыва­ем и о фай­рво­лах.

Вто­рым шагом нам понадо­бит­ся дос­тупный из интерне­та SSH-сер­вер. Тех­ничес­ки подой­дет любое решение. Я исполь­зовал VPS с Debian 10 (один мой зна­комый под­нимал такой сер­вак даже на роуте­ре, что небезо­пас­но). Даль­ше сто­ит раз­делить решения на нес­коль­ко вер­сий, кон­крет­ная реали­зация зависит от того, как орга­низо­вано получе­ние дан­ных для авто­риза­ции поль­зовате­лей.

Пер­воначаль­но у нас исполь­зовал­ся Kerio с авто­риза­цией поль­зовате­лей через AD.

Схе­ма под­клю­чения

Кли­ент под­клю­чал­ся по SSH, проб­расывал порт на API Kerio Control Server, затем под­клю­чал­ся к нему, выпол­нял поиск по задан­ным парамет­рам (логин или фамилия сот­рудни­ка), искал IP локаль­ного ПК. Далее раз­рывал SSH-соеди­нение и уста­нав­ливал новое уже с проб­росом пор­та на най­ден­ный IP, на порт RDP (3389), пос­ле чего штат­ными средс­тва­ми Windows под­нималась сес­сия RDP с переда­чей парамет­ров под­клю­чения.

Та­кое решение работа­ло доволь­но быс­тро, но нам это­го ста­ло мало, и мы раз­делили его на две час­ти. Сер­верный скрипт стал работать на SSH-сер­вере и сам вре­мя от вре­мени ходить в Kerio за информа­цией. Кли­ент­ская часть под­клю­чалась к сер­веру по SFTP, иска­ла нуж­ные дан­ные, офор­млен­ные в JSON, и выпол­няла под­клю­чение. В ито­ге ско­рость работы уве­личи­лась.

Ре­комен­дую сра­зу нас­тро­ить сер­вер OpenSSH с дос­тупом по клю­чам и под­готовить RSA-клю­чи для авто­риза­ции. Для это­го нуж­но соз­дать отдель­ного поль­зовате­ля и огра­ничить его в пра­вах, затем отдать ему пуб­личную часть клю­ча. Ниже при­веду часть /etc/ssh/sshd_config с нас­трой­ками этих двух поль­зовате­лей:

Match User sftp  
      PubkeyAuthentication yes  
      # PasswordAuthentication yes  
      ChrootDirectory /srv/sftp  
      ForceCommand internal-sftp  
      AllowTcpForwarding  no
      
Match User user1  
      X11Forwarding no  
      ForceCommand /usr/bin/cmatrix # Подойдет и любая другая заглушка (можно заморочиться и отправлять пользователя в песочный bash)  
      PasswordAuthentication yes

Пер­вому поль­зовате­лю SFTP раз­решено под­клю­чать­ся толь­ко к это­му самому SFTP. Вто­рому — лишь для проб­роса пор­тов. Если у тебя исполь­зует­ся Kerio для получе­ния дан­ных о поль­зовате­лях, Active Directory или еще что‑то цен­тра­лизо­ван­ное, рекомен­дую завес­ти отдель­ную учет­ку и огра­ничить ее в пра­вах на вся­кий слу­чай.

РЕАЛИЗАЦИЯ

Итак, мы приб­лизились к реали­зации намечен­ной цели. Писать будем все это дело на Python 3.8. Во‑пер­вых, это муль­тип­латфор­менный язык, во‑вто­рых, собира­ется быс­тро и прос­то. В‑треть­их, он лег­кий в осво­ении, в‑чет­вертых, вклю­чает огромное количес­тво биб­лиотек.

В про­екте исполь­зуют­ся четыре биб­лиоте­ки: sshtunnel, PyQt5, threading и (в нашем слу­чае) API Kerio Control, но без него мож­но обой­тись. На самом деле я уже все написал и про­тес­тировал, поэто­му прос­то покажу, где какую стро­ку нуж­но поп­равить, что­бы прог­рамма запус­тилась. Ты можешь ска­чать написан­ную мною прог­рамму с GitHub и пос­мотреть, как она устро­ена, а в статье я дам необ­ходимые пояс­нения.

INFO
Раз­рабаты­ваемая в текущий момент вет­ка называ­ется develop. Вет­ка master реали­зова­на рань­ше, в этой вер­сии под­клю­чение про­исхо­дит на API Kerio и поиск IP по логину (фамилии поль­зовате­ля) выпол­няет­ся с помощью это­го API. Фун­кция поис­ка опи­сана в фай­ле kerio/keriofunction.py.

Биб­лиоте­ки луч­ше уста­нав­ливать в отдель­ное окру­жение Python. Я собирал всю опи­сыва­емую здесь конс­трук­цию в Debian 10, это сто­ит учи­тывать.

Итак, соз­даем окру­жение с Python 3.8 и заходим в него.

virtualenv --python=3.8 tmp/venv/ source tmp/venv/bin/activate 

Ус­танав­лива­ем все необ­ходимые биб­лиоте­ки:

pip install PyQt5 rhreading, sshtunnel 

Да­лее запус­каем «Qt Дизай­нер» и рису­ем фор­му вхо­да. В нашей биб­лиоте­ке он дос­тупен вот так:

tmp/venv/bin/pyqt5designer 

Соз­даем новое окно, под­готав­лива­ем окно авто­риза­ции и сох­раня­ем его. Резуль­тат показан на скрин­шоте ниже. Мы не меняли стан­дар­тные име­на объ­ектов форм, весь при­веден­ный ниже код сох­ранил штат­ные наз­вания объ­ектов.

Два вари­анта фор­мы при­ложе­ния для обыч­ных и прод­винутых поль­зовате­лей

«QT Дизай­нер» сох­раня­ет фай­лы в фор­мате .ui. Кон­верти­руем их в .py.

pyuic5 name.ui -o name.py 

Или так:

python -m PyQt5.uic.pyuic -x [FILENAME].ui -o [FILENAME].py 

В ито­ге получа­ем понят­ный для Рython файл дизай­на desing.py. Теперь о том, как устро­ена прог­рамма start.py. Сна­чала под­клю­чаем все необ­ходимые модули:

import sys, threading, time
from PyQt5 import QtWidgets, uic
from desing import Ui_MainWindow
from PyQt5.QtCore import QCoreApplication

Да­лее идет стан­дар­тный класс отоб­ражения окна PyQt:

class mywindow(QtWidgets.QMainWindow): 
    def __init__(self):  
        super(mywindow, self).__init__()  
        self.ui = Ui_MainWindow()  
        self.ui.setupUi(self)  
        self.ui.pushButton.clicked.connect(self.connectionstart)  
        self.ui.statusbar.showMessage("Программа готова к работе") # Отображаем в статус-баре состояние программы

За­тем сле­дует нес­коль­ко фун­кций, которые будут раз­делены по раз­ным потокам. Сде­лано это для того, что­бы не бло­киро­вать интерфейс прог­раммы во вре­мя выпол­нения кода этих фун­кций. Основная фун­кция connectionstart запус­кает­ся нажати­ем кноп­ки pushButton.

Эта фун­кция запус­кает в отдель­ном потоке фун­кцию монито­рин­га сос­тояния прог­раммы, для отоб­ражения его (сос­тояния) в ста­тус‑баре. Фун­кция про­веря­ет запол­ненность полей логина и пароля. Если одно из полей пус­тое, про­исхо­дит воз­врат и прог­рамма оста­нав­лива­ется. Под­клю­чает­ся к сер­веру СУБД или к сер­веру с фай­лом JSON и переда­ет вве­ден­ную фамилию в качес­тве парамет­ра для поис­ка IP-адре­са.

def connectionstart(self): 
    potok = threading.Thread (target=self.writelabelstatus, daemon=True) 
    potok.start () 
    from client import sshconnect 
    sshconnect.login = self.ui.lineEdit.text () 
    if sshconnect.login == '' or sshconnect.login is None:  
        sshconnect.setstatus = "emptylogin"  
        return 
    sshconnect.password = self.ui.lineEdit_2.text () 
    if sshconnect.password == '' or sshconnect.password is None:  
        sshconnect.setstatus = "emptypassword"  
        return 
    if sshconnect.login != "emptylogin" or sshconnect.local is not None and sshconnect.password != "emptypassword" or sshconnect.password is not None:  
        if sshconnect.setstatus == "ready":  
            self.starttun()

Пол­ный файл с кодом дос­тупен по ссыл­ке. Фун­кция starttun вызыва­ет в отдель­ном потоке фун­кцию sshconnect.connecttopc. Отдель­ный поток исполь­зует­ся для исклю­чения бло­киров­ки модуля форм.

Да­лее обра­тим­ся к фай­лу sshconnect.py. Оста­новим­ся толь­ко на стро­ках с нас­трой­ками, пол­ный текст кода дос­тупен на GitHub.

publicipadress = ('Public_IP', PORT) # Публичный IP-адрес и порт SSH-сервера

Это перемен­ная пуб­лично­го белого адре­са и пор­та SSH-сер­вера. Порт может быть откры­тым или рас­полагать­ся внут­ри локаль­ной сети и проб­расывать­ся через фай­рвол (DNAT). Обра­ти вни­мание на фун­кцию sshtungetip. Имен­но она получа­ет IP-адрес из фай­ла JSON.

Файл JSON дос­тупен на SFTP-сер­вере в дирек­тории SFTP. При­мер:

[  
  {  
    "User": {  
        "login": "Логин 1",  
        "FullName": "Логин1 Иван",  
        "ipaddress": {  
                 "ip": [  "192.168.0.2"  ]  
        }  
     }  
  },  
  {  "User": {  
         "login": "Логин2",  
         "FullName": "Логин2 Степан Борисович",  
         "ipaddress": {  
                  "ip": [  "192.168.0.3"  ]  
         }  
     }  
  },  
  ...
]

Пос­ле переда­чи парамет­ров под­клю­чения откры­ваем соеди­нение, под­клю­чаем биб­лиоте­ку JSON, тран­сфор­миру­ем файл в JSON-объ­ект:

sftp = getipsftp.open_sftp() 
    import json 
    fip = None 
    with sftp.file('ip-client.json', 'r') as f:  
        dataip = json.load(f) 
    getipsftp.close()

Даль­ше ищем в нем нуж­ную информа­цию по задан­ному клю­чу и воз­вра­щаем адрес под­клю­чения в качес­тве парамет­ра.

for x in dataip:  
        if login == x["User"]["login"] or login in x["User"]["FullName"]:  
            fip = x["User"]["ipaddress"]["ip"][0]  
            fullname = x["User"]["login"] 
        if fip == "Не найдено" or fip is None:  
            fip = "IP не найден" 
        time.sleep(1) 
        return(fip)

Те­перь рас­смот­рим фун­кцию sshtungetip. Ука­жем имя фай­ла зак­рытой час­ти клю­ча (ключ будет лежать в одном катало­ге с прог­раммой).

pk = RSAKey.from_private_key_file('srv.key')
WARNING
Ключ srv.key здесь при­веден ис­клю­читель­но в озна­коми­тель­ных целях. Катего­ричес­ки не рекомен­дую его исполь­зовать, ина­че любой поль­зователь смо­жет под­клю­чить­ся к сис­теме, в которой име­ется такой ключ.

На пос­леднем эта­пе в отдель­ном потоке вызыва­ется фун­кция rdpdataconnection, которая переда­ет в сис­тему управле­ния учет­ными дан­ными Windows вве­ден­ные в фор­му зна­чения. Помимо ука­зан­ных поль­зовате­лем фамилии и пароля (фамилия исполь­зует­ся в качес­тве логина), переда­ется адрес домена (если тре­бует­ся), адрес сер­вера (localhost), проб­рошен­ный порт (2222). Пос­ле это­го фун­кция ини­циирует новый про­цесс mstsc, которо­му переда­ются парамет­ры под­клю­чения и адре­с сер­вера.

subprocess.call("mstsc /v:localhost:2222")

Учет­ные дан­ные берут­ся из сис­темы управле­ния учет­ными дан­ными Windows. Через некото­рое вре­мя вызыва­ется фун­кция, которая уда­ляет дан­ные под­клю­чения.

ИТОГ

Мы получи­ли кли­ент RDP over SSH, который уста­нав­лива­ет тун­нель SSH с проб­росом пор­та до целево­го локаль­ного ПК и под­нима­ет «штат­ный» кли­ент RDP из ком­плек­та пос­тавки Windows. От поль­зовате­ля тре­бует­ся минималь­ное количес­тво дан­ных: толь­ко логин и пароль от его рабоче­го ком­па. Вер­сию для прод­винутых поль­зовате­лей опи­сывать деталь­но смыс­ла нет, вся раз­ница — нет фун­кций поис­ка IP и вве­ден­ные дан­ные переда­ются в качес­тве парамет­ров под­клю­чения.

Ес­ли ключ ском­про­мети­рован, мы можем заменить его новым и заново раз­дать поль­зовате­лям. Мож­но реали­зовать резер­вное под­клю­чение по логину и паролю SFTP для получе­ния фай­ла клю­ча или переда­вать его через сис­тему обновле­ний. Прос­тор для твор­чес­тва здесь огра­ничен толь­ко силой тво­его вооб­ражения.


Один хакер может причинить столько же вреда, сколько 10 000 солдат! Подпишись на наш Телеграм канал, чтобы узнать первым, как выжить в цифровом кошмаре!