Работая в компании IT-аутсорса в качестве руководителя 3 линии поддержки, задумался, как автоматизировать подключение сотрудников по RDP, через VPN к серверам десятков клиентов.

Таблички с адресами, паролями и прочими настройками серверов, конечно, хорошо, но поиск клиента и вбивание адресов с аккаунтами занимает довольно существенное время.
Держать все подключения к VPN в Windows не самая лучшая идея, да и при переустановке оного, создавать VPNы тоже не доставляет удовольствие.
Плюс к тому, в большинстве случаев, требуется установить VPN подключение к клиенту без использования шлюза. дабы не гонять весь интернет-трафик через клиента.
Задача, к тому же, осложняется тем, что у некоторых клиентов pptp, у кого-то l2tp, у некоторых несколько подсетей, туннели и т.п.

В результате, для начала был написан скрипты на Powershell для каждого клиента, но позже, узнав, что в Powershell можно использовать Winforms они переродились в некое приложение, написанное с помощью того же Powershell.

До написания этого скрипта-приложения программированием не занимался вообще, разве что лет 20 назад что-то пописывал на VBS в MS Excel и MS Access, поэтому не гарантирую красивость кода и принимаю критику от опытных программистов, как можно было бы сделать красивее.

В Powershell, начиная с Windows 8 и, конечно в Windows 10, появилась прекрасная возможность создавать VPN подключения командой Add-VpnConnection и указывать какие маршруты использовать с этими соединениями командой Add-VpnConnectionRoute, для использования VPN без шлюза.

На основании этих команд и создано данное приложение. Но, обо всем по порядку.

? Хранить чувствительные данные (имена, пароли, коды доступа) в публичных облаках небезопасно, выбирайте защищенные места хранения таких данных.
В данном коде таблица в Google Disk представлена для примера, не содержит никаких реальных данный и предназначена для быстрой проверки работоспособности скрипта без раскрытия каких-либо данных автора, которые он не хотел бы разглашать.

Для начала, создаем в удобном вам и безопасном хранилище (в примере в Google Disk) таблицу с именованными столбцами:
Number; Name; VPNname; ServerAddress; RemoteNetwork; VPNLogin; VPNPass; VPNType; l2tpPsk; RDPcomp; RDPuser; RDPpass; DefaultGateway; PortWinbox; WinboxLogin; WinboxPwd; Link; Inform

Основное требование к таблице - свободное конвертирование в формат CSV, либо изначальное хранение формате CSV (знак разделения указывается в переменной comma)

  • VPNname – произвольное имя для VPN соединения

  • ServerAddress – адрес VPN сервера

  • RemoteNetwork – адреса подсети или подсетей клиента, разделенные «;»

  • VPNLogin; VPNPass – учетная запись VPN

  • VPNType -тип VPN  (пока используется pptp или l2tp)

  • l2tpPsk – PSK для l2tp, в случае pptp оставляем пустым

  • RDPcomp – адрес сервера RPD

  • RDPuser; RDPpass – учетная запись RPD

  • DefaultGateway принимает значение TRUE или FALSE и указывает на то, использовать ли «Шлюз по умолчанию» для этого соединения. В 90% случаев = FALSE

  • PortWinbox; WinboxLogin; WinboxPwd – порт, логин и пароль для Winbox, поскольку у нас большинство клиентов использует Mikrotik)

  • Link – ссылка на расширенную информацию о компании, например, на диске Google, или в любом другом месте, будет выводиться в информационном поле для быстрого доступа к нужной информации

Inform – примечание

Пример таблицы доступен по ссылке

Number

Name

VPNname

ServerAddress

RemoteNetwork

VPNLogin

VPNPass

VPNType

l2tpPsk

RDPcomp

RDPuser

RDPpass

DefaultGateway

PortWinbox

WinboxLogin

WinboxPwd

Link

Inform

1

Тест1

Test1

a.b.c.d

192.168.10.0/24: 10.10.0.0/24

vpnuser

passWord

pptp

none

192.168.10.1

user

passWord

TRUE

8291

Admin

Admin

http://yandex.ru

тест

2

Тест2

Test2

e.f.j.k

192.168.2.0/24

vpnuser

passWord

l2tp

KdoSDtdP

192.168.2.1

user

passWord

FALSE

8291

Admin

Admin

Скриншот работающего приложения с затертыми данными:

Основное окно программы
Основное окно программы

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

