Wake-on-Lan (WoL) — технология, которая используется (как и следует из названия) для включения компьютера посредством отправки специального пакета на адрес требуемого хоста. Но что если при помощи WoL хочется ещё и выключать компьютер?
По сути весь дальнейший текст — это ещё один способ превратить буханку хлеба в троллейбус. Но если очень хочется, то почему нет?
В чём суть?
Я активно использую WoL для включения домашнего компьютера через смартфон и практически не использую физическую кнопку на корпусе ПК. Приложение для отправки WoL-пакета, которое я использую — это первое приложение, которое вылезло в Play Market'е, когда я вообще решил реализовывать эту затею. И что мне особенно в нём нравится, так это то, что у этого приложения ровно одна функция - отправить пакет на ПК. Всё.
После минутной настройки компьютер включается по нажатию одной кнопки в приложении из любой точки локальной сети. А если внести пару изменений в конфиг роутера, то и из любой точки мира, где есть интернет.
Но через какое-то время появилась потребность так же легко по нажатию одной кнопки выключать компьютер. Беглый гуглёж в русскоязычном и англоязычном интернетах подсказал, что для выключения можно поставить на компьютер какой-нибудь агент (вариантов много) и клиентское приложение на телефон и далее можно будет хоть включать ПК, хоть выключать, хоть получать доступ к файлам и много всего остального.
Так как из вышеперечисленного меня интересовали только включение и выключение, а менять приложение, к которому я привык и тем более ставить агенты на ПК мне очень не хотелось, я подумал, а нельзя ли использовать функционал WoL для выключения?
Поиск решения
В общем, после ещё некоторого гугления выяснилось, что технология WoL подходит только для включения и на этом его полномочия всё. А менять приложение и что-то устанавливать, как уже говорил, ну совсем не хотелось и тут мелькнула простая, как топор, мысль: "А почему бы не заставить компьютер считать, что в случае, если приходит WoL-пакет, то это знак, что ему пора бы выключиться?".
Для реализации подобных вещей идеально подходит Python, так что открываем IDE и начинаем менять смысл WoL на прямо противоположный.
Проектируем
Для реализации задуманного сначала нужно разобраться, что такое WoL-пакет и из чего он сделан.
WoL-пакет или по-другому magic packet отправляется посредством UDP чаще всего на 7 или 9 порт, «весит» 102 байта и состоит из следующих частей:
Символ 0xFF, повторяющийся 6 раз. Итого 6 байт;
MAC-адрес целевого устройства, повторяющийся 16 раз. Размер MAC-адреса 6 байт, итого 96 байт.
Теперь, зная это, план следующий:
Определить сетевой интерфейс, на который будем отправлять WoL-пакеты и выбрать номер порта. Интерфейс желательно выбрать, т.к. на ПК может быть несколько сетевых интерфейсов, каждый со своим адресом;
Получить IP и MAC-адреса выбранного интерфейса;
Самостоятельно вычислить правильный WoL-пакет для выбранного интерфейса;
Запустить прослушивание UDP-порта, куда собираемся отправлять WoL-пакеты;
Декодировать полученные данные и затем сравнить их с тем, что получилось в п. 3 и если данные совпадают, то запустить команду завершения работы;
Profit!
Реализация
Приступаем к реализации.
С первым пунктом всё просто. Название интерфейса можно посмотреть где-нибудь в настройках операционной системы. В моём случае — это Ethernet 4. Номер порта для WoL-пакетов, как уже говорилось выше, чаще всего 7 и 9. Я буду использовать 9.
WOL_PORT = 9
INTERFACE_NAME = 'Ethernet 4'
Пункт второй. Для получения IP и MAC-адресов понадобится библиотека psutil (больше никаких зависимостей устанавливать не будем).
pip install psutil
Затем пишем код для получения адресов:
import psutil
def get_ip_mac_address(interface_name: str) -> tuple:
ip_addr = mac_addr = None
for item in psutil.net_if_addrs()[interface_name]:
addr = item.address
# В IPv4-адресах разделители - точки
if '.' in addr:
ip_addr = addr
# В MAC-адресах разделители либо тире, либо одинарное двоеточие.
# Двойное двоеточие - это разделители для адресов IPv6
elif ('-' in addr or ':' in addr) and '::' not in addr:
# Приводим MAC-адрес к одному формату. Формат может меняться в зависимости от ОС
mac_addr = addr.replace(':', '-').upper()
if not ip_addr or not mac_addr or ip_addr == '127.0.0.1':
raise 'Не удалось получить IP или MAC-адрес сетевого интерфейса'
return ip_addr, mac_addr
Мелкие пояснения по коду есть в самом коде, но некоторого описания требует строка 6.
Функция psutil.net_if_addrs()[interface_name]
возвращает список именованных кортежей c различными параметрами для каждого адреса сетевого интерфейса. Порядок этих кортежей разный для разных ОС (я тестировал на Windows 10 и Ubuntu 20.04), соответственно на выборку по индексам рассчитывать не стоит, поэтому запускаем цикл по адресам интерфейса и парой условных операторов идентифицируем кто есть кто.
(Уже постфактум я подумал, что можно было для этого использовать значение family, но переписывать было лень).
Со вторым пунктом разобрались, теперь пункт три. Тут всё просто, почти как в первом.
Используем полученный MAC-адрес и собираем магический пакет:
def assemble_wol_packet(mac_address: str) -> str:
return f'{"FF-" * 6}{(mac_address + "-") * 16}'
Четвёртый пункт не сильно сложнее остальных.
Запускаем процесс прослушивания указанного порта с полученным IP-адресом интерфейса.
На строке 4 используется функция из пункта 2, а на строке 9 — функция из пункта 3
import socket
def run_udp_port_listener(port: int, interface_name: str):
ip_addr, mac_addr = get_ip_mac_address(interface_name)
server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_socket.bind((ip_addr, port))
assembled_wol_packet = assemble_wol_packet(mac_addr)
while True:
data, _ = server_socket.recvfrom(1024)
И завершающий пункт - как обычно, ничего сложного.
Для начала декодируем данные:
decoded_packet_data = '-'.join(f'{byte:02x}' for byte in raw_bytes).upper() + '-'
Немного пояснений: полученные данные — это набор байт, так что сначала приводим данные к шестнадцатеричному виду при помощи команды byte:02x
. Далее то, что получилось разделяем через знак "-" и в конце приводим к верхнему регистру.
По идее без знака тире, что здесь, что в третьем пункте можно обойтись, но с ним проще проводить отладку, если вдруг что-то идёт не так.
Затем осталось сравнить данные и запустить команду выключения ПК. Для удобства лучше обернуть декодирование и сравнение в одну функцию:
def check_is_wol_packet(raw_bytes: bytes, assembled_wol_packet: str) -> int:
decoded_packet_data = '-'.join(f'{byte:02x}' for byte in raw_bytes).upper() + '-'
if decoded_packet_data == assembled_wol_packet:
return 1
return 0
В случае совпадения данных инициируем shutdown:
import os
# ... Здесь код запуска прослушивания порта ...
while True:
data, _ = server_socket.recvfrom(1024)
is_wol_packet = check_is_wol_packet(data, assembled_wol_packet)
if is_wol_packet == 1:
if os.name == 'posix':
os.system('sudo shutdown -h now')
elif os.name == 'nt':
os.system('shutdown -s -t 0 -f')
И теперь соберём весь код вместе
import socket
import os
import logging
import psutil
WOL_PORT = 9
INTERFACE_NAME = 'Ethernet 4'
logging.basicConfig(format='%(levelname)s: %(asctime)s %(message)s', level=logging.INFO)
logger = logging.getLogger(__name__)
def get_ip_mac_address(interface_name: str) -> tuple:
ip_addr = mac_addr = None
for item in psutil.net_if_addrs()[interface_name]:
addr = item.address
# В IPv4-адресах разделители - точки
if '.' in addr:
ip_addr = addr
# В MAC-адресах разделители либо тире, либо одинарное двоеточие.
# Двойное двоеточие - это разделители для адресов IPv6
elif ('-' in addr or ':' in addr) and '::' not in addr:
# Приводим MAC-адрес к одному формату. Формат может меняться в зависимости от ОС
mac_addr = addr.replace(':', '-').upper()
if not ip_addr or not mac_addr or ip_addr == '127.0.0.1':
raise 'Не удалось получить IP или MAC-адрес сетевого интерфейса'
return ip_addr, mac_addr
def assemble_wol_packet(mac_address: str) -> str:
return f'{"FF-" * 6}{(mac_address + "-") * 16}'
def check_is_wol_packet(raw_bytes: bytes, assembled_wol_packet: str) -> int:
decoded_packet_data = '-'.join(f'{byte:02x}' for byte in raw_bytes).upper() + '-'
if decoded_packet_data == assembled_wol_packet:
return 1
return 0
def run_udp_port_listener(port: int, interface_name: str):
ip_addr, mac_addr = get_ip_mac_address(interface_name)
server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_socket.bind((ip_addr, port))
logger.info(f'Listening on {ip_addr}:{port}')
assembled_wol_packet = assemble_wol_packet(mac_addr)
while True:
data, _ = server_socket.recvfrom(1024)
is_wol_packet = check_is_wol_packet(data, assembled_wol_packet)
if is_wol_packet == 1:
if os.name == 'posix':
os.system('sudo shutdown -h now')
elif os.name == 'nt':
os.system('shutdown -s -t 0 -f')
run_udp_port_listener(WOL_PORT, INTERFACE_NAME)
Теперь если всё сделано верно, то при отправке WoL-пакета на IP-адрес ПК он выключится. Если при этом немного поколдовать с роутером, то это получится делать и через интернет.
Далее можно добавить данный скрипт в автозагрузку или настроить службу, инструкций для этого в интернете хватает для любой ОС, так что про реализацию добавить больше нечего.
Итого
Конечно, вышеприведённый код не идеален. Его можно написать лучше, добавить различные проверки для безопасности и сделать много других вещей, но лично для меня это уже ненужное усложнение. То что было нужно мне я реализовал и думаю, что на этом можно и закончить.
Комментарии (43)
pashkovka
24.05.2024 08:06Любоптыно было бы посмотреть на само приложение, как через него реализовано выключение.
HydrogenOxyde Автор
24.05.2024 08:06+4В тексте вроде всё описано. Запускаете вышеприведённый код на ПК, скачиваете любое приложение, которое умеет отправлять WoL-пакеты, отправляете пакет на адрес ПК с запущенным кодом и если всё сделано верно, то будет инициирована команда завершения работы
SergeyMax
24.05.2024 08:06+23ставить агенты на ПК мне очень не хотелось
добавить данный скрипт в автозагрузку
Интрига.
HydrogenOxyde Автор
24.05.2024 08:06+2Не хотелось ставить сторонние агенты с кучей функций, которые мне не нужны. Хотя так, конечно, было бы быстрее. Но захотелось сделать по-своему (тег про троллейбус из хлеба не просто так указан).
Плюс может кому-нибудь такое решение пригодится
SergeyMax
24.05.2024 08:06+3сторонние агенты с кучей функций
а вот питон, что с ним?)
HydrogenOxyde Автор
24.05.2024 08:06+3А питон у меня уже был установленный, я им время от времени пользуюсь. Да и он всё-таки много у кого стоит (из-за похожих причин), в отличие от различных агентов
SergeyMax
24.05.2024 08:06+4Можно было сначала поставить агента, а потом уже сказать себе, что он уже был установленный. Ну и время от времени им пользоваться.
avacha
24.05.2024 08:06@ABy, @HydrogenOxyde
PoC. Powershell one-liner, без стороннего ПО в системе:
powershell -NoProfile -Command $addr=New-Object System.Net.IPEndPoint([IPAddress]::Any,9);$udp=New-Object System.Net.Sockets.UdpClient(9);while($true){$content=$udp.Receive([ref]$addr);if(($content[0]-eq$content[1]-eq$content[2]-eq$content[3]-eq$content[4]-eq$content[5]-eq0xFF)-and($content.Length-eq102)){Write-Host Got WOL packet!;shutdown /s /t 3600;break;}else{Write-Host Got non-WOL packet!;}}
Или так:
powershell -NoProfile -Command (Add-Type -TypeDefinition 'using System;using System.Net;using System.Net.Sockets;using System.Diagnostics;public static class Program{public static void Main(){var addr=new IPEndPoint(IPAddress.Any,9);var udp=new UdpClient(9);while(true){var content=udp.Receive(ref addr);if ((content[0]==0xFF^&^&content[1]==0xFF^&^&content[2]==0xFF^&^&content[3]==0xFF^&^&content[4]==0xFF^&^&content[5]==0xFF)^&^&(content.Length==102)){Console.WriteLine("""Got WOL packet!""");Process.Start("""cmd.exe""","""/c shutdown /s /t 3600""");break;}else{Console.WriteLine("""Got non-WOL packet!""");}}}}' -PassThru)::Main();
Написан за десять минут 16-летним сыном. Можно просто указать команду в планировщике.
Оно же, более читаемо, в виде bat-файлов:
Вариант 1
@echo off powershell -NoProfile -Command ^ $addr = New-Object System.Net.IPEndPoint([IPAddress]::Any,9); ^ $udp = New-Object System.Net.Sockets.UdpClient(9); ^ while ($true) { ^ $content = $udp.Receive([ref]$addr); ^ if (($content[0] -eq $content[1] -eq $content[2] -eq $content[3] -eq $content[4] -eq $content[5] -eq 0xFF) -and ($content.Length -eq 102)) { ^ Write-Host Got WOL packet!; ^ shutdown /s /t 3600; ^ break; ^ } else { ^ Write-Host Got non-WOL packet!; ^ } ^ } pause
Вариант 2
@echo off powershell -NoProfile -Command ^ (Add-Type -TypeDefinition ' ^ using System; ^ using System.Net; ^ using System.Net.Sockets; ^ using System.Diagnostics; ^ public static class Program { ^ public static void Main() { ^ var addr = new IPEndPoint(IPAddress.Any, 9); ^ var udp = new UdpClient(9); ^ while(true) { ^ var content = udp.Receive(ref addr); ^ if ((content[0]==0xFF^&^&content[1]==0xFF^&^&content[2]==0xFF^&^&content[3]==0xFF^&^&content[4]==0xFF^&^&content[5]==0xFF)^&^&(content.Length==102)) {^ Console.WriteLine("""Got WOL packet!"""); ^ Process.Start("""cmd.exe""","""/c shutdown /s /t 3600"""); ^ break; ^ } else { ^ Console.WriteLine("""Got non-WOL packet!"""); ^ } ^ } ^ } ^ }' -PassThru)::Main(); pause
ABy
24.05.2024 08:06А нельзя такое поведение через планировщик задач добавить в windows?
HydrogenOxyde Автор
24.05.2024 08:06У меня так и настроено. Создан powershell-файл, в котором прописана активация venv и запуск python-скрипта. Сам запуск powershell-файла настроен в планировщике при запуске ПК.
ABy
24.05.2024 08:06+1Я имею в виду вообще без скриптов. Добавить в планировщик событие - получение wol пакета и действие - завершение работы. Или так не получится?
HydrogenOxyde Автор
24.05.2024 08:06+3Предполагаю, что нет. Всё-таки должна же быть какая-то программа, которая будет слушать определённый порт, на определённом интерфейсе и подтверждать, что пришедший пакет - это WoL-пакет. И так как WoL по своей сути предназначен именно для включения, то не думаю, что в Windows вообще есть такие встроенные обработчики, т.к. по идее это задача мат. платы
dartraiden
24.05.2024 08:06Как вариант, можно включить встроенный SSH-сервер, а на телефоне использовать что-то типа Termius, который позволяет сделать скрипт "выполнить такие-то команды при заходе на этот сервер" (тушить машину).
https://support.termius.com/hc/en-us/articles/4402057170457-Snippets#runonstart
Либо можно из планировщика задач попробовать ловить событие в журнале OpenSSH.
Это если задаться целью по-максимуму обойтись тем, что есть в Windows "из коробки".
dartraiden
24.05.2024 08:06+2Планировщик может ловить определенные события в журналах, но, вроде бы, там не фиксируется приход WOL-пакета.
wildlayer
24.05.2024 08:06+5https://github.com/SR-G/sleep-on-lan
Было аналогичное желание, решение с гитхаба работает без нареканий
alexyr
24.05.2024 08:06+6Слегка режет глаз "return 1" и "if is_wol_packet == 1:"
Почему не вернуть просто True/False и сделать "if is_wol_packet:"?
Это же питон, не C...
Да и с единицей тоже будет работать "if is_wol_packet:"HydrogenOxyde Автор
24.05.2024 08:06Вы правы, можно было бы и так. В тексте так и написано
Конечно, вышеприведённый код не идеален. Его можно написать лучше
redfox0
24.05.2024 08:06И у вас никогда не бросится исключение (строка с raise). Точнее бросится, но не то, которое вы ожидаете:
TypeError: exceptions must derive from BaseException
.
Barnaby
24.05.2024 08:06Можно дергать кнопку еспшкой и никакие агенты и софт не нужный, хоть через браузер открывай.
multisu
24.05.2024 08:06Но wol же не включает, а только из спящего режима выводит. У меня не получалось включать выключенный ПК, только из stand-by.
HydrogenOxyde Автор
24.05.2024 08:06+1Я нажимаю "Завершение работы", ПК выключается, никакие состояния не сохраняются, включение идёт с логотипа мат. платы. Я так понимаю это не спящий режим. Хотя возможно я чего-то не знаю.
И из такого состояния без проблем включается через WoL. Если у вас включить не получается после завершения работы, то попробуйте полазить по настройкам адаптера и биоса
LF69ssop
24.05.2024 08:06Я правильно понимаю что при попытке включить уже включенный он у вас выключится?
HydrogenOxyde Автор
24.05.2024 08:06+1Да (если вы имеете ввиду скрипт из статьи, а не просто стандартный WoL)
nidalee
24.05.2024 08:06+1В этом и смысл, как отправить wake on lan для включения переизобретать не нужно.
Kurochkin
24.05.2024 08:06Зависит от железа (настроек BIOS/UEFI в том числе). Вообще, WoL может быть полной заменой кнопки питания, т.е. именно включать компьютер; хотя это может быть неочевидно из названия "wake on LAN"
GDragon
24.05.2024 08:06+1А чем не устроил
shutdown /s /m \\имя_компьютера
HydrogenOxyde Автор
24.05.2024 08:06В таком случае необходимо будет иметь разные приложения для включения и выключения
nav68
24.05.2024 08:06Термукс и два скрипта.
HydrogenOxyde Автор
24.05.2024 08:06Термуксом не пользовался, но если он умеет отправлять пакеты по UDP, тогда да, тоже можно реализовать похожее решение. Если только TCP, тогда WoL работать не будет.
Andreyul
24.05.2024 08:06+3Если не нравится, что одна и та же кнопка отправляющего приложения и включает, и выключает компьютер, то в коде меняем символы MAC адреса в обратном порядке и в отправляющее приложение забиваем ещё один тот же MAC, но в обратном порядке. Это работает, т.к. пакет broadcast-овый.
LeshaRB
Я все просил Яндекс... Добавить такой функционал в свою колонку
В итоге купли плату , для включения и выключения (((
TimurRyabinin
Здравствуйте! Я из Яндекса. Мы всегда прислушиваемся к пожеланиям наших пользователей. Постараемся удивить вас в будущем!
VasilievVictor
Давным-давно писал о том же и даже ответ был похож, но ничего не сделано. Подождал и на коленке набросал приложение для ios и mac, кому нужно, можно бесплатно скачать в аппсторе. При желании можно сделать включение голосом через Сири, любое устройство, хбокс, телевизор, компьютер(естественно при такой возможности на устройстве)
П.с. Минусы не мои
express
Года 2 назад писал в поддержку, чтобы в яндекс.браузере можно было автоматически вводить сохраненные пароли на самоподписанных хттпс-сайтах и вот, вуаля - месяц назад наконец стало можно. Яндекс для терпеливых. Но окошко с кнопкой "сделать исключение для этого сайта" вылезает после каждого перезапуска браузера :(
TimurRyabinin
Здравствуйте! Наши разработчики действительно стараются реализовать самые полезные и популярные предложения. Однако любые изменения мы стараемся вводить постепенно, так как для их реализации требуются значительные ресурсы.
Уточните, пожалуйста, добавлен ли самоподписанный сертификат в «Доверенные корневые центры сертификации»? Если нет, то вам понадобится:
Перейти к самоподписанному сертификату через замок в адресной строке → Подробнее → Данные сертификата.
Выбрать «Копировать в файл».
Оставить опцию по умолчанию.
Выбрать директорию и сохранить сертификат → открыть его в этой директории двойным нажатием клавиши → в окне выбрать «Установить сертификат».
Выбрать тип установки.
Выбрать «Доверенные корневые центры сертификации».
Завершить установку.
В некоторых случаях понадобится также прописать DNS-имя сайта в поле SAN (дополнительное имя субъекта) сертификата.
PriFak
в голос
rufus20145
Немного костыльно, но это вполне реализуемо и в текущей Алисе. Тут хорошо описан процесс. Даём доступ к порту ПК, куда будет приходить WoL пакет, из интернета; выбираем любой онлайн сервис, который умеет посылать WoL пакеты после GET запроса с параметрами, а не каждый раз вводить данные в форму, проверяем его (у меня пакет дошёл только от 1 из 3 сервисов); создаём виртуальное устройство у Домовёнка Кузи, создаем ему триггер на включение из Алисы, куда вставляем ссылку с предыдущего шага; связываем Кузю с Алисой; profit.
vomidug
Я когда-то получил тот же результат, только через малинку, которая с реверс-туннелем подключалась к впс, куда был натравлен Кузя. "Алиса, включи/выключи компьютер" -> Алиса дёргает Кузю, Кузя дёргает эндпоинт на впс, который летит на малинку, а малинка уже либо шлёт вол-запрос, либо прям ссшится на домашний комп и делает там systemctl suspend.