Wake-on-Lan (WoL) — технология, которая используется (как и следует из названия) для включения компьютера посредством отправки специального пакета на адрес требуемого хоста. Но что если при помощи WoL хочется ещё и выключать компьютер?

По сути весь дальнейший текст — это ещё один способ превратить буханку хлеба в троллейбус. Но если очень хочется, то почему нет?

В чём суть?

Я активно использую WoL для включения домашнего компьютера через смартфон и практически не использую физическую кнопку на корпусе ПК. Приложение для отправки WoL-пакета, которое я использую — это первое приложение, которое вылезло в Play Market'е, когда я вообще решил реализовывать эту затею. И что мне особенно в нём нравится, так это то, что у этого приложения ровно одна функция - отправить пакет на ПК. Всё.

После минутной настройки компьютер включается по нажатию одной кнопки в приложении из любой точки локальной сети. А если внести пару изменений в конфиг роутера, то и из любой точки мира, где есть интернет.

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

Так как из вышеперечисленного меня интересовали только включение и выключение, а менять приложение, к которому я привык и тем более ставить агенты на ПК мне очень не хотелось, я подумал, а нельзя ли использовать функционал WoL для выключения?

Поиск решения

В общем, после ещё некоторого гугления выяснилось, что технология WoL подходит только для включения и на этом его полномочия всё. А менять приложение и что-то устанавливать, как уже говорил, ну совсем не хотелось и тут мелькнула простая, как топор, мысль: "А почему бы не заставить компьютер считать, что в случае, если приходит WoL-пакет, то это знак, что ему пора бы выключиться?".

Для реализации подобных вещей идеально подходит Python, так что открываем IDE и начинаем менять смысл WoL на прямо противоположный.

Проектируем

Для реализации задуманного сначала нужно разобраться, что такое WoL-пакет и из чего он сделан.

WoL-пакет или по-другому magic packet отправляется посредством UDP чаще всего на 7 или 9 порт, «весит» 102 байта и состоит из следующих частей:

  1. Символ 0xFF, повторяющийся 6 раз. Итого 6 байт;

  2. MAC-адрес целевого устройства, повторяющийся 16 раз. Размер MAC-адреса 6 байт, итого 96 байт.