function Get-Clients #Функция принимает строку адреса файла в Google Drive и возвращает в виде массива данных о клиентах
	{
		param
		(
			[string]$google_url = ""
		)

		[string]$xlsFile = $google_url
		$csvFile = "$env:temp\clients.csv"
		$Comma = ','
		Invoke-WebRequest $xlsFile -OutFile $csvFile
		
		$clients = Import-Csv -Delimiter $Comma -Path "$env:temp\clients.csv"
		Remove-Item -Path $csvFile
		return $clients
	}

function Main {
<#
    Функция, срабатываемая при запуске скрипта
#>
	Param ([String]$Commandline)
	
	#Иннициализируем переменные и присваиваем начальные значения. Здесь же, указываем путь к таблице с клиентами
	
	$Global:Clients = $null
	$Global:Current
	$Global:CurrentRDPcomp
	$Global:google_file = "https://docs.google.com/spreadsheets/d/1O-W1YCM4x3o5W1w6XahCJZpkTWs8cREXVF69gs1dD0U/export?format=csv" # Таблица скачивается сразу в виде csv-файла
	
	$Global:Clients = Get-Clients ($Global:google_file) # Присваиваем значения из таблицы массиву 
	
	#Скачиваем Winbox64 во временную папку
	
	$download_url = "https://download.mikrotik.com/winbox/3.27/winbox64.exe"
	$Global:local_path = "$env:temp\winbox64.exe"
	If ((Test-Path $Global:local_path) -ne $true)
	{
		$WebClient = New-Object System.Net.WebClient
		$WebClient.DownloadFile($download_url, $Global:local_path)
	}
	
  #Разрываем все текущие VPN соединения (на всякий случай)
	
	foreach ($item in get-vpnconnection | where { $_.ConnectionStatus -eq "Connected" })
	{
		Rasdial $item.Name /disconnect
	}
	
  #Удаляем все, ранее созданные программой временные соединения, если вдруг не удалились при некорректном закрытии приложения
	
	get-vpnconnection | where { $_.Name -match "tmp" } | Remove-VpnConnection -Force
	
	#Запускаем приложение
	
	Show-MainForm_psf
}

#Собственно, само приложение

