[Перевод] Учим домашний сервер Linux засыпать при простое и просыпаться по запросу

Всё началось с, казалось бы, обыденного изменения в моём домашнем сервере для хостинга бэкапов Time Machine: я хотел, чтобы он уходил в сон, когда находился в состоянии простоя, и пробуждался при необходимости. Уход в сон при простое — кажется, в Windows эта функция встроена с Windows 98? Насколько сложно будет это настроить на современной версии Ubuntu?

Честно говоря, мне требовалось нечто большее, чем засыпание при простое, мне нужно было ещё и пробуждение по запросу; оказалось, вот это второе требование реализовать довольно сложно. Я много раз заходил в тупик, но продолжал искать решение, которое «просто работает» без необходимости ручного включения сервера для каждого бэкапа. Вы можете прочитать статью целиком, чтобы узнать о моём пути, или просто прочитать готовые инструкции.

vvaopiwp2kuzjfej5cg5s_aonve.png


Результат:


  • Сервер автоматически сохраняет состояние в ОЗУ при простое
  • Сервер автоматически пробуждается при необходимости всем остальным в сети, включая SSH, бэкапы Time Machine и так далее.


Вам понадобится:


  • Постоянно включённое устройство с Linux в той же сети, что и ваш сервер, например, Raspberry Pi
  • Устройство сетевого интерфейса для сервера, поддерживающее wake-on-LAN при помощи unicast-пакетов


На сервере:


  • Включить wake-on-LAN при помощи unicast-пакетов (не только при помощи magic-пакетов), сделать его постоянным
sudo ethtool -s eno1 wol ug
sudo tee /etc/networkd-dispatcher/configuring.d/wol << EOF
#!/usr/bin/env bash

ethtool -s eno1 wol ug || true
EOF
sudo chmod 755 /etc/networkd-dispatcher/configuring.d/wol


  • Настроить cron job для засыпания при простое (замените /home/ubuntu на нужное вам местоположение скрипта)
tee /home/ubuntu/auto-sleep.sh << EOF
#!/bin/bash
logged_in_count=$(who | wc -l)
# Мы ожидаем две строки вывода от `lsof -i:548` при простое: одна — для вывода заголовков, другая — для 
# сервера, слушающего подключения. Если строк больше двух, то это означает наличие входящих соединений.
afp_connection_count=$(lsof -i:548 | wc -l)
if [[ $logged_in_count < 1 && $afp_connection_count < 3 ]]; then
  systemctl suspend
else
  echo "Not suspending, logged in users: $logged_in_count, connection count: $afp_connection_count"
fi
EOF
chmod +x /home/ubuntu/auto-sleep.sh
sudo crontab -e
# В редакторе добавьте следующую строку:
*/10 * * * * /home/ubuntu/auto-sleep.sh | logger -t autosuspend


  • Отключить IPv6: при таком способе применяется ARP, который IPv6 не использует
sudo nano /etc/default/grub
# Найдите GRUB_CMDLINE_LINUX=""
# Измените на GRUB_CMDLINE_LINUX="ipv6.disable=1"
sudo update-grub
sudo reboot


  • Необязательно: сконфигурировать сетевые сервисы (например, Netatalk) так, чтобы они отключались перед сном, чтобы избежать нежелательных пробуждений при сетевой активности
sudo tee /etc/systemd/system/netatalk-sleep.service << EOF
[Unit]
Description=Netatalk sleep hook
Before=sleep.target
StopWhenUnneeded=yes

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=-/usr/bin/systemctl stop netatalk
ExecStop=-/usr/bin/systemctl start netatalk

[Install]
WantedBy=sleep.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable netatalk-sleep.service


В постоянно включённом устройстве:


  • Установить ARP Stand-in: сверхпростой скрипт на Ruby, выполняемый как системный сервис и отвечающий на ARP-запросы от лица другой машины. Сконфигурировать его так, чтобы он отвечал от лица спящего сервера.
  • Необязательно: сконфигурировать Avahi так, чтобы он объявлял о сетевых сервисах от лица сервера, когда тот спит.
sudo apt install avahi-daemon
sudo tee /etc/systemd/system/avahi-publish.service << EOF
[Unit]
Description=Publish custom Avahi records
After=network.target avahi-daemon.service
Requires=avahi-daemon.service

[Service]
ExecStart=/usr/bin/avahi-publish -s homeserver _afpovertcp._tcp 548 -H homeserver.local

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable avahi-publish.service --now
systemctl status avahi-publish.service