Теперь, зная это, план следующий:

  1. Определить сетевой интерфейс, на который будем отправлять WoL-пакеты и выбрать номер порта. Интерфейс желательно выбрать, т.к. на ПК может быть несколько сетевых интерфейсов, каждый со своим адресом;

  2. Получить IP и MAC-адреса выбранного интерфейса;

  3. Самостоятельно вычислить правильный WoL-пакет для выбранного интерфейса;

  4. Запустить прослушивание UDP-порта, куда собираемся отправлять WoL-пакеты;

  5. Декодировать полученные данные и затем сравнить их с тем, что получилось в п. 3 и если данные совпадают, то запустить команду завершения работы;

  6. 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)


  1. LeshaRB
    24.05.2024 08:06
    +2

    Я все просил Яндекс... Добавить такой функционал в свою колонку

    В итоге купли плату , для включения и выключения (((


    1. TimurRyabinin
      24.05.2024 08:06

      Здравствуйте! Я из Яндекса. Мы всегда прислушиваемся к пожеланиям наших пользователей. Постараемся удивить вас в будущем!


      1. VasilievVictor
        24.05.2024 08:06
        +1

        Давным-давно писал о том же и даже ответ был похож, но ничего не сделано. Подождал и на коленке набросал приложение для ios и mac, кому нужно, можно бесплатно скачать в аппсторе. При желании можно сделать включение голосом через Сири, любое устройство, хбокс, телевизор, компьютер(естественно при такой возможности на устройстве)

        П.с. Минусы не мои


        1. express
          24.05.2024 08:06

          Года 2 назад писал в поддержку, чтобы в яндекс.браузере можно было автоматически вводить сохраненные пароли на самоподписанных хттпс-сайтах и вот, вуаля - месяц назад наконец стало можно. Яндекс для терпеливых. Но окошко с кнопкой "сделать исключение для этого сайта" вылезает после каждого перезапуска браузера :(


          1. TimurRyabinin
            24.05.2024 08:06
            +1

            Здравствуйте! Наши разработчики действительно стараются реализовать самые полезные и популярные предложения. Однако любые изменения мы стараемся вводить постепенно, так как для их реализации требуются значительные ресурсы.

            Уточните, пожалуйста, добавлен ли самоподписанный сертификат в «Доверенные корневые центры сертификации»? Если нет, то вам понадобится:

            1. Перейти к самоподписанному сертификату через замок в адресной строке → Подробнее → Данные сертификата.

            2. Выбрать «Копировать в файл».

            3. Оставить опцию по умолчанию.

            4. Выбрать директорию и сохранить сертификат → открыть его в этой директории двойным нажатием клавиши → в окне выбрать «Установить сертификат».

            5. Выбрать тип установки.

            6. Выбрать «Доверенные корневые центры сертификации».

            7. Завершить установку.

            В некоторых случаях понадобится также прописать DNS-имя сайта в поле SAN (дополнительное имя субъекта) сертификата.


      1. PriFak
        24.05.2024 08:06
        +1

        в голос


    1. rufus20145
      24.05.2024 08:06

      Немного костыльно, но это вполне реализуемо и в текущей Алисе. Тут хорошо описан процесс. Даём доступ к порту ПК, куда будет приходить WoL пакет, из интернета; выбираем любой онлайн сервис, который умеет посылать WoL пакеты после GET запроса с параметрами, а не каждый раз вводить данные в форму, проверяем его (у меня пакет дошёл только от 1 из 3 сервисов); создаём виртуальное устройство у Домовёнка Кузи, создаем ему триггер на включение из Алисы, куда вставляем ссылку с предыдущего шага; связываем Кузю с Алисой; profit.


      1. vomidug
        24.05.2024 08:06

        Я когда-то получил тот же результат, только через малинку, которая с реверс-туннелем подключалась к впс, куда был натравлен Кузя. "Алиса, включи/выключи компьютер" -> Алиса дёргает Кузю, Кузя дёргает эндпоинт на впс, который летит на малинку, а малинка уже либо шлёт вол-запрос, либо прям ссшится на домашний комп и делает там systemctl suspend.


  1. pashkovka
    24.05.2024 08:06

    Любоптыно было бы посмотреть на само приложение, как через него реализовано выключение.


    1. HydrogenOxyde Автор
      24.05.2024 08:06
      +4

      В тексте вроде всё описано. Запускаете вышеприведённый код на ПК, скачиваете любое приложение, которое умеет отправлять WoL-пакеты, отправляете пакет на адрес ПК с запущенным кодом и если всё сделано верно, то будет инициирована команда завершения работы


  1. SergeyMax
    24.05.2024 08:06
    +23

    ставить агенты на ПК мне очень не хотелось

    добавить данный скрипт в автозагрузку

    Интрига.


    1. HydrogenOxyde Автор
      24.05.2024 08:06
      +2

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

      Плюс может кому-нибудь такое решение пригодится


      1. SergeyMax
        24.05.2024 08:06
        +3

        сторонние агенты с кучей функций

        а вот питон, что с ним?)


        1. HydrogenOxyde Автор
          24.05.2024 08:06
          +3

          А питон у меня уже был установленный, я им время от времени пользуюсь. Да и он всё-таки много у кого стоит (из-за похожих причин), в отличие от различных агентов


          1. SergeyMax
            24.05.2024 08:06
            +4

            Можно было сначала поставить агента, а потом уже сказать себе, что он уже был установленный. Ну и время от времени им пользоваться.


    1. 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


  1. ABy
    24.05.2024 08:06

    А нельзя такое поведение через планировщик задач добавить в windows?


    1. HydrogenOxyde Автор
      24.05.2024 08:06

      У меня так и настроено. Создан powershell-файл, в котором прописана активация venv и запуск python-скрипта. Сам запуск powershell-файла настроен в планировщике при запуске ПК.


      1. ABy
        24.05.2024 08:06
        +1

        Я имею в виду вообще без скриптов. Добавить в планировщик событие - получение wol пакета и действие - завершение работы. Или так не получится?


        1. HydrogenOxyde Автор
          24.05.2024 08:06
          +3

          Предполагаю, что нет. Всё-таки должна же быть какая-то программа, которая будет слушать определённый порт, на определённом интерфейсе и подтверждать, что пришедший пакет - это WoL-пакет. И так как WoL по своей сути предназначен именно для включения, то не думаю, что в Windows вообще есть такие встроенные обработчики, т.к. по идее это задача мат. платы


          1. dartraiden
            24.05.2024 08:06

            Как вариант, можно включить встроенный SSH-сервер, а на телефоне использовать что-то типа Termius, который позволяет сделать скрипт "выполнить такие-то команды при заходе на этот сервер" (тушить машину).

            https://support.termius.com/hc/en-us/articles/4402057170457-Snippets#runonstart

            Либо можно из планировщика задач попробовать ловить событие в журнале OpenSSH.

            Это если задаться целью по-максимуму обойтись тем, что есть в Windows "из коробки".


        1. dartraiden
          24.05.2024 08:06
          +2

          Планировщик может ловить определенные события в журналах, но, вроде бы, там не фиксируется приход WOL-пакета.


          1. HydrogenOxyde Автор
            24.05.2024 08:06

            Я о том же


  1. wildlayer
    24.05.2024 08:06
    +5

    https://github.com/SR-G/sleep-on-lan

    Было аналогичное желание, решение с гитхаба работает без нареканий


    1. HydrogenOxyde Автор
      24.05.2024 08:06
      +1

      Когда искал решение, то на этот репозиторий не натыкался


  1. 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:"


    1. HydrogenOxyde Автор
      24.05.2024 08:06

      Вы правы, можно было бы и так. В тексте так и написано

      Конечно, вышеприведённый код не идеален. Его можно написать лучше


      1. redfox0
        24.05.2024 08:06

        И у вас никогда не бросится исключение (строка с raise). Точнее бросится, но не то, которое вы ожидаете: TypeError: exceptions must derive from BaseException.


  1. Barnaby
    24.05.2024 08:06

    Можно дергать кнопку еспшкой и никакие агенты и софт не нужный, хоть через браузер открывай.


  1. multisu
    24.05.2024 08:06

    Но wol же не включает, а только из спящего режима выводит. У меня не получалось включать выключенный ПК, только из stand-by.


    1. HydrogenOxyde Автор
      24.05.2024 08:06
      +1

      Я нажимаю "Завершение работы", ПК выключается, никакие состояния не сохраняются, включение идёт с логотипа мат. платы. Я так понимаю это не спящий режим. Хотя возможно я чего-то не знаю.

      И из такого состояния без проблем включается через WoL. Если у вас включить не получается после завершения работы, то попробуйте полазить по настройкам адаптера и биоса


      1. LF69ssop
        24.05.2024 08:06

        Я правильно понимаю что при попытке включить уже включенный он у вас выключится?


        1. HydrogenOxyde Автор
          24.05.2024 08:06
          +1

          Да (если вы имеете ввиду скрипт из статьи, а не просто стандартный WoL)


        1. nidalee
          24.05.2024 08:06
          +1

          В этом и смысл, как отправить wake on lan для включения переизобретать не нужно.


          1. HydrogenOxyde Автор
            24.05.2024 08:06
            +1

            Про это никто и не говорит


    1. Kurochkin
      24.05.2024 08:06

      Зависит от железа (настроек BIOS/UEFI в том числе). Вообще, WoL может быть полной заменой кнопки питания, т.е. именно включать компьютер; хотя это может быть неочевидно из названия "wake on LAN"


    1. ajijiadduh
      24.05.2024 08:06
      +1

      wol включает


    1. nav68
      24.05.2024 08:06

      Интел и АМД по разному эту фичу обрабатывают вроде бы.


  1. GDragon
    24.05.2024 08:06
    +1

    А чем не устроил

    shutdown /s /m \\имя_компьютера


    1. HydrogenOxyde Автор
      24.05.2024 08:06

      В таком случае необходимо будет иметь разные приложения для включения и выключения


      1. nav68
        24.05.2024 08:06

        Термукс и два скрипта.


        1. HydrogenOxyde Автор
          24.05.2024 08:06

          Термуксом не пользовался, но если он умеет отправлять пакеты по UDP, тогда да, тоже можно реализовать похожее решение. Если только TCP, тогда WoL работать не будет.


  1. Andreyul
    24.05.2024 08:06
    +3

    Если не нравится, что одна и та же кнопка отправляющего приложения и включает, и выключает компьютер, то в коде меняем символы MAC адреса в обратном порядке и в отправляющее приложение забиваем ещё один тот же MAC, но в обратном порядке. Это работает, т.к. пакет broadcast-овый.