Страницы

среда, 19 июня 2013 г.

Wake on LAN на PowerShell.

Wake on LAN (WOL) - технология, позволяющая удаленно включить компьютер путем отправки на его сетевой адаптер специально сформированного пакета данных состоящего из 6 байт FF и его собственного MAC - адреса, повторенного 16 раз (так называемого Magic Packet — "волшебного пакета"). Приготовим решение, использующее WOL, на PowerShell.



Процесс приготовления будет состоять из следующих этапов:
- получение MAC-адреса сетевого адаптера
- конфигурирование сетевого адаптера
- формирование и отправка "волшебного пакета"

1. Получим MAC-адрес сетевого адаптера (адаптеров), для чего используем WMI-класс Win32_NetworkAdapterConfiguration.

На VBScript код получения MAC-адреса мог бы выглядеть так:
For Each objItem In GetObject("winmgmts:\\.\root\cimv2").ExecQuery _
  ("Select MACAddress From Win32_NetworkAdapterConfiguration Where IPEnabled = True") 
 s = s & objItem.MACAddress & vbCrLf
Next
MsgBox s, vbInformation

На PowerShell все гораздо проще:
(Get-WmiObject -Class Win32_NetworkAdapterConfiguration -Filter IPEnabled=True  | 
Select-Object -Property MACAddress).MACAddress


Наверное удобнее сразу сохранять полученные MAC-адреса в файл CSV.
Кроме того сделаем наш код более "гибким" - напишем так называемую "продвинутую" функцию, которая будет получать в качестве параметра массив имен компьютеров и возвращать массив MAC-адресов.
function Get-MACAddress {
    [CmdletBinding()]
    param ([string[]]$HostName = 'localhost')        
    Get-WmiObject -Class Win32_NetworkAdapterConfiguration -Filter IPEnabled=True -ComputerName $HostName | `
    Select-Object -Property MACAddress        
}
Get-MACAddress -hostName localhost, localhost | `
Export-Csv -Path "mac.csv" -Encoding UTF8 -NoTypeInformation -Append


Теперь представьте себе сколько строчек кода это заняло бы на VBScript.
После отработки нашего "продвинутого" кода получаем примерно такой CSV-файл.

2. Конфигурируем сетевой адаптер (адаптеры) - разрешим ему (им) выводить компьютер из спящего режима, для чего используем утилиту powercfg.exe.

Можно было попробовать обойтись и без утилиты powercfg.exe - использовать WMI-классы MSPower_DeviceWakeEnable и MSNdis_DeviceWakeOnMagicPacketOnly, проживающие в пространстве имен root\wmi, но они не документированы и, как следствие, не поддерживаются Microsoft, поэтому у меня нет уверенности в том, что код, использующий эти классы отработает правильно на любой оси. По крайней мере на 64-битной 7 и 8 - не получилось, хотя в репозитории классы есть. В общем поверьте на слово - лучше использовать powercfg.exe.

Получить справку по использованию утилиты можно стандартным способом - powercfg /?

Команда powercfg /devicequery wake_programmable отображает список устройств, которые могут выводить компьютер из спящего режима.

Выполним команду в PowerShell ISE.

Выведем список имен сетевых адаптеров подключенных к сети, для чего используем WMI-класс Win32_NetworkAdapter.
Значение свойства NetConnectionStatus=2 доступно начиная с Windows XP SP3.

Комбинируем - попробуем получить список сетевых адаптеров подключенных к сети, у которых есть возможность выводить компьютер из спящего режима.
Сразу обернем код в функцию - пригодится.
# функция возвращает массив имен сетевых адаптеров подключенных к сети, 
# у которых есть возможность выводить компьютер из спящего режима
function Get-WakeAdapterName {
    [CmdletBinding()]
    param ([string[]]$HostName = 'localhost')    
    $arrWake = Invoke-Command -ComputerName $HostName -ScriptBlock {powercfg /devicequery wake_programmable} | `
            Where-Object {$_ -ne ''}    
    $arrAdapters = (Get-WmiObject -Class  Win32_NetworkAdapter -Filter NetConnectionStatus=2 -ComputerName $HostName | `
            Select-Object -Property Name).Name    
    for ($i=0; $i -lt $arrWake.length; $i++) {    
        for ($j=0; $j -lt $arrAdapters.length; $j++) {
            if ($arrWake[$i] -eq $arrAdapters[$j]) {
                Write-Output $arrWake[$i]                
            }
        }                     
    }    
}
Get-WakeAdapterName localhost, localhost

Выполним функцию.

Если у Вас проблема с настройкой PowerShell Remoting - можно выполнить код локально.
# функция возвращает массив имен сетевых адаптеров подключенных к сети, 
# у которых есть возможность выводить компьютер из спящего режима
function Get-WakeAdapterName {
    [CmdletBinding()]
    param ([string[]]$HostName = 'localhost')
    $arrWake = powercfg /devicequery wake_programmable | `
            Where-Object {$_ -ne ''}    
    $arrAdapters = (Get-WmiObject -Class  Win32_NetworkAdapter -Filter NetConnectionStatus=2 -ComputerName $HostName | `
            Select-Object -Property Name).Name    
    for ($i=0; $i -lt $arrWake.length; $i++) {    
        for ($j=0; $j -lt $arrAdapters.length; $j++) {
            if ($arrWake[$i] -eq $arrAdapters[$j]) {
                Write-Output $arrWake[$i]                
            }
        }                     
    }    
}
Get-WakeAdapterName localhost, localhost

Лирическое отступление на VBScript. Почему не получить список адаптеров так:
For Each objItem In GetObject("winmgmts:\\.\root\cimv2").ExecQuery _
  ("Select MACAddress From Win32_NetworkAdapterConfiguration Where IPEnabled = True") 
 For Each obj In objItem.Associators_(, "Win32_NetworkAdapter")  
  s = s & obj.Name
 Next
Next
MsgBox s, vbInformation


или так:
Set objWMI = GetObject("winmgmts:\\.\root\cimv2")
For Each objItem In objWMI.ExecQuery _
  ("Select MACAddress From Win32_NetworkAdapterConfiguration Where IPEnabled = True")
 For Each obj In objWMI.ExecQuery _
  ("Select * From Win32_NetworkAdapter Where MACAddress = '" & objItem.MACAddress & "' And PhysicalAdapter = True") 
  s = s & obj.Name
 Next
Next
MsgBox s, vbInformation


Очевидно: в первом случае отображается виртуальный адаптер Hyper-V, во втором он тоже в списке.
Поэтому использование powercfg.exe + NetConnectionStatus=2 на мой взгляд наиболее подходящий вариант.
Возражения принимаются.

Теперь попробуем отправить полученные функцией Get-WakeAdapterName имена адаптеров по конвейеру утилите powercfg.exe ...
# функция возвращает массив имен сетевых адаптеров подключенных к сети, 
# у которых есть возможность выводить компьютер из спящего режима
function Get-WakeAdapterName {
    [CmdletBinding()]
    param ([string[]]$HostName = 'localhost')
    $arrWake = powercfg /devicequery wake_programmable | `
            Where-Object {$_ -ne ''}    
    $arrAdapters = (Get-WmiObject -Class  Win32_NetworkAdapter -Filter NetConnectionStatus=2 -ComputerName $HostName | `
            Select-Object -Property Name).Name    
    for ($i=0; $i -lt $arrWake.length; $i++) {    
        for ($j=0; $j -lt $arrAdapters.length; $j++) {
            if ($arrWake[$i] -eq $arrAdapters[$j]) {
                Write-Output $arrWake[$i]                
            }
        }                     
    }    
}
Get-WakeAdapterName | ForEach-Object {powercfg deviceenablewake "$_"}


... и получаем законное возражение - я не под админом :).

Открываем PowerShell ISE от имени администратора, выполняем скрипт еще раз.

Проверяем свойства адаптера.

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

3. Сформируем и отправим "волшебный пакет".

MAC-адрес моего сетевого адаптера выглядит так: E4:D5:3D:97:D1:8C.

Стало быть в моем случае пакет должен выглядеть следующим образом:
FF FF FF FF FF FF E4 D5   3D 97 D1 8C E4 D5 3D 97
D1 8C E4 D5 3D 97 D1 8C   E4 D5 3D 97 D1 8C E4 D5
3D 97 D1 8C E4 D5 3D 97   D1 8C E4 D5 3D 97 D1 8C
E4 D5 3D 97 D1 8C E4 D5   3D 97 D1 8C E4 D5 3D 97
D1 8C E4 D5 3D 97 D1 8C   E4 D5 3D 97 D1 8C E4 D5
3D 97 D1 8C E4 D5 3D 97   D1 8C E4 D5 3D 97 D1 8C
E4 D5 3D 97 D1 8C

Пишем функцию отправки "волшебного пакета".
# функция формирования и отправки "волшебного пакета"
# получает MAC-адрес (обязательный параметр) и порты удаленного компьютера 
# (по умолчанию - 0, 7, 9)
function Send-Packet {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$True)]
        [string[]]$Mac,
        [int32[]]$Ports = @(0,7,9)
    )    
    $bmac = $Mac.split(':') | ForEach-Object { [byte]('0x' + $_) }
    $packet = [byte[]](,0xFF * 6) + $bmac * 16
    
    $broadcast = [System.Net.IPAddress]::Broadcast

    $UdpClient = New-Object System.Net.Sockets.UdpClient
    foreach ($port in $Ports) {
     $UdpClient.Connect($broadcast, $port)
     Write-Host $("Отправляем пакет на MAC-адрес $Mac порт $port") 
        $UdpClient.Send($packet, $packet.Length) | Out-Null
    }
    $UdpClient.Close()
}
Send-Packet -Mac 'E4:D5:3D:97:D1:8C'

Запустим какой-нибудь сниффер. Я использовал SmartSniff от NirSoft.
Начнем захват пакетов, после чего выполним нашу функцию.

Остановим захват пакетов сниффером и проверим содержание каждого свежего пакета.

То, что доктор прописал. Остается прочитать содержимое файла mac.csv и отправить по конвейеру MAC-адреса в функцию отправки "волшебного пакета".
# функция формирования и отправки "волшебного пакета"
# получает MAC-адрес (обязательный параметр) и порты удаленного компьютера 
# (по умолчанию - 0, 7, 9)
function Send-Packet {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$True)]
        [string[]]$Mac,
        [int32[]]$Ports = @(0,7,9)
    )    
    $bmac = $Mac.split(':') | ForEach-Object { [byte]('0x' + $_) }
    $packet = [byte[]](,0xFF * 6) + $bmac * 16
    
    $broadcast = [System.Net.IPAddress]::Broadcast

    $UdpClient = New-Object System.Net.Sockets.UdpClient
    foreach ($port in $Ports) {
     $UdpClient.Connect($broadcast, $port)
     Write-Host $("Отправляем пакет на MAC-адрес $Mac порт $port") 
        $UdpClient.Send($packet, $packet.Length) | Out-Null
    }
    $UdpClient.Close()
}

$csvPath = "$env:USERPROFILE/mac.csv"
if (Test-Path $csvPath) {
    Import-Csv -Path $csvPath | ForEach-Object {Send-Packet -Mac $_.MACAddress}
}


Решение готово. В реальных условиях не пробовал пока, поэтому если что не так - сообщите.