Тонкости


  • Сетевое устройство сервера должно поддерживать wake-on-LAN от unicast-пакетов
  • Чтобы предотвратить нежелательные пробуждения, нужно сделать так, чтобы никакое устройство в сети не отправляло серверу посторонних пакетов


Сначала я расскажу о своём оборудовании, потому что моё решение частично зависит от него:

  • HP ProDesk 600 G3 SFF
  • CPU: Intel Core i5–7500
  • Сетевой адаптер: Intel I219-LM


Засыпание при простое


Я начал решение задачи с засыпания при простое, которое сводилось к двум вопросам:

  • Как определить, находится ли сервер в простое или он занят в конкретный момент времени
  • Как автоматически выполнить сохранение состояния в ОЗУ после простоя в течение определённого времени


Большинство найденных мной руководств по функции сна при простое, например, это, предназначались для Ubuntu Desktop — похоже, эту функцию редко реализуют для Ubuntu Server. Я нашёл несколько многообещающих инструментов, самым примечательным из которых был circadian. Однако в целом оказалось, что не существует какого-то стандартного/рекомендованного способа включения этой функции, поэтому я решил развернуть её самостоятельно и максимально простым способом.

Определение состояния простоя/загруженности


Я задался вопросом, какие действия сервера можно считать загруженностью, и пришёл к двум пунктам:

  • Подключенные SSH-сессии
  • Выполняемые бэкапы Time Machine


Выбрать соответствующие метрики было довольно просто:

  • Подсчёт залогиненных пользователей при помощи who
  • Подсчёт количества подключений к порту AFP (548) при помощи lsof (я использую AFP для сетевого ресурса Time Machine)


Для обеих метрик я сначала записал значения в простое, а затем — когда сервер был загружен задачами.

Автоматическое сохранение состояния в ОЗУ


Чтобы не усложнять, я решил использовать cron job, запускающий bash-скрипт — см. показанную выше готовую версию. Пока он работает хорошо; если мне придётся учитывать другие метрики для выявления состояния простоя, то я подумаю над применением более функционального инструмента наподобие circadian.

Пробуждение по запросу


Разобравшись с засыпанием при простое, я начал выяснять, как будить сервер по запросу.

Можно ли настроить машину так, чтобы она автоматически просыпалась при получении сетевого запроса? Я знал, что Wake-on-LAN поддерживает пробуждение компьютера при помощи специально созданного «магического пакета», и создать такую систему очень легко. Вопрос заключался в том, может ли то же самое делать обычный, «немагический» пакет.

Пробуждение по PHY?


После онлайн-поисков я нашёл обсуждение на superuser, которое выглядело многообещающе. Там приводилась ссылка на man-страницу ethtool — Linux-утилиты, используемой для конфигурирования сетевого оборудования. На странице приведены все опции конфигурации wake-on-LAN ethtool:

wol p|u|m|b|a|g|s|f|d...
      Sets Wake-on-LAN options.  Not all devices support
      this.  The argument to this option is a string of
      characters specifying which options to enable.

      p   Wake on PHY activity
      u   Wake on unicast messages
      m   Wake on multicast messages
      b   Wake on broadcast messages
      a   Wake on ARP
      g   Wake on MagicPacket™
      s   Enable SecureOn™ password for MagicPacket™
      f   Wake on filter(s)
      d   Disable (wake on nothing).  This option
          clears all previous options.


В частности, в обсуждении упоминалась опция Wake on PHY activity, которая для моей ситуации казалась идеальной. Вроде бы любой пакет, отправленный на MAC-адрес сетевого интерфейса, должен был его пробуждать. Я включил флаг при помощи ethtool, вручную перевёл машину в режим сна, а затем попытался снова залогиниться при помощи SSH и отправлять пинги. Увы, несмотря на множество попыток, машина продолжала спать. Неудача.

Прорыв: пробуждение по unicast


Ни одна из других опций wake-on-LAN ethtool не показалась мне подходящей, однако после поисков я нашёл другой вариант для проверки — Wake on unicast messages. Я включил флаг при помощи ethtool, вручную перевёл машину в сон, а затем попытался залогиниться при помощи SSH. Бинго! На этот раз машина проснулась. Я понял, что задача решена.

Впрочем, не стоит торопиться, оставалось ещё две проблемы:

  1. Иногда сервер пробуждался без заметной мне сетевой активности
  2. Спустя какое-то время после перехода сервера в сон его оказывалось невозможно разбудить при помощи какой бы то ни было сетевой активности, за исключением магического пакета


