26 January 2017

Обратный туннель для доступа по RDP с использованием putty и icinga2

System administration


Не всегда есть возможность организовать единую локальную сеть в силу разных обстоятельств, но есть необходимость в удаленном доступе по протоколу RDP к хостам за натом с серым ip, например небольшой удаленный филиал с каналом связи через сотовую связь. Да, конечно можно поднять что-то вроде openVPN или воспользоваться TeamViewer, но опять же такие варианты не всегда приемлемы. Будем использовать icinga2, putty, SSH сервер и пару скриптов на powershell.

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

Установка SSH сервера:

sudo apt-get install openssh-server

Создание пользователя без доступа к шелу:

sudo useradd -d /dev/null -s /dev/null ssh_user
sudo passwd ssh_user

На всякий случай изменим порт по умолчанию с 22 на 2201. Это можно сделать изменив параметр Port в файле /etc/ssh/sshd_config.

Создаем туннель. На удаленном сервере (к которому в итоге мы хотим подключиться) необходимо каким-то способом выполнить команду:

plink.exe -batch -P 2201 -N -C -v -R 33899:localhost:3389 ssh_user@222.222.222.222 -pw password

А на компьютере администратора выполнить команды:

plink.exe -P 2201 -N -C -v -L 3379:localhost:33899 ssh_user@222.222.222.222 -pw password
mstsc.exe /v localhost:3379

Но это все теория, на практике же мы можем столкнуться с разными нюансами. При не стабильном канале может обрываться туннель и используемые порты остаются открытыми, удаленному серверу нужно как то сообщить что бы он поднял туннель, нам может потребоваться подключение к нескольким серверам одновременно.

Самое простое решение это создать задачу в планировщике которая каждые n минут будет опрашивать какой то ресурс в интернете на наличие определенного флага к действию и при обнаружении этого флага создавать туннель с отправкой email на наш адрес. Мы же сделаем все изящнее, воспользуемся системой мониторинга которая у нас установлена, например такой как icinga2. В этом случае удаленному серверу с установленным агентом можно будет просто послать команду на создание туннеля в нужный нам момент времени.

Если вы еще не сталкивались с этой системой мониторинга, рекомендую посмотреть:


Создадим сервис в icinga2 для создания туннеля:

apply Service "create-rdp-tunnel" {
    enable_active_checks = false
    max_check_attempts = 2
    assign where host.name == NodeName
    ignore where host.vars.os == "Linux"
    check_command = "powershell"
    vars.ps_command = "c:\\ProgramData\\icinga2\\Scripts\\icinga2\\create_rdp_tunnel.ps1"
}

Теперь нужно распространить скрипт создания туннеля и файл plink.exe на агенты. В предыдущей статье было описано как это можно сделать с помощью icinga2.

Скрипт создания туннеля
<#
icinga2scripts
Version 1.0
Description: Скрипт для Icinga 2 - Создание ssh туннеля с удаленным хостом
Pavel Satin (c) 2016
pslater.ru@gmail.com
#>
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8

$returnStateOK = 0
$returnStateWarning = 1
$returnStateCritical = 2
$returnStateUnknown = 3


$portnum = "338" + (Get-Random -minimum 10 -maximum 99).ToString()


$tunnelcmd = "c:\ProgramData\icinga2\Scripts\icinga2\plink.exe"
$tunnelarg = "-batch -P 2201 -N -C -v -R " + $portnum + ":localhost:3389 ssh_user@222.222.222.222 -pw password"


$regSSHkey = "HKCU:\Software\SimonTatham\PuTTY\SshHostKeys"
$regSSHname = "rsa2@2201:222.222.222.222"
$regSSHval = "0x10001,0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"

if (!(Test-Path $regSSHkey -PathType Any)) {
	New-Item -Path $regSSHkey -Force | Out-Null
	New-ItemProperty -Path $regSSHkey -Name $regSSHName -Value $regSSHval -PropertyType String -Force | Out-Null
} else {
	New-ItemProperty -Path $regSSHkey -Name $regSSHName -Value $regSSHval -PropertyType String -Force | Out-Null
}


$process = (start-process $tunnelcmd -argumentlist $tunnelarg -PassThru)

Start-Sleep -s 5

if ($process.HasExited) {
    Write-Host "Failed to start plink. The process is closed with the code: " $process.ExitCode
    [System.Environment]::Exit($returnStateCritical)
} else {

    #Отправка pushover сообщения
    $uri = "https://api.pushover.net/1/messages.json"
    $parameters = @{
        token = "API_TOKEN"
        user = "API_USER"
        message = "Создан туннель на порту: $portnum с узлом: $env:computername"
    }
    
    $pushoverreq = $parameters | Invoke-RestMethod -Uri $uri -Method Post

    Write-Host "OK - The tunnel is created. Port number: $portnum"
    Write-Host "To connect:"
    Write-Host "plink.exe -P 2201 -N -C -v -L 3379:localhost:$portnum ssh_user@222.222.222.222 -pw password"
    [System.Environment]::Exit($returnStateOK)
}



