Некоторые картинки не загружаются из РФ и РК, используйте VPN.

среда, 27 мая 2020 г.

Проект #31. Переводим пользователей на удаленку

Корона внесла неожиданные изменения в нашу работу, приходится подстраиваться. Крупные организации, или кого не зацепило спокойно скупили ноутбуки сотрудникам и радуются жизни. В нашем случае пришлось искать другие пути.
Кто-то пользуется Teamviewer`ом, ну что ж, флаг им в руки, а я буду держаться подальше по следующим причинам:
  1. Программа платная (использование за пределами одной сети = коммерческое использование)
  2. Использование 3 точки в цепочке
  3. Повышенная нагрузка на сервис
  4. Режим работы без блокировки монитора
  5. и т.д.

Можно использовать RDP, хорошо, но:
  1. Нужно иметь сервер терминалов или подключаться к пользовательским ПК
  2. Проброс порта RDP не помеха для сканера, гигантская дыра в безопасности
  3. Пробросить порты на RDP на 5 машин можно, но на 50+ и без статики на тачках - такое себе
  4. и т.д.
На ум напрашивается VPN и пущай пользователь как-то подключается к ресурсу (тот же Teamviewer,LiteManager, RDP)
На всех объектах используется роутер Mikrotik.
Я избрал следующий путь для одного объекта:
  1. Настраиваем ПК пользователей
    1. Включаем Wake-On-LAN и проверяем
    2. Персонифицируем имена компьютеров (допустим по фамилии сотрудника)
    3. Включаем RDP (напоминаю, он есть только в редакциях PRO и выше)
    4. Проверяем настройки файервола и антивируса, чтобы они не блокировали трафик для удаленного доступа от пула адресов VPN сервера
  2. Настраиваем роутер:
    1. Пул DHCP сервера должен быть отличен от 192.168.Х.Х, т.к. у пользователей дома именно такая сеть.
    2. Фиксируем лизы для компьютеров
    3. Настраиваем L2TP IPsec PSK (или другой VPN сервер на роутере)
    4. Создаем пользователей (имя пользователя соответствует имени его ПК)
    5. Проверяем настройки файервола, чтобы трафик из сети VPN ходил в сеть организации
    6. В разделе PPP>Profiles>НашПрофиль>вкладка Scripts, здесь мы пишем скрипт в On Up (получаем MAC,шлем магический пакет):
      :local macAddress [/ip dhcp-server lease get [/ip dhcp-server lease find host-name=$user] mac-address];
      tool wol interface=bridge mac=$macAddress
      :log info message=("User " . $user . " connetcted. Sending magic packet for MAC="  . $macAddress)
  3. Настраиваем подключение с личного ПК пользователя, обязательно отключаем "Использовать как основной шлюз"
На другом объекте машин 50+, и DHCP сервер находится на контроллере домена, хоть шлюзом и выступает Mikrotik. Почесав репу, вспомнил что список MAC-NAME_PC у меня есть на сервере для бэкапов. Вот и решил его использовать. Логика остается та же:
  1. Имя пользователя ПК=имени ПК=имя пользователя VPN
  2. При подключении находим MAC, отправляем магический пакет
  3. Если через 60 секунд машина не пингуется, то сбросить подключение
  4. Иначе добавить правило в файервол, разрешающее трафик на порт 3389 от конкретного пользователя к конкретному ПК
  5. При отключении удалить правило
Мысль огонь, мне зашло как надо, но в ходе реализации я столкнулся с кучей проблем:
  1. Язык Mikrotik оставляет желать лучшего, аналога awk нету
  2. Область действия переменных. Т.к. итоговый скрипт длинный, я решил его записать в разделе System>Script, но для того чтобы его вызвать при подключении пользователя, нужно объявить переменные (параметры для скрипта) как глобальные. Глобальные переменные работают на все подключения и тут возникает гигантская коллизия. Пришлось при получении параметра сразу уничтожать глобальную переменную.
  3. Попытка разбора файла на сервере с UNIX системой потерпела фиаско из-за требования входить на сервер по сертификату
  4. и т.д.
В итоге листинг скрипта:
#get name user, resolve, wol, ping, add rule
#Example run
#:global Iuser $user;:global IremoteAddress $"remote-address";/system script run plug_user_local
#Two input param 
:global Iuser;
:global IremoteAddress;

:put ("Input: " .$Iuser. " ". $IremoteAddress)
:local user [$Iuser]
:local remoteAddress [$IremoteAddress];

# Remove global var
/system script environment remove IremoteAddress
/system script environment remove Iuser

:local dnsServer "10.20.0.2"
:local HOST ($user . ".mydomain.ru");
:local PINGCOUNT "2";
:local CodeExitPing;
:local ipAdr;
:local macAddr;

:local patern $user;
:local FileName "compoff2.txt"; 

:local StatusCode "1"

:put ("LocalInput: " .$user. " ". $remoteAddress)
:put ("plug: " . $HOST  . " ". $PINGCOUNT  . " ". $dnsServer)
#####################
##FUNC##

###GETMAC
:local getMac do={
 :local macAddr "";
 :do { [/file get [/file find name=$FileName]]} on-error={:set macAddr "0"}
 :put ("FN " . $FileName . " " .$patern)
 if ([ :len $FileName]!=0 and [ :len $patern ]!=0 and  [/file get [/file find name=$FileName] size] != 0 and $macAddr="") do={
  :put ("getMac begin")
  :local content [/file get [/file find name=$FileName] contents] ;
  :local contentLen [ :len $content ] ;  
  :local lineEnd 0;
  :local line "";
  :local lastEnd 0;   
  :do {
   :set lineEnd [:find $content "\n" $lastEnd ] ;
   :set line [:pick $content $lastEnd $lineEnd] ;
   :set lastEnd ( $lineEnd + 1 ) ;   
   :if ( [:pick $line 0 1] != "#" ) do={   
    :local entry [:pick $line 0 $lineEnd ]
    :if ( [:len $entry ] > 0 ) do={
     if ([:find $entry $patern]>0) do={
      :set macAddr [:pick $entry ([:len $entry]-20) ([:len $entry]-3)]
      #break
      :set lastEnd ( $contentLen + 1 ) ;   
     }
    }
   }
  } while ($lastEnd < $contentLen)
 }
 if ($macAddr="") do={:set macAddr "0"}
 :return $macAddr
}
###checkPing
:local checkPing do={
 :put ("ping: " . $HOST ." ". $PINGCOUNT ." ". $dnsServer)
 :local ipAdr;
 :local CodeExitPing;
 if ( $HOST != "" and $PINGCOUNT != "" and $dnsServer != "") do={
  :do {:set ipAdr [:resolve $HOST server=$dnsServer]} on-error={:set ipAdr "0"};
  if ($ipAdr=0) do={
   :set CodeExitPing "0"
  } else {
   :set CodeExitPing [/ping $ipAdr interval=1 count=$PINGCOUNT];
  }
 } else={
  :set CodeExitPing "0";
  :put "Empty param"
 }
 :return $CodeExitPing
}

###FUNC##
#####################

:set CodeExitPing [$checkPing HOST=$HOST PINGCOUNT=$PINGCOUNT dnsServer=$dnsServer]
:put ("Exit code " . $CodeExitPing);
if ($CodeExitPing=0) do={
 set macAddr [$getMac patern=$patern FileName=$FileName]
 :put ("macAddr " . $macAddr)
 if ($macAddr != 0) do={
  :put ("|" . $macAddr . "|")
  /tool wol mac=$macAddr interface=LAN_Bridge
  :put "Delay 60s"
  :log info message=("Send magic packet on mac:".$macAddr.", for user ".$patern)
  :delay delay-time=60
  :set CodeExitPing [$checkPing HOST=$HOST PINGCOUNT=$PINGCOUNT dnsServer=$dnsServer]
  if ($CodeExitPing>0) do={
   :set StatusCode "0"
  }
 }
} else={
 :put "if else"
 :set StatusCode "0"
}

:put ("Pre error" . $StatusCode)

if ($StatusCode = 0) do={
 :set ipAdr [:resolve $HOST server=$dnsServer]
 :local email "it@mydomain.ru"
 /tool e-mail send to=$email subject="[VPN_RDP] User $user connected" body="User $user connected at $[/system clock get time].\r\nIP-address - $"caller-id".\r\nInfo - http://apps.db.ripe.net/search/query.html?searchtext=$"caller-id""
 :log info message=("Conected " . $user . " ip pc_office" . $ipAdr . " ip pc_vpn " . $remoteAddress)
 :local p [/ip firewall filter find comment~"VPN_AUTO_END"]; #Специально созданное правило-метка (оно еще разрешает DNS запросы)
 :local comm ("VPN_auto.".$user . "." . $remoteAddress) #унифицированная метка правила
 /ip firewall filter  add action=accept chain=forward comment=$comm dst-port=3389 out-interface=LAN_Bridge protocol=tcp src-address=$remoteAddress dst-address=$ipAdr
 /ip firewall filter  add action=accept chain=forward comment=$comm dst-port=3389 out-interface=LAN_Bridge protocol=udp src-address=$remoteAddress dst-address=$ipAdr
 /ip firewall filter move [find comment=$comm] destination=$p
} else={
 :log info message=("PC not found" . $remoteAddress)
 /ppp active remove [find where name=$user]
}

И собственно в On up:
:global Iuser $user;
:global IremoteAddress $"remote-address";
/system script run plug_user_local

И собственно в On down:
:local email "it@mydomain.ru"
/tool e-mail send to=$email subject="[VPN_RDP] User $user disconnected" body="User $user disconnected at $[/system clock get time]."
:local comm ("VPN_auto.".$user . "." . $"remote-address"); /ip firewall filter remove [find comment=$comm]


UP 27/05/2020
Забыл добавить, в последнем варианте при помощи GPO включается RDP на клиентских машинах и в локальную группу "Пользователи удаленного рабочего стола" добавляется разрешенный пользователь.
Именно для этого и было создано соответствие Имя Пользователя ПК (домена)=Имени ПК
Политика нацелена на ПК входящие в группу разрешенных и логика выглядит так:
Если ПК в группе Разрешенные, то добавить пользователя с именем mydomain\%computername% в группу


UP 10/12/2022, для отправки wol в другой сегмент широковещательной сети был добавлен запуск удаленной команды на другом Mikrotik, плюс куча лог-выводов для диагностики

#get name user, resolve, wol, ping, add rule
#Example run
#:global Iuser $user;:global IremoteAddress $"remote-address";/system script run plug_user_local
#Two input param 
:global Iuser;
:global IremoteAddress;
:global ICallerId;
:global WriteLog 1

:put ("Input: " .$Iuser. " ". $IremoteAddress)
:local user [$Iuser]
:local remoteAddress [$IremoteAddress];
:local CallerId [$ICallerId];

# Remove global var
/system script environment remove IremoteAddress
/system script environment remove Iuser
/system script environment remove ICallerId


#Для теста из консоли
#:local user "etishk"
#:local remoteAddress "10.30.5.10";

:local dnsServer "10.20.0.3"
:local HOST ($user . ".domain.ru");
:local PINGCOUNT "2";
:local CodeExitPing;
:local ipAdr;
:local macAddr;

:local patern $user;
:local FileName "compoff2.txt"; 

:local StatusCode "1"



#####################
##FUNC##
# Func loging. For debug. Error write forever, other only of WriteLog <0 (0 1 2 - level logging), setup up of global var
# $TYPE - topics (error,info)
# $LEVEL - level logging
# 	0 only error
#	1 main
#	2 all
# $MESSAGE - text message
# Example
# 	$Logging MESSAGE=("My text") TYPE="info"  LEVEL=2
# for use in func use global context and declared WriteLog
# Exmaple
# :global WriteLog
# $Logging MESSAGE=("lineEnd=(".$lineEnd.");line=(".$line.");lastEnd=(".$lastEnd.")") TYPE="info"  LEVEL=2

:global Logging do={
	:global WriteLog
	:local TextMessage ("PlugUser => ".$MESSAGE)	
	:if ($LEVEL>=$WriteLog) do={
		:if ($TYPE="info") do={ :log info message=($TextMessage) }
		:if ($TYPE="error") do={ :log error message=($TextMessage) }
	}
	:put $TextMessage
}
###GETMAC
:local getMac do={
	:global Logging
	:local macAddr "";
	:do { [/file get [/file find name=$FileName]]} on-error={:set macAddr "0"}
	$Logging MESSAGE=("FN=(" . $FileName . ") pattern=(" .$patern.")") TYPE="info" LEVEL=1
	:if ([:len $FileName]!=0 and [ :len $patern ]!=0 and  [/file get [/file find name=$FileName] size] != 0 and $macAddr="") do={
		$Logging MESSAGE=("Len Filename=(".[:len $FileName]."). Len pattern=(".[:len $patern ]."). FileSize=(".[/file get [/file find name=$FileName] size]."). macAddr can be empty=(".$macAddr.")") TYPE="info"  LEVEL=2
		:put ("getMac begin")
		:local content [/file get [/file find name=$FileName] contents] ;
		:local contentLen [ :len $content ] ;  
		:local lineEnd 0;
		:local line "";
		:local lastEnd 0;   
		:do {
			:set lineEnd [:find $content "\n" $lastEnd ] ;
			:set line [:pick $content $lastEnd $lineEnd] ;
			:set lastEnd ( $lineEnd + 1 ) ;   
			$Logging MESSAGE=("lineEnd=(".$lineEnd.");line=(".$line.");lastEnd=(".$lastEnd.")") TYPE="info"  LEVEL=2
			:if ( [:pick $line 0 1] != "#" ) do={   
				:local entry [:pick $line 0 $lineEnd ]
				:if ( [:len $entry ] > 0 ) do={
					if ([:find $entry $patern]>0) do={
						:set macAddr [:pick $entry ([:len $entry]-20) ([:len $entry]-3)]
						#break
						:set lastEnd ( $contentLen + 1 ) ;   
					}
				}
			}
		} while ($lastEnd > $contentLen)
	} else={
		$Logging MESSAGE=("Function getMac can not be exec") TYPE="error"  LEVEL=0
	}
	if ($macAddr="") do={:set macAddr "0"}
	$Logging MESSAGE=($macAddr) TYPE="info"  LEVEL=1
	:return $macAddr
}
###checkPing
:local checkPing do={
	:global Logging
	$Logging MESSAGE=("Ping: " . $HOST ." ". $PINGCOUNT ." ". $dnsServer) TYPE="info" LEVEL=1
	:local ipAdr;
	:local CodeExitPing;
	if ( $HOST != "" and $PINGCOUNT != "" and $dnsServer != "") do={
		:do {:set ipAdr [:resolve $HOST server=$dnsServer]} on-error={:set ipAdr "0"};
		if ($ipAdr=0) do={
			:set CodeExitPing "0"
			$Logging MESSAGE=("Not resolve host: " . $HOST ." on ". $dnsServer) TYPE="info" LEVEL=1
		} else={
			:set CodeExitPing [/ping $ipAdr interval=1 count=$PINGCOUNT];
			$Logging MESSAGE=("Resolve host: " . $HOST ." on ". $dnsServer." to ".$ipAdr) TYPE="info" LEVEL=1
		}
	} else={
		:set CodeExitPing "0";
		$Logging MESSAGE=("CheckPing Empty param") TYPE="error" LEVEL=0
		
	}
	:return $CodeExitPing
}

###FUNC##
#####################

$Logging MESSAGE=("LocalInput: " .$user. " ". $remoteAddress) TYPE="info" LEVEL=1

:set CodeExitPing [$checkPing HOST=$HOST PINGCOUNT=$PINGCOUNT dnsServer=$dnsServer]

$Logging MESSAGE=("CheckPing return exit code (". $CodeExitPing.")") TYPE="info" LEVEL=1


if ($CodeExitPing=0) do={
	:set macAddr [$getMac patern=$patern FileName=$FileName]
	if ($macAddr != 0) do={
		$Logging MESSAGE=("Send magic packet on mac:".$macAddr.", for user ".$patern) TYPE="info" LEVEL=1
		/tool wol mac=$macAddr interface=bridge1_LAN
		:local RemoteMessage ("Send magic packet on mac:$macAddr, for user $patern")
		
		do {/system ssh-exec user=vint address=10.21.0.1 command=("tool wol mac=$macAddr interface=bridge1_LAN; :log info message=(\"Send magic packet on mac:$macAddr, for user $patern\")")
			} on-error={
				$Logging MESSAGE=("SSH Command execution error") TYPE="error"  LEVEL=0
		}
		
		$Logging MESSAGE=("Delay 60s") TYPE="info"  LEVEL=2
		:delay delay-time=60
		:set CodeExitPing [$checkPing HOST=$HOST PINGCOUNT=$PINGCOUNT dnsServer=$dnsServer]
		if ($CodeExitPing>0) do={
			:set StatusCode "0"
		} else={
			$Logging MESSAGE=("Host (".$HOST.") not turning on") TYPE="error" LEVEL=1
		}
	} else={
		$Logging MESSAGE=("User not found (".$patern.")") TYPE="error" LEVEL=0
		:set StatusCode "1"
	}
} else={
	:set StatusCode "0"
}

$Logging MESSAGE=("Status code(".$StatusCode.")") TYPE="info" LEVEL=1

if ($StatusCode = 0) do={
	:set ipAdr [:resolve $HOST server=$dnsServer]
	:local email "it@domain.ru"
	/tool e-mail send to=$email subject="[VPN_RDP] User $user connected" body="User $user connected at $[/system clock get time].\r\nIP-address --- $CallerId.\r\nInfo - http://apps.db.ripe.net/search/query.html?searchtext=$CallerId"
	$Logging MESSAGE=("Conected " . $user . " ip pc_office" . $ipAdr . " ip pc_vpn " . $remoteAddress) TYPE="info" LEVEL=1
	:local p [/ip firewall filter find comment~"VPN_AUTO_END"];
	:local comm ("VPN_auto.".$user . "." . $remoteAddress)
	/ip firewall filter  add action=accept chain=forward comment=$comm dst-port=3389,5650 protocol=tcp src-address=$remoteAddress dst-address=$ipAdr
	/ip firewall filter move [find comment=$comm] destination=$p
} else={
	$Logging MESSAGE=("PC (".$remoteAddress.") not found. Name user (".$user.")" ) TYPE="info" LEVEL=1
	/ppp active remove [find where name=$user]
}

# Remove global var
/system script environment remove WriteLog
/system script environment remove Logging

Комментариев нет:

Отправить комментарий