function Show-MainForm_psf
{
	
	[void][reflection.assembly]::Load('System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089')
	[void][reflection.assembly]::Load('System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a')
	
	#Создаем форму и объекты формы
	
	[System.Windows.Forms.Application]::EnableVisualStyles()
	$formКлиентыАльбус = New-Object 'System.Windows.Forms.Form'
	$statusbar1 = New-Object 'System.Windows.Forms.StatusBar'
	$groupboxTools = New-Object 'System.Windows.Forms.GroupBox'
	$buttonPing = New-Object 'System.Windows.Forms.Button'
	$buttonВыход = New-Object 'System.Windows.Forms.Button'
	$buttonWindox = New-Object 'System.Windows.Forms.Button'
	$buttonПеречитатьДанные = New-Object 'System.Windows.Forms.Button'
	$buttonPingAll = New-Object 'System.Windows.Forms.Button'
	$groupboxRDP = New-Object 'System.Windows.Forms.GroupBox'
	$comboboxRDP = New-Object 'System.Windows.Forms.ComboBox'
	$textboxRDPLogin = New-Object 'System.Windows.Forms.TextBox'
	$textboxRdpPwd = New-Object 'System.Windows.Forms.TextBox'
	$buttonПодключитьRDP = New-Object 'System.Windows.Forms.Button'
	$groupboxVPN = New-Object 'System.Windows.Forms.GroupBox'
	$buttonПодключитьVPN = New-Object 'System.Windows.Forms.Button'
	$buttonОтключитьVPN = New-Object 'System.Windows.Forms.Button'
	$checkboxШлюзПоумолчанию = New-Object 'System.Windows.Forms.CheckBox'
	$richtextboxinfo = New-Object 'System.Windows.Forms.RichTextBox'
	$listbox_clients = New-Object 'System.Windows.Forms.ListBox'
	$InitialFormWindowState = New-Object 'System.Windows.Forms.FormWindowState'
  
	#----------------------------------------------
	# Обработчики событий
	#----------------------------------------------
	
	$formКлиентыАльбус_Load = {
		#При загрузке формы очистить поле информации и заполнить поле с клиентами (их названиями) 
		$richtextboxinfo.Clear()
		$Global:Clients | ForEach-Object {
			[void]$listbox_clients.Items.Add($_.Name)
			
		} # В листбокс добавляем всех наших клиентов по именам и массива при загрузке формы
	}
	
	$listbox_clients_SelectedIndexChanged = {
		#Прочитать из массива информацию о клиенте при выборе его в поле listbox_clients (массив, как мы помним считан из файла с диска Google)
		
		$statusbar1.Text = 'Выбран клиент: ' + $listbox_clients.SelectedItem.ToString() # Пишем клиента в статусбар
		
		$Global:Current = $Global:Clients.Where({ $_.Name -eq $listbox_clients.SelectedItem.ToString() })
		If ($Current.PortWinbox -ne 0) # Если порт Winbox указан, то у клиента Mikrotik, включаем соответствующую кнопку
		{
			$buttonWindox.Enabled = $true
			
			$buttonWindox.Text = "Winbox"
		}
		
		$VPNname = $Global:Current.VPNname + "-tmp" #Добавляем к имени VPN соединения "-tmp" для указания метки временного соединения, чтобы при выходе удалить только их
		
		switch ($Global:Current.VPNType) #В зависимости от типа VPN пишем на кнопке "Подключить pptp VPN" или "Подключить l2tp VPN", если у клиента нет VPN, то пишем "Здесь нет VPN"
		{
			
			"pptp" {
				$buttonПодключитьVPN.Enabled = $true
				$buttonПодключитьVPN.Text = "Подключить pptp VPN"
			}
			"l2tp" {
				$buttonПодключитьVPN.Enabled = $true
				$buttonПодключитьVPN.Text = "Подключить l2tp VPN"
			}
			DEFAULT
			
			{
				$buttonПодключитьVPN.Enabled = $false
				$buttonПодключитьVPN.Text = "Здесь нет VPN"
			}
		}
		switch ($Global:Current.DefaultGateway) #Смотрим в массиве, используется ли у клиента "Шлюз по-умолчанию" и заполняем соответствующий чекбокс
		{
			"FALSE"
			{ $checkboxШлюзПоумолчанию.Checked = $false }
			"Нет"
			{ $checkboxШлюзПоумолчанию.Checked = $false }
			"TRUE"
			{ $checkboxШлюзПоумолчанию.Checked = $true }
			"Да"
			{ $checkboxШлюзПоумолчанию.Checked = $true }
			DEFAULT
			{ $checkboxШлюзПоумолчанию.Checked = $false }
		}
		
		$VPNStatus = (ipconfig | Select-String $VPNname -Quiet) #Проверяем, не установлено ли уже это VPN соединение?
		
		If ($VPNStatus) #Если установлено, то разблокируем кнопку "Подключить RDP"
		{
			$buttonПодключитьRDP.Enabled = $true
		}
		else
		{
			$buttonПодключитьRDP.Enabled = $false
		}
		
		$richtextboxinfo.Clear() #Очищаем информационное поле 
		
		# И заполняем информацией о клиенте из массива
		$richtextboxinfo.SelectionColor = 'Black'
		$richtextboxinfo.Text = "Клиент: " + $Global:Current.Name + [System.Environment]::NewLine + `
		"Имя VPN: " + $Global:Current.VPNname + [System.Environment]::NewLine + `
		"Тип VPN: " + $Global:Current.VPNType + [System.Environment]::NewLine + `
		"Адрес сервера: " + $Global:Current.ServerAddress + [System.Environment]::NewLine + `
		"Подсеть клиента: " + $Global:Current.RemoteNetwork + [System.Environment]::NewLine + `
		"Адрес сервера RDP: " + $Global:Current.RDPcomp + [System.Environment]::NewLine + [System.Environment]::NewLine + `
		"DefaultGateway: " + $Global:Current.DefaultGateway + [System.Environment]::NewLine + [System.Environment]::NewLine + `
		"Примечание: " + [System.Environment]::NewLine + $Global:Current.Inform + [System.Environment]::NewLine + `
		"Connection '" + $VPNname + "' status is " + $buttonПодключитьRDP.Enabled + [System.Environment]::NewLine
		$richtextboxinfo.AppendText($Global:Current.Link)
		
		$RDPServers = $Global:Current.RDPcomp.Split(';') -replace '\s', '' #Считываем и разбираем RDP серверы клиента из строки с разделителем в массив
		
		#Добавляем из в выпадающее поле выбора сервера
		$comboboxRDP.Items.Clear()
		$comboboxRDP.Text = $RDPServers[0]
		foreach ($RDPServer in $RDPServers)
		{
			$comboboxRDP.Items.Add($RDPServer)
		}
		
		#Заполняем поля имени и пароля RDP по умолчанию из таблицы о клиенте (при желании, их можно поменять в окне программы)
		$textboxRdpPwd.Text = $Global:Current.RDPpass
		$textboxRdpLogin.Text = $Global:Current.RDPuser
	} # Форма заполнена, при смене выбранного клиента произойдет перезаполнение полей в соответствии с выбранным клиентом
	
	$buttonWindox_Click = {
		#Обработка нажатия кнопки Winbox
		If ($Global:Current.PortWinbox -ne 0) #Если порт Winbox заполнен, то открываем скачанный ранее Winbox, подставляем туда имя и пароль к нему и запускаем
		{
			$runwinbox = "$env:temp\winbox64.exe"
			$ServerPort = $Global:Current.ServerAddress + ":" + $Global:Current.PortWinbox
			$ServerLogin = " """ + $Global:Current.WinboxLogin + """"
			$ServerPass = " """ + $Global:Current.WinboxPwd + """"
			$Arg = "$ServerPort $ServerLogin $ServerPass "
			Start-Process -filePath $runwinbox -ArgumentList $Arg
		}
	}
	
	$buttonПодключитьVPN_Click = {
		#Обработка нажатия кнопки ПодключитьVPN
		$VPNname = $Global:Current.VPNname + "-tmp" #Добавляем к имени VPN соединения "-tmp" для указания метки временного соединения, чтобы при выходе удалить только их
		$richtextboxinfo.Clear() #Очищаем информационное поля для вывода туда информации о процессе подключения
		
		$richtextboxinfo.Text = "Клиент: " + $Global:Current.Name + [System.Environment]::NewLine
		
		foreach ($item in get-vpnconnection | where { $_.ConnectionStatus -eq "Connected" }) #Разрываем все установленные соединения
		{
			$richtextboxinfo.Text = $richtextboxinfo.Text + "Обнаружено активное соединение " + $item.Name + " разрываем его" + [System.Environment]::NewLine
			Rasdial $item.Name /disconnect
		}
		
		Remove-VpnConnection $VPNname -Force #Удаляем соединение, если ранее оно было создано
		
		$RemoteNetworks = $Global:Current.RemoteNetwork.Split(';') -replace '\s', '' #Считываем и разбираем по строкам в массив список подсетей клиента разделенный ;
		
		switch ($Global:Current.VPNType) #В зависимости от типа VPNа создаем pptp или l2tp соединение
		{
			
			"pptp" {
				$richtextboxinfo.Text = $richtextboxinfo.Text + "Создаем pptp подключение " + $VPNname + [System.Environment]::NewLine
				If ($checkboxШлюзПоумолчанию.Checked -eq $false) #Если не используется "Шлюз по-умолчанию", то создаем VPN соединение без него и прописываем маршруты
				{
					$Errcon = (Add-VpnConnection -Name $VPNname -ServerAddress $Global:Current.ServerAddress -TunnelType $Global:Current.VPNType -SplitTunneling -Force -RememberCredential -PassThru) #Здесь происходит создание VPN
					
					foreach ($RemoteNetwork in $RemoteNetworks) #Добавляем все подсети клиента к этому VPN
					{
						$richtextboxinfo.AppendText('Добавляем маршрут к ' + $RemoteNetwork + [System.Environment]::NewLine)
						Add-VpnConnectionRoute -ConnectionName $VPNname -DestinationPrefix $RemoteNetwork -PassThru
					}
					
				}
				
				else #Если используется "Шлюз по-умолчанию", то создаем VPN соединение с ним и маршруты к клиенту не нужны
				{
					$Errcon = (Add-VpnConnection -Name $VPNname -ServerAddress $Global:Current.ServerAddress -TunnelType $Global:Current.VPNType -Force -RememberCredential -PassThru)
				}
				
			}
			"l2tp" {
				$richtextboxinfo.Text = $richtextboxinfo.Text + "Создаем l2tp подключение " + $Global:Current.VPNname + [System.Environment]::NewLine
				If ($checkboxШлюзПоумолчанию.Checked -eq $false) #Если не используется "Шлюз по-умолчанию", то создаем VPN соединение без него и прописываем маршруты
				{
					$Errcon = (Add-VpnConnection -Name $VPNname -ServerAddress $Global:Current.ServerAddress -TunnelType $Global:Current.VPNType -L2tpPsk $Global:Current.l2tpPsk -SplitTunneling -Force -RememberCredential -PassThru) #Здесь происходит создание VPN
					foreach ($RemoteNetwork in $RemoteNetworks) #Добавляем все подсети клиента к этому VPN
					{
						$richtextboxinfo.AppendText('Добавляем маршрут к ' + $RemoteNetwork + [System.Environment]::NewLine)
						Add-VpnConnectionRoute -ConnectionName $VPNname -DestinationPrefix $RemoteNetwork -PassThru
					}
				}
				else #Если используется "Шлюз по-умолчанию", то создаем VPN соединение с ним и маршруты к клиенту не нужны
				{
					$Errcon = (Add-VpnConnection -Name $VPNname -ServerAddress $Global:Current.ServerAddress -TunnelType $Global:Current.VPNType -L2tpPsk $Global:Current.l2tpPsk -Force -RememberCredential -PassThru)
				}
				
			}
			
		}
		
		$richtextboxinfo.AppendText("Устанавливаем " + $Global:Current.VPNType + " подключение к " + $VPNname + [System.Environment]::NewLine)
		$Errcon = Rasdial $VPNname $Global:Current.VPNLogin $Global:Current.VPNPass #Устанавливаем созданное VPN подключение и выводим информацию в поле
		
		$richtextboxinfo.Text = $richtextboxinfo.Text + [System.Environment]::NewLine + $Errcon + [System.Environment]::NewLine
		
		If ((ipconfig | Select-String $VPNname -Quiet)) #Проверяем успешность соединения и, если все удачно, разблокируем кнопку RDP  и кнопку "Отключить VPN"
		{
			$buttonПодключитьRDP.Enabled = $true
			$buttonОтключитьVPN.Visible = $true
			$buttonОтключитьVPN.Enabled = $true
			$statusbar1.Text = $Global:Current.Name + ' подключен'
		}
		
	}
	
	$formКлиентыАльбус_FormClosing = [System.Windows.Forms.FormClosingEventHandler]{
		
		#При закрытии формы подчищаем за собой. Разрываем и удаляем все созданные соединения. 
		
		foreach ($item in get-vpnconnection | where { $_.ConnectionStatus -eq "Connected" })
		{
			$richtextboxinfo.Text = $richtextboxinfo.Text + "Обнаружено активное соединение " + $item.Name + " разрываем его" + [System.Environment]::NewLine
			Rasdial $item.Name /disconnect
		}
		$richtextboxinfo.Text = $richtextboxinfo.Text + "Удаляем все временные соединения" + [System.Environment]::NewLine
		get-vpnconnection | where { $_.Name -match "tmp" } | Remove-VpnConnection -Force
		
		#Удаляем информацию о RPD-серверах из реестра
		$Global:Clients | ForEach-Object {
			$term = "TERMSRV/" + $_.RDPcomp
			cmdkey /delete:$term
		}
	}
	
	$buttonПодключитьRDP_Click = {
		#Обработка кнопки ПодключитьRDP
		$RDPcomp = $comboboxRDP.Text
		$RDPuser = $textboxRDPLogin.Text
		$RDPpass = $textboxRdpPwd.Text
		cmdkey /generic:"TERMSRV/$RDPcomp" /user:"$RDPuser" /pass:"$RDPpass"
		mstsc /v:$RDPcomp		
	}
	
	$buttonОтключитьVPN_Click = {
		#При отключении VPN подчищаем за собой и оповещаем о процессе в поле информации
		foreach ($item in get-vpnconnection | where { $_.ConnectionStatus -eq "Connected" })
		{
			$richtextboxinfo.Text = $richtextboxinfo.Text + "Обнаружено активное соединение " + $item.Name + " разрываем его" + [System.Environment]::NewLine
			Rasdial $item.Name /disconnect
		}
		$richtextboxinfo.Text = $richtextboxinfo.Text + "Удаляем все временные соединения" + [System.Environment]::NewLine
		get-vpnconnection | where { $_.Name -match "tmp" } | Remove-VpnConnection -Force
		
		$buttonОтключитьVPN.Visible = $false
		$buttonПодключитьRDP.Enabled = $false
		$statusbar1.Text = $Global:Current.Name + ' отключен'	
	}
	
	$buttonPingAll_Click={
		#Пингуем всех клиентов и оповещаем о результатах
		$I=0
		$richtextboxinfo.Clear()
		$richtextboxinfo.SelectionColor = 'Black'
		$clientscount = $Global:Clients.count
				
		$Global:Clients | ForEach-Object {
			if ((test-connection -Count 1 -computer $_.ServerAddress -quiet) -eq $True)
			{
				$richtextboxinfo.SelectionColor = 'Green'
				$richtextboxinfo.AppendText($_.Name +' ('+ $_.ServerAddress +') доступен' + [System.Environment]::NewLine)
			}
			else
			{
				$richtextboxinfo.SelectionColor = 'Red'
				$richtextboxinfo.AppendText($_.Name + ' (' + $_.ServerAddress + ')  недоступен (или закрыт ICMP)' + [System.Environment]::NewLine)
			}
			$richtextboxinfo.ScrollToCaret()
			$I = $I + 1
			Write-Progress -Activity "Ping in Progress" -Status "$i clients of $clientscount pinged" -PercentComplete ($i/$clientscount*100)
			
		}
		$richtextboxinfo.SelectionColor = 'Black'
		Write-Progress -Activity "Ping in Progress" -Status "Ready" -Completed
	}

$buttonПеречитатьДанные_Click={
		#Перечитываем данные из таблицы Google
		$Global:Clients = Get-Clients ($Global:google_file)
		$listbox_clients.Items.Clear()
		$Global:Clients | ForEach-Object {
			[void]$listbox_clients.Items.Add($_.Name)
		}
	}
	$buttonВыход_Click = {
		#Выход
		$formКлиентыАльбус.Close()
	}
	$richtextboxinfo_LinkClicked=[System.Windows.Forms.LinkClickedEventHandler]{
	#Обработка нажатия на ссылку в окне информации
		Start-Process $_.LinkText.ToString()
	}
	$buttonPing_Click={
		#Пингуем ip текущего клиента и выводим результат в поле информации
		if ((test-connection -Count 1 -computer $Global:Current.ServerAddress -quiet) -eq $True)
		{
			$richtextboxinfo.AppendText([System.Environment]::NewLine)
			$richtextboxinfo.SelectionColor = 'Green'
			$richtextboxinfo.AppendText($Global:Current.Name + ' (' + $Global:Current.ServerAddress + ') доступен' + [System.Environment]::NewLine)
		}
		else
		{
			$richtextboxinfo.AppendText([System.Environment]::NewLine)
			$richtextboxinfo.SelectionColor = 'Red'
			$richtextboxinfo.AppendText($Global:Current.Name + ' (' + $Global:Current.ServerAddress + ')  недоступен (или закрыт ICMP)' + [System.Environment]::NewLine)
		}
	}
	#----------------------------------------------
	#Описание объектов формы
	#----------------------------------------------
	#
	# formКлиентыАльбус
	#
	$formКлиентыАльбус.Controls.Add($statusbar1)
	$formКлиентыАльбус.Controls.Add($groupboxTools)
	$formКлиентыАльбус.Controls.Add($groupboxRDP)
	$formКлиентыАльбус.Controls.Add($groupboxVPN)
	$formКлиентыАльбус.Controls.Add($richtextboxinfo)
	$formКлиентыАльбус.Controls.Add($listbox_clients)
	$formКлиентыАльбус.AutoScaleDimensions = '6, 13'
	$formКлиентыАльбус.AutoScaleMode = 'Font'
	$formКлиентыАльбус.AutoSize = $True
	$formКлиентыАльбус.ClientSize = '763, 446'
	$formКлиентыАльбус.FormBorderStyle = 'FixedSingle'
	$formКлиентыАльбус.MaximizeBox = $False
	$formКлиентыАльбус.Name = 'formКлиентыАльбус'
	$formКлиентыАльбус.SizeGripStyle = 'Hide'
	$formКлиентыАльбус.StartPosition = 'CenterScreen'
	$formКлиентыАльбус.Text = 'Клиенты Альбус'
	$formКлиентыАльбус.add_FormClosing($formКлиентыАльбус_FormClosing)
	$formКлиентыАльбус.add_Load($formКлиентыАльбус_Load)
	#
	# statusbar1
	#
	$statusbar1.Location = '0, 424'
	$statusbar1.Name = 'statusbar1'
	$statusbar1.Size = '763, 22'
	$statusbar1.TabIndex = 17
	#
	# groupboxTools
	#
	$groupboxTools.Controls.Add($buttonPing)
	$groupboxTools.Controls.Add($buttonВыход)
	$groupboxTools.Controls.Add($buttonWindox)
	$groupboxTools.Controls.Add($buttonПеречитатьДанные)
	$groupboxTools.Controls.Add($buttonPingAll)
	$groupboxTools.Location = '308, 258'
	$groupboxTools.Name = 'groupboxTools'
	$groupboxTools.Size = '147, 163'
	$groupboxTools.TabIndex = 10
	$groupboxTools.TabStop = $False
	$groupboxTools.Text = 'Tools'
	$groupboxTools.UseCompatibleTextRendering = $True
	#
	# buttonPing
	#
	$buttonPing.Location = '7, 44'
	$buttonPing.Name = 'buttonPing'
	$buttonPing.Size = '133, 23'
	$buttonPing.TabIndex = 12
	$buttonPing.Text = 'Ping'
	$buttonPing.UseCompatibleTextRendering = $True
	$buttonPing.UseVisualStyleBackColor = $True
	$buttonPing.add_Click($buttonPing_Click)
	#
	# buttonВыход
	#
	$buttonВыход.Location = '7, 125'
	$buttonВыход.Name = 'buttonВыход'
	$buttonВыход.Size = '133, 23'
	$buttonВыход.TabIndex = 15
	$buttonВыход.Text = 'Выход'
	$buttonВыход.UseCompatibleTextRendering = $True
	$buttonВыход.UseVisualStyleBackColor = $True
	$buttonВыход.add_Click($buttonВыход_Click)
	#
	# buttonWindox
	#
	$buttonWindox.Enabled = $False
	$buttonWindox.Location = '7, 17'
	$buttonWindox.Name = 'buttonWindox'
	$buttonWindox.Size = '133, 23'
	$buttonWindox.TabIndex = 11
	$buttonWindox.Text = 'Windox'
	$buttonWindox.UseCompatibleTextRendering = $True
	$buttonWindox.UseVisualStyleBackColor = $True
	$buttonWindox.add_Click($buttonWindox_Click)
	#
	# buttonПеречитатьДанные
	#
	$buttonПеречитатьДанные.Location = '7, 98'
	$buttonПеречитатьДанные.Name = 'buttonПеречитатьДанные'
	$buttonПеречитатьДанные.Size = '133, 23'
	$buttonПеречитатьДанные.TabIndex = 14
	$buttonПеречитатьДанные.Text = 'Перечитать данные'
	$buttonПеречитатьДанные.UseCompatibleTextRendering = $True
	$buttonПеречитатьДанные.UseVisualStyleBackColor = $True
	$buttonПеречитатьДанные.add_Click($buttonПеречитатьДанные_Click)
	#
	# buttonPingAll
	#
	$buttonPingAll.Location = '7, 71'
	$buttonPingAll.Name = 'buttonPingAll'
	$buttonPingAll.Size = '133, 23'
	$buttonPingAll.TabIndex = 13
	$buttonPingAll.Text = 'Ping All'
	$buttonPingAll.UseCompatibleTextRendering = $True
	$buttonPingAll.UseVisualStyleBackColor = $True
	$buttonPingAll.add_Click($buttonPingAll_Click)
	#
	# groupboxRDP
	#
	$groupboxRDP.Controls.Add($comboboxRDP)
	$groupboxRDP.Controls.Add($textboxRDPLogin)
	$groupboxRDP.Controls.Add($textboxRdpPwd)
	$groupboxRDP.Controls.Add($buttonПодключитьRDP)
	$groupboxRDP.Location = '308, 128'
	$groupboxRDP.Name = 'groupboxRDP'
	$groupboxRDP.Size = '147, 126'
	$groupboxRDP.TabIndex = 5
	$groupboxRDP.TabStop = $False
	$groupboxRDP.Text = 'RDP'
	$groupboxRDP.UseCompatibleTextRendering = $True
	#
	# comboboxRDP
	#
	$comboboxRDP.FormattingEnabled = $True
	$comboboxRDP.Location = '7, 17'
	$comboboxRDP.Name = 'comboboxRDP'
	$comboboxRDP.Size = '133, 21'
	$comboboxRDP.TabIndex = 6
	$comboboxRDP.Text = 'IP RDP сервера'
	#
	# textboxRDPLogin
	#
	$textboxRDPLogin.Location = '7, 44'
	$textboxRDPLogin.Name = 'textboxRDPLogin'
	$textboxRDPLogin.Size = '133, 20'
	$textboxRDPLogin.TabIndex = 7
	$textboxRDPLogin.Text = 'RDP-login'
	#
	# textboxRdpPwd
	#
	$textboxRdpPwd.Location = '7, 69'
	$textboxRdpPwd.Name = 'textboxRdpPwd'
	$textboxRdpPwd.PasswordChar = '*'
	$textboxRdpPwd.Size = '133, 20'
	$textboxRdpPwd.TabIndex = 8
	$textboxRdpPwd.Text = 'RDP-Password'
	#
	# buttonПодключитьRDP
	#
	$buttonПодключитьRDP.Enabled = $False
	$buttonПодключитьRDP.Location = '7, 94'
	$buttonПодключитьRDP.Name = 'buttonПодключитьRDP'
	$buttonПодключитьRDP.Size = '133, 20'
	$buttonПодключитьRDP.TabIndex = 9
	$buttonПодключитьRDP.Text = 'Подключить RDP'
	$buttonПодключитьRDP.UseCompatibleTextRendering = $True
	$buttonПодключитьRDP.UseVisualStyleBackColor = $True
	$buttonПодключитьRDP.add_Click($buttonПодключитьRDP_Click)
	#
	# groupboxVPN
	#
	$groupboxVPN.Controls.Add($buttonПодключитьVPN)
	$groupboxVPN.Controls.Add($buttonОтключитьVPN)
	$groupboxVPN.Controls.Add($checkboxШлюзПоумолчанию)
	$groupboxVPN.Location = '308, 27'
	$groupboxVPN.Name = 'groupboxVPN'
	$groupboxVPN.Size = '147, 98'
	$groupboxVPN.TabIndex = 1
	$groupboxVPN.TabStop = $False
	$groupboxVPN.Text = 'VPN'
	$groupboxVPN.UseCompatibleTextRendering = $True
	#
	# buttonПодключитьVPN
	#
	$buttonПодключитьVPN.Enabled = $False
	$buttonПодключитьVPN.Location = '7, 45'
	$buttonПодключитьVPN.Name = 'buttonПодключитьVPN'
	$buttonПодключитьVPN.Size = '133, 20'
	$buttonПодключитьVPN.TabIndex = 3
	$buttonПодключитьVPN.Text = 'Подключить VPN'
	$buttonПодключитьVPN.UseCompatibleTextRendering = $True
	$buttonПодключитьVPN.UseVisualStyleBackColor = $True
	$buttonПодключитьVPN.add_Click($buttonПодключитьVPN_Click)
	#
	# buttonОтключитьVPN
	#
	$buttonОтключитьVPN.Enabled = $False
	$buttonОтключитьVPN.Location = '7, 67'
	$buttonОтключитьVPN.Name = 'buttonОтключитьVPN'
	$buttonОтключитьVPN.Size = '133, 20'
	$buttonОтключитьVPN.TabIndex = 4
	$buttonОтключитьVPN.Text = 'Отключить VPN'
	$buttonОтключитьVPN.UseCompatibleTextRendering = $True
	$buttonОтключитьVPN.UseVisualStyleBackColor = $True
	$buttonОтключитьVPN.Visible = $False
	$buttonОтключитьVPN.add_Click($buttonОтключитьVPN_Click)
	#
	# checkboxШлюзПоумолчанию
	#
	$checkboxШлюзПоумолчанию.Location = '7, 19'
	$checkboxШлюзПоумолчанию.Name = 'checkboxШлюзПоумолчанию'
	$checkboxШлюзПоумолчанию.Size = '133, 24'
	$checkboxШлюзПоумолчанию.TabIndex = 2
	$checkboxШлюзПоумолчанию.Text = 'Шлюз по-умолчанию'
	$checkboxШлюзПоумолчанию.TextAlign = 'MiddleRight'
	$checkboxШлюзПоумолчанию.UseCompatibleTextRendering = $True
	$checkboxШлюзПоумолчанию.UseVisualStyleBackColor = $True
	#
	# richtextboxinfo
	#
	$richtextboxinfo.Cursor = 'Default'
	$richtextboxinfo.ForeColor = 'WindowText'
	$richtextboxinfo.HideSelection = $False
	$richtextboxinfo.Location = '461, 27'
	$richtextboxinfo.Name = 'richtextboxinfo'
	$richtextboxinfo.ReadOnly = $True
	$richtextboxinfo.ScrollBars = 'ForcedVertical'
	$richtextboxinfo.ShowSelectionMargin = $True
	$richtextboxinfo.Size = '290, 394'
	$richtextboxinfo.TabIndex = 16
	$richtextboxinfo.Text = ''
	$richtextboxinfo.add_LinkClicked($richtextboxinfo_LinkClicked)
	#
	# listbox_clients
	#
	$listbox_clients.FormattingEnabled = $True
	$listbox_clients.Location = '12, 27'
	$listbox_clients.Name = 'listbox_clients'
	$listbox_clients.Size = '290, 394'
	$listbox_clients.TabIndex = 0
	$listbox_clients.add_SelectedIndexChanged($listbox_clients_SelectedIndexChanged)

  #Сохраняем состояние формы
	$InitialFormWindowState = $formКлиентыАльбус.WindowState
	#Восстанавливаем состояние при загрузке
	$formКлиентыАльбус.add_Load($Form_StateCorrection_Load)
	#Очищаем элементы формы при закрытии
	$formКлиентыАльбус.add_FormClosed($Form_Cleanup_FormClosed)
	#Сохраняем значения элементов при закрытии
	$formКлиентыАльбус.add_Closing($Form_StoreValues_Closing)
	#Показать форму
	return $formКлиентыАльбус.ShowDialog()
}
#Запуск приложения!
Main ($CommandLine) 

Скрипт можно запускать как скрипт ps1 или скомпилировать в exe через ps2exe и использовать как полноценное приложение

UPD: скрипт может работать непосредственно в Powershell, достаточно запустить PS от имени Администратора, скопировать и вставить скрипт в окно PS