В скрипте учтен такой момент как запрос на сохранение ssh ключей удаленного сервера. Если на удаленном сервере ни разу не выполнялось подключение к вашему SSH серверу, putty будет выдавать предупреждение при подключении, а plink вообще откажется подключаться.


Что бы избежать этого, нужно подключиться к SSH серверу с любой другой машины, выдернуть из реестра кэшированные ключи (HKCU:\Software\SimonTatham\PuTTY\SshHostKeys) и изменить переменную $regSSHval в скрипте. Скрипт создает подключение к псевдослучайному номеру порта, что поможет избежать конфликтов при множестве соединений. Так же скрипт отправляет push сообщение с помощью сервиса pushover.net.

Отправим команду агенту на создание туннеля. Это можно сделать через веб-интерфейс или в консоли нашего сервера мониторинга:

/bin/echo "[`date +%s`] SCHEDULE_FORCED_SVC_CHECK;ИмяНоды;create-rdp-tunnel;`date +%s`" >> /var/run/icinga2/cmd/icinga2.cmd

Теперь администратору на своем компьютере останется только запустить скрипт с указанием имени удаленного сервера в качестве аргумента. Остальное сделает скрипт, выяснит на каком порту создан туннель, установит туннель с пробросом порта, запустит rdp клиент с нужными параметрами.

Скрипт подключения через туннель
<#
icinga2scripts
Version 1.0
Description: Скрипт для Icinga 2 - Запуск RemoteDesktop через туннель
Pavel Satin (c) 2016
pslater.ru@gmail.com
#>

$returnStateOK = 0
$returnStateWarning = 1
$returnStateCritical = 2
$returnStateUnknown = 3

#Windows Balloon
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$objNotifyIcon = New-Object System.Windows.Forms.NotifyIcon 

if ($args[0] -eq $null) {

    $objNotifyIcon.Icon = "C:\Scripts\images\icinga.ico"
    $objNotifyIcon.BalloonTipIcon = "Error" 
    $objNotifyIcon.BalloonTipText = "Параметр с именем хоста не передан! Работа скрипта завершена."
    $objNotifyIcon.BalloonTipTitle = "Подключение через туннель"
 
    $objNotifyIcon.Visible = $True 
    $objNotifyIcon.ShowBalloonTip(30000)

    Start-Sleep -s 10
    $objNotifyIcon.Visible = $false
    $script:objNotifyIcon.Dispose()
    exit
}

$rdpHost = $args[0]

$plinkPath = "C:\Scripts\bin\"

add-type -TypeDefinition  @"
        using System.Net;
        using System.Security.Cryptography.X509Certificates;
        public class TrustAllCertsPolicy : ICertificatePolicy {
            public bool CheckValidationResult(
                ServicePoint srvPoint, X509Certificate certificate,
                WebRequest request, int certificateProblem) {
                return true;
            }
        }
"@
[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy

$user = "icinga"
$pass= "password"
$secpasswd = ConvertTo-SecureString $pass -AsPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential($user, $secpasswd)
$apiurl = "https://222.222.222.222:5665/v1/objects/services/" + $rdpHost + "!create-rdp-tunnel?attrs=last_check_result"

$apireq = Invoke-WebRequest -Credential $credential -Uri $apiurl -Method Get -UseBasicParsing -ContentType "text/plain; charset=Windows-1251"

$outputresult = $apireq | ConvertFrom-Json | Select -expand Results | Select -expand attrs | Select -expand last_check_result 
$strOutput = $outputresult.output
$indxPlink = $strOutput.IndexOf("plink")

$portnum = "339" + (Get-Random -minimum 10 -maximum 99).ToString()

$strOutput2 = $strOutput.Substring($indxPlink, $strOutput.Length - $indxPlink)

$cmdArgs = "/C " + $strOutput2.Replace("3379", $portnum)
$mstscArgs = "/v localhost:$portnum"

#Запуск процессов
Start-Process cmd.exe $cmdArgs
Start-Process mstsc.exe $mstscArgs

$objNotifyIcon.Icon = "C:\Scripts\images\icinga.ico"
$objNotifyIcon.BalloonTipIcon = "Info" 
$objNotifyIcon.BalloonTipText = "Устанавливаем подключение к $rdpHost"
$objNotifyIcon.BalloonTipTitle = "Подключение через туннель"
 
$objNotifyIcon.Visible = $True 
$objNotifyIcon.ShowBalloonTip(30000)

Start-Sleep -s 30
$objNotifyIcon.Visible = $false
$script:objNotifyIcon.Dispose()




Ссылки


Putty
Используем powershell скрипты в icinga2
Tags:администрированиеpowershellicinga2
Hubs: System administration
+10
15.2k 122
Comments 15
Popular right now
Профессия iOS-разработчик
November 30, 202075,000 ₽SkillFactory
Основы HTML и CSS
November 30, 2020FreeНетология
Курс по аналитике данных
November 30, 202053,500 ₽SkillFactory
SMM-менеджер
November 30, 202059,998 ₽GeekBrains
Frontend-разработчик с нуля
November 30, 202077,940 ₽Нетология
Top of the last 24 hours