При более внимательном изучении того же обсуждения на superuser выяснилась причина второй проблемы: вскоре после ухода в сон машина, по сути, исчезала из сети, потому что больше не отвечала на ARP-запросы.

ARP


То есть после того, как срок действия кэшированной записи ARP на других машинах сети истекал, они больше не могли резолвить IP-адрес сервера в его MAC-адрес. Иными словами, при попытках пинга моего сервера 192.168.1.2 не удавалось даже передать пакет на сервер, потому что MAC-адрес был неизвестен. Без отправки пакета сервер никак нельзя было заставить пробудиться.

Статический ARP?


Моей первой реакцией было следующее: будем вручную создавать записи кэша ARP на каждом сетевом клиенте. Это возможно в macOS при помощи следующей команды:

sudo arp -s [IP address] [MAC address]


Но это не соответствует моим требованиям, мне нужно было, чтобы всё «просто работало»: не хотелось создавать статические записи кэша ARP на каждой машине, которая будет получать доступ к серверу. Так что придётся искать другие варианты.

Перенос протокола ARP?


После дальнейших поисков я обнаружил нечто интересное: эту проблему в мире Windows уже очень давно решили.

Это называется ARP protocol offload (перенос протокола ARP):

  • Сетевое оборудование способно отвечать на ARP-запросы независимо от CPU
  • Перед уходом в сон ОС конфигурирует сетевое оборудование так, чтобы оно отвечало на ARP-запросы
  • Во сне сетевое оборудование самостоятельно отвечает на ARP-запросы, не пробуждая остальную часть машины для использования CPU


Вуаля, это именно то, что мне нужно. Я даже изучил даташит моего сетевого оборудования, на главной странице которого указана функция ARP Offload.

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

, только для того, чтобы выяснить, что перенос ARP не поддерживается драйвером для Linux. Я попытался пропатчить драйвер, чтобы добавить в него поддержку переноса ARP…, но потом напомнил себе, что патчинг кода драйвера для Linux — это гораздо больше, чем я надеялся достичь в таком хобби-проекте. (Хотя, возможно, когда-нибудь…)

Другие решения при помощи магических пакетов


Ещё немного поискав, я нашёл другие умные и сложные решения с задействованием магических пакетов. Основная идея заключалась в автоматизации отправки магических пакетов. Одно решение (wake-on-arp) слушает ARP-запросы к указанному хосту, чтобы запустить отправку на этот хост магического пакета. В другом решении реализован веб-интерфейс и интеграция с Home Assistant для отправки магического пакета из веб-браузера смартфона. Всё это впечатляет, но мне требовалось что-то более простое и не требующее ручного пробуждения сервера.

Я рассмотрел ещё несколько вариантов, но отказался от них, потому что они показались слишком сложными и нестабильными:

  • Создание скрипта для отправки магического пакета, а затем мгновенный запуск бэкапа Time Machine при помощи tmutil. Скрипт нужно будет устанавливать вручную, после чего запланировать его периодический запуск на каждом Mac.
  • Применение HAProxy для проксирования всего релевантного сетевого трафика через Raspberry Pi и использование хука для отправки магического пакета на сервер в случае активности.


Прорыв: ARP Stand-in


То, что я пытался реализовать, не сильно отличалось от сопоставления статического IP, которое стандартно конфигурируется на домашних роутерах, только оно выполнялось для DHCP, а не для ARP. Может ли мой роутер делать то же самое для ARP?

Подробнее изучив протокол ARP, я выяснил, что для резолвинга ARP даже не требуется, чтобы на запросы отвечал конкретный полномочный хост — на ARP-запросы может отвечать любое другое сетевое устройство. Иными словами, резолвить ARP-запросы необязательно должен мой сервер, это может быть кто угодно. То есть можно просто настроить любое устройство отвечать от лица спящего сервера?

Вот, что я пытался сделать:

cpkmomswjvjrnnbfv3cs03clnvu.png


Я подумал, что это можно реализовать как сетевую конфигурацию Linux, но самое близкое, что мне удалось обнаружить — это Proxy ARP, выполнявшую другую цель. Поэтому я опустился на один уровень ниже, к сетевому программированию.

Как же нам подойти к решению задачи прослушивания пакетов ARP-запросов? Очевидно, это можно сделать при помощи сырого сокета, но я также знал, что tcpdump и Wireshark могут использовать фильтры для перехвата пакетов только заданного типа. Поэтому я решил изучить libpcap — библиотеку, лежащую в основе обоих этих инструментов. Я узнал, что использование libpcap имеет явное преимущество перед сырым сокетом: libpcap реализует очень эффективную фильтрацию непосредственно в ядре, а сырой сокет потребовал бы ручной фильтрации пакетов в пользовательском пространстве, что менее эффективно.

Чтобы не усложнять, я решил написать решение на Ruby, что привело меня к pcaprub — Ruby-обвязке для libpcap. Далее мне достаточно было разобраться, какой фильтр использовать с libpcap. После исследований, проб и ошибок я получил следующий фильтр:

arp and arp[6:2] == 1 and arp[24:4] == [IP address converted to hex]


Например, при использовании целевого IP -адреса 192.168.1.2:

arp and arp[6:2] == 1 and arp[24:4] == 0xc0a80102


Давайте разберём эту строку при помощи определения структуры ARP-пакета для байтовых смещений и длин:

  • arp — ARP-пакеты
  • arp[6:2] == 1 — пакеты ARP-запросов. [6:2] означает »2 байта, найденные по байтовому смещению 6».
  • arp[24:4] == [IP address converted to hex] — ARP-пакеты с «указанным целевым адресом».[24:4] означает »4 байта, найденные по байтовому смещению 24».


Всё остальное было достаточно просто, готовое решение свелось примерно к пятидесяти строкам кода на Ruby. Если вкратце, то arp_standin — это демон, выполняющий следующее:

  • Запускает себя, получает такие опции конфигурации:
    • IP-адрес и MAC-адрес машины, которую он подменяет («цели»)
    • Сетевой интерфейс, с которым нужно работать

  • Слушает ARP-запросы для IP-адреса цели
  • При обнаружении ARP-запроса к IP-адресу цели отвечает MAC-адресом цели


Так как сопоставление IP-адреса и MAC-адреса сервера задано статически при помощи конфигурации демона arp_standin, нас не волнует истечение срока действия записи кэша ARP в Raspberry Pi.

Для установки демона или для изучения исходного кода перейдите по следующей ссылке: репозиторий arp_standin в GitHub.

ARP используется в IPv4, а в IPv6 его заменил Neighbor Discovery Protocol (NDP). Пока у меня нет никакой потребности в IPv6, поэтому я полностью отключил IPv6 на сервере при помощи описанных ниже действий. В будущем можно будет добавить в сервис ARP-Standin поддержку Neighbor Discovery.

Запустив на Raspberry Pi новый сервис, я воспользовался Wireshark, чтобы убедиться, что отправляемые на сервер ARP-запросы вызывали ответы от ARP Stand-in. Всё сработало, система выглядела многообещающе.

Соединяем всё вместе


Две основные части были реализованы:

  • сервер уходил в сон в состоянии простоя
  • сервер мог просыпаться от unicast-пакетов
  • другие машины могли резолвить MAC-адрес сервера при помощи ARP ещё долго после того, как он ушёл в сон


Запустив ARP Stand-in, я включил сервер и начал выполнять бэкап с моего компьютера. После завершения бэкапа сервер автоматически ушёл в сон. Но возникла проблема: после ухода в сон сервер мгновенно просыпался.

Нежелательные пробуждения


Первым делом я проверил системные логи Linux, но они оказались не особо полезными, потому что в них не указывалось, какой именно пакет приводит к пробуждению. Wireshark/tcpdump здесь тоже не помогли, потому что они не запускались, когда компьютер спал. Тогда я подумал использовать зеркалирование: перехватывать пакеты с промежуточного устройства между сервером и остальной сетью. После краткой безуспешной попытки настроить дополнительный роутер для запуска OpenWRT поиск самого дешёвого сетевого коммутатора с зеркалированием портов привёл меня к TP-Link TL-SG105E ценой примерно $30.

3w7nejk4wygzzx9z5nb7q71shlu.png


TL-SG105E: простой недорогой коммутатор с поддержкой зеркалирования портов

Подключив коммутатор и настроив зеркалирование, я начал перехватывать пакеты с помощью Wireshark, и виновники сразу были выявлены:

  1. Мой Mac, сконфигурированный для использования сервера как хоста бэкапов Time Machine при помощи AFP, отправлял серверу AFP-пакеты после того, как он уходил в сон
  2. Netgear R7000, работающий в качестве точки беспроводного доступа, самовольно отправлял серверу частые NBTSTAT-запросы NetBIOS


Устраняем AFP-пакеты


У меня была догадка о том, почему Mac отправлял эти пакеты:

  • Mac монтировал общий ресурс AFP, чтобы выполнить бэкап Time Machine
  • Бэкап Time Machine завершался, однако ресурс оставался примонтированным
  • Mac периодически проверял состояние ресурса, как это обычно делается для примонтированного сетевого ресурса


Ещё у меня возникла догадка, что для решения проблемы нужно, чтобы ресурс размонтировался перед уходом сервера в сон и Mac больше не пинговал состояние сервера. Я выяснил, что отключение AFP-сервиса приводит к размонтированию общих ресурсов на всех его клиентах, в чём и заключается наша цель. Теперь мне достаточно было лишь гарантировать, что сервис будет отключаться при уходе сервера в сон, а затем запускаться, когда он пробудится.

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

Устранение пакетов NetBIOS


Это оказалось более сложной задачей, поскольку пакеты поступали по собственной инициативе, они казались случайными и не связанными с действиями, выполняемыми сервером. Я подумал, что они могут быть связаны с запущенными на сервере сервисами Samba, но пакеты продолжали поступать, даже когда я полностью удалил Samba с сервера.

Но почему вообще сетевой роутер отправляет NetBIOS-запросы? Оказалось, что у роутеров Netgear есть функция ReadySHARE для отображения USB-устройств по сети при помощи протокола SMB. Предположительно, внутри прошивки роутера находится Samba, которая использует NetBIOS-запросы для создания и поддержания собственного представления хостов NetBIOS в сети. Решение просто — достаточно отключить ReadySHARE, ведь так? Увы, в стандартной прошивке Netgear сделать это невозможно.

Это заставило меня прошить роутер опенсорсной FreshTomato. Я рад, что сделал это, потому что эта прошивка гораздо лучше стандартной, и она мгновенно прекратила отправку нежелательных пакетов NetBIOS.

Time Machine не запускает пробуждение


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

Это было замечательно, но одна функция не работала: когда я запускал бэкап на Mac, то Time Machine бесконечно показывала состояние загрузки сообщением Connecting to backup disk... и в конце концов сдавалась. Причина была в том, что серверу не удавалось разбудить отправляемыми Mac пакетами или Mac вообще не отправлял пакеты?

sr2xb44x6v-du1p7zn4dig8-i-k.png


На этот вопрос ответил перехват Wireshark со включенным зеркалированием: Mac не отправлял пакетов серверу, даже спустя долгое время после сообщения Connecting to backup disk.... Я начал изучать логи Time Machine в macOS при помощи следующей команды:

log show --style syslog --predicate 'senderImagePath contains[cd] "TimeMachine"' --info


Благодаря нескольким записям всё стало понятно:

(TimeMachine) [com.apple.TimeMachine:Mounting] Attempting to mount 'afp://backup_mbp@homeserver._afpovertcp._tcp.local./tm_mbp'
...
(TimeMachine) [com.apple.TimeMachine:General] Failed to resolve CFNetServiceRef with name = homeserver type = _afpovertcp._tcp. domain = local.


Для резолвинга IP-адреса сервера бэкапов по его имени хоста Mac использовал mDNS (он же Bonjour, Zeroconf). Сервер находился во сне, а потому не отвечал на запросы, поэтому Mac не удавалось резолвить IP-адрес. Это объясняло, почему Mac не отправлял пакеты серверу, не пробуждая его.

mDNS stand-in


У меня уже был сервис ARP stand-in, теперь мне нужно было, чтобы Raspberry Pi ещё и отвечала на mDNS-запросы к серверу, пока он спит. Я знал, что одной из основных реализаций mDNS для Linux был Avahi. Сначала я попробовал использовать эти инструкции при помощи файлов .service для настройки Raspberry Pi так, чтобы она отвечала на mDNS-запросы от лица сервера. Для проверки результата я использовал на Mac следующую команду:

dns-sd -L homeserver _afpovertcp._tcp local


По какой-то причине этот подход просто не работал; Avahi не отвечал от лица сервера. Потом я поэкспериментировал с avahi-publish (man-страница), и, к моему приятному удивлению, это сразу сработало. Я использовал следующую команду:

avahi-publish -s homeserver _afpovertcp._tcp 548 -H homeserver.local


Далее мне достаточно было лишь создать определение сервиса systemd, который будет автоматически запускать команду avahi-publish при включении (конфигурация показана выше).

Завершение


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

© Habrahabr.ru