11. Службы с активацией в стиле inetd

В одной из предыдущих глав (гл. 3) я рассказывал, как можно преобразовать SysV init-скрипт в юнит-файл systemd. В этой главе мы рассмотрим, как провести аналогичное преобразование для служб inetd.

Начнем с небольшого экскурса в историю вопроса. Уже многие годы inetd считается одной из базовых служб Unix-систем. Работая как суперсервер, он слушает сетевые сокеты от имени различных служб, и активирует соответствующие службы при поступлении на эти сокеты входящих соединений. Таким образом, он обеспечивает механизм активации по запросу (on-demand activation). Подобный подход избавляет от необходимости держать постоянно работающими все серверные процессы, что позволяет поддерживать множество служб даже на системах с очень ограниченными ресурсами. В дистрибутивах Linux можно встретить несколько различных реализаций inetd. Наиболее популярные из них — BSD inetd и xinetd. Хотя inetd во многих дистрибутивах до сих пор устанавливается по умолчанию, сейчас он уже редко используется для запуска сетевых служб — теперь большинство из них запускаются автономно при загрузке системы (основной аргумент в пользу такой схемы — она позволяет обеспечить более высокую производительность служб).
Одной из ключевых возможностей systemd (а также launchd от Apple) является сокет-активация — тот же самый механизм, давным-давно реализованный inetd, однако в данном случае ключевые цели немного другие. Сокет-активация в стиле systemd прежде всего ориентирована на локальные сокеты (AF_UNIX), хотя поддерживаются также и сетевые сокеты (AF_INET). И более важное отличие — сокет-активация в systemd обеспечивает не только экономию системных ресурсов, но также и эффективную параллелизацию работы (так как она позволяет запускать клиентские и серверные процессы одновременно, непосредственно после создания сокета), упрощение процесса конфигурации (отпадает необходимость явно указывать зависимости между службами) и увеличение надежности (перезапуск службы, служебный или экстренный — в случае падения — не приводит к недоступности сокета). Тем не менее, systemd ничуть не хуже inetd может запускать службы в ответ на входящие сетевые соединения. Любая сокет-активация требует соответствующей поддержки со стороны самой запускаемой службы. systemd предоставляет очень простой интерфейс, который может быть использован службами для обеспечения сокет-активации. В основе этого простого и минималистичного механизма лежит функция sd_listen_fds(). Однако, интерфейс, традиционно используемый в inetd, еще проще. Он позволяет передавать запускаемой службе только один сокет, который формируется из потоков STDIN и STDOUT запущенного процесса. Поддержка этого механизма также присутствует в systemd — для обеспечения совместимости со множеством служб, у которых сокет-активация реализована только в стиле inetd.

Прежде, чем мы перейдем к примерам, рассмотрим три различных схемы, использующих сокет-активацию:

1. Сокет-активация, ориентированная на параллелизацию, упрощение и надежность: сокеты создаются на ранних стадиях загрузки, и единственный экземпляр службы тут же начинает обслуживать все поступающие запросы. Такая схема подходит для часто используемых системных служб — очевидно, что такие службы лучше запускать как можно раньше, и по возможности параллельно. В качестве примера можно привести D-Bus и Syslog.

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

3. Сокет-активация для служб, запускающих по экземпляру на каждое соединение: сокеты создаются на ранней стадии загрузки, и при каждом входящем соединении запускается экземпляр службы, которому передается сокет соединения (слушающий сокет при этом остается у суперсервера, inetd или systemd). Эта схема подходит для редко используемых служб, не критичных по производительности (каждый новый процесс занимает сравнительно немного ресурсов). Пример: SSH.

Описанные схемы отнюдь не эквивалентны с точки зрения производительности. Первая и вторая схема, после завершения запуска службы, обеспечивают точно такую же производительность, как и в случае с независимой (stand-alone) службой (т.е. службой, запущенной без использования суперсервера и сокет-активации), так как слушающий сокет передается непосредственно процессу службы, и дальнейшая обработка входящих соединений происходит точно так же, как и в независимой службе. В то же время, производительность третьей из предложенных схем порою оставляет желать лучшего: каждое входящее соединение порождает еще один процесс службы, что, при большом количестве соединений, может привести к значительному потреблению системных ресурсов. Впрочем, эта схема имеет и свои достоинства. В частности, обеспечивается эффективная изоляция обработки клиентских запросов и, кроме того, выбор такой схемы активации значительно упрощает процесс разработки службы.
В systemd наибольшее внимание уделяется первой из этих схем, однако остальные две тоже прекрасно поддерживаются. В свое время, я рассказывал, какие изменения в коде службы нужно сделать для обеспечения работы по второй схеме, на примере сервера CUPS. Что же касается inetd, то он предназначен прежде всего для работы по третьей схеме, хотя поддерживает и вторую (но не первую). Именно из-за специфики этой схемы inetd получил репутации «медленного» (что, на самом деле, немного несправедливо).

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

В качестве примера возьмем SSH, известную и широко распространенную службу. На большинстве систем, где она используется, частота обращений к ней не превышает одного раза в час (как правило, она значительно меньше этой величины). SSH уже очень давно поддерживает сокет-активацию в стиле inetd, согласно третьей схеме. Так как необходимость в данной службе возникает сравнительно редко, и число одновременно работающих процессов обычно невелико, она хорошо подходит для использования по этой схеме. Перерасход системных ресурсов должен быть незначителен: большую часть времени такая служба фактически не выполняется и не тратит ресурсы. Когда кто-либо начинает сеанс удаленной работы, она запускается, и останавливается немедленно по завершении сеанса, освобождая ресурсы. Что ж, посмотрим, как в systemd можно воспользоваться режимом совместимости с inetd и обеспечить сокет-активацию SSH.
Так выглядит строка с конфигурацией службы SSH для классического inetd:


ssh stream tcp nowait root /usr/sbin/sshd sshd -i



Аналогичный фрагмент конфигурации для xinetd:


service ssh {
socket_type = stream
protocol = tcp
wait = no
user = root
server = /usr/sbin/sshd
server_args = -i
}


Б´ольшая часть опций интуитивно понятна, кроме того, нетрудно заметить, что оба этих текста повествуют об одном и том же. Однако, некоторые моменты не вполне очевидны. Например, номер прослушиваемого порта (22) не указывается в явном виде. Он определяется путем поиска имени службы в базе данных /etc/services. Подобный подход был некогда весьма популярен в Unix, но сейчас он уже постепенно выходит из моды, и поэтому в современных версиях xinetd поддерживается возможность явного указания номера порта. Одна из наиболее интересных настроек названа не вполне очевидно — nowait (wait=no). Она определяет, будет ли служба работать по второй (wait) или третьей (nowait) схеме. И наконец, ключ -i активирует inetd-режим в SSH-сервере (без этого ключа он работает в независимом режиме).
На языке systemd эти фрагменты конфигурации превращаются в два файла. Первый из них, sshd.socket, содержит информацию о прослушиваемом сокете:


[Unit]
Description=SSH Socket for Per-Connection Servers
[Socket]
ListenStream=22
Accept=yes
[Install]
WantedBy=sockets.target


Смысл большинства опций вполне очевиден. Сделаю лишь пару замечаний. Accept=yes соответствует режиму nowait. Надеюсь, предложенное мною название более точно отражает смысл опции — в режиме nowait суперсвервер сам вызывает accept() для слушающего сокета, в то время как в режиме wait эта работа ложится на процесс службы. Опция WantedBy=sockets.target обеспечивает активацию данного юнита в нужный момент при загрузке системы. Второй из этих файлов — sshd@.service:


[Unit]
Description=SSH Per-Connection Server
[Service]
ExecStart=-/usr/sbin/sshd -i
StandardInput=socket


Большинство представленных здесь опций, как всегда, понятны интуитивно. Особое внимание стоит обратить лишь на строчку StandardInput=socket, которая, собственно, и включает для данной службы режим совместимости с inetd-активацией. Опция StandardInput= позволяет указать, куда будет подключен поток STDIN процесса данной службы (подробности см. на странице руководства). Задав для нее значение socket, мы обеспечиваем подключение этого потока к сокету соединения, как того и требует механизм inetd-активации. Заметим, что явно указывать опцию StandardOutput= в данном случае необязательно — она автоматически устанавливается в то же значение, что и StandardInput, если явно не указано что-то другое. Кроме того, можно отметить наличие «-» перед именем бинарного файла sshd. Таким образом мы указываем systemd игнорировать код выхода процесса sshd. По умолчанию, systemd сохраняет коды выхода для всех экземпляров службы, завершившихся с ошибкой (сбросить эту информацию можно командой systemctl reset-failed). SSH довольно часто завершается с ненулевым кодом выхода, и мы разрешаем systemd не регистрировать подобные «ошибки».
Служба sshd@.service предназначена для работы в виде независимых экземпляров (такие службы мы рассматривали в предыдущей главе 10). Для каждого входящего соединения systemd будет создавать свой экземпляр этой службы, причем идентификатор экземпляра формируется на основе реквизитов соединения.
Быть может, вы спросите: почему для настройки inetd-службы в systemd требуется два файла конфигурации, а не один? Отвечаем: чтобы избежать излишнего усложнения, мы обеспечиваем максимально прозрачную связь между работающими юнитами и соответствующими им юнит-файлами. Такой подход позволяет независимо оперировать юнитом сокета и юнитами соответствующих служб при формировании графа зависимостей и при управлении юнитами. В частности, вы можете остановить (удалить) сокет, не затрагивая уже работающие экземпляры соответствующей службы, или остановить любой из этих экземпляров, не трогая другие экземпляры и сам сокет.
Посмотрим, как наш пример будет работать. После того, как мы поместим оба предложенных выше файла в каталог /etc/systemd/system, мы сможем включить сокет (то есть, обеспечить его активацию при каждой нормальной загрузке) и запустить его (то есть активировать в текущем сеансе работы):


# systemctl enable sshd.socket
ln -s ’/etc/systemd/system/sshd.socket’ ’/etc/systemd/system/sockets.target.wants/sshd.socket’
# systemctl start sshd.socket
# systemctl status sshd.socket
sshd.socket - SSH Socket for Per-Connection Servers
        Loaded: loaded (/etc/systemd/system/sshd.socket; enabled)
        Active: active (listening) since Mon, 26 Sep 2011 20:24:31 +0200; 14s ago
      Accepted: 0; Connected: 0
        CGroup: name=systemd:/system/sshd.socket


Итак, наш сокет уже прослушивается, но входящих соединений на него пока не поступало (счетчик Accepted: показывает количество принятых соединений с момента создания сокета, счетчик Connected: — количество активных соединений на текущий момент). Подключимся к нашему серверу с двух разных хостов, и посмотрим на список служб:


$ systemctl --full | grep ssh
sshd@172.31.0.52:22-172.31.0.4:47779.service    loaded   active   running    SSH Per-Connection Server
sshd@172.31.0.52:22-172.31.0.54:52985.service   loaded   active   running    SSH Per-Connection Server
sshd.socket                                     loaded   active   listening  SSH Socket for Per-Connection Serv


Как и следовало ожидать, работают два экземпляра нашей службы, по одному на соединение, и их в названиях указаны IP-адреса и TCP-порты источника и получателя.
Заметим, что в случае с локальными сокетами типа AF_UNIX там были бы указаны идентификаторы процесса и пользователя, соответствующие подключенному клиенту.
Таким образом, мы можем независимо выслеживать и убивать отдельные экземпляры sshd (например, если нам нужно прервать конкретный удаленный сеанс):


# systemctl kill sshd@172.31.0.52:22-172.31.0.4:47779.service


Вот, пожалуй, и все, что вам нужно знать о портировании inetd-служб в systemd и дальнейшем их использовании.

Применительно к SSH, в большинстве случаев схема с inetd-активацией позволяет сэкономить системные ресурсы и поэтому оказывается более эффективным решением, чем использование обычного service-файла sshd, обеспечивающего запуск одиночной службы без использования сокет-активации (поставляется в составе пакета и может быть включен при необходимости). В ближайшее время я собираюсь направить соответствующий запрос относительно нашего пакета SSH в багтрекер Fedora.

В завершение нашей дискуссии, сравним возможности xinetd и systemd, и выясним, может ли systemd полностью заменить xinetd, или нет. Вкратце: systemd поддерживает далеко не все возможности xinetd и поэтому отнюдь не является его полноценной заменой на все случаи жизни. В частности, если вы заглянете в список параметров конфигурации xinetd, вы заметите, что далеко не все эти опции доступны в systemd. Например, в systemd нет и никогда не будет встроенных служб echo, time, daytime, discard и т.д. Кроме того, systemd не поддерживает TCPMUX и RPC. Впрочем, б´ольшая часть этих опций уже не актуальна в наше время. Подавляющее большинство inetd-служб не используют эти опции (в частности, ни одна из имеющихся в Fedora xinetd-служб не требует поддержки перечисленных опций). Стоит отметить, что xinetd имеет некоторые интересные возможности, отсутствующие в systemd, например, списки контроля доступа для IP-адресов (IP ACL). Однако, большинство администраторов, скорее всего, согласятся, что настройка брандмауэра является более эффективным решением подобных задач, а для ценителей устаревших технологий systemd предлагает поддержку tcpwrap.

Прим. перев.: Стоит отметить, что приведенный пример является не единственным случаем, когда возможности брандмауэра Linux дублируются опциями xinetd. Например, количество соединений с каждого хоста может быть ограничено критерием connlimit, а скорость поступления входящих соединений можно контролировать сочетанием критериев limit и conntrack (ctstate NEW). Критерий recent позволяет создать аналог простейшей IDS/IPS, реализованной механизмом SENSORS в xinetd. Кроме того, в ряде случаев возможности брандмауэра значительно превосходят функциональность xinetd. Например, критерий hashlimit, опять-таки в сочетании с conntrack, позволяет ограничить скорость поступления входящих соединений с каждого хоста (не путать с критерием connlimit, ограничивающим количество одновременно открытых соединений). Также стоит отметить, что интегрированная в Linux подсистема ipset гораздо лучше подходит для работы с большими списками разрешенных/запрещенных адресов, нежели встроенные механизмы xinetd.

С другой стороны, systemd тоже предоставляет ряд возможностей, отсутствующих в xinetd, в частности, индивидуальный контроль над каждым экземпляром службы (см. выше), и внушительный набор настроек для контроля окружения, в котором запускаются экземпляры. Я надеюсь, что возможностей systemd должно быть достаточно для решения большинства задач, а в тех редких случаях, когда вам потребуются специфические опции xinetd — ничто не мешает вам запустить его в дополнение к systemd.
Таким образом, уже сейчас в большинстве случаев xinetd можно выкинуть из числа обязательных системных компонентов. Можно сказать, что systemd не просто возвращает функциональность классического юниксового inetd, но еще и восстанавливает ее ключевую роль в Linux-системах.

Теперь, вооруженные этими знаниями, вы можете портировать свои службы с inetd на systemd. Но, конечно, будет лучше, если этим займутся разработчики из апстрима приложения, или сопровождающие вашего дистрибутива.

Содержание
Вперед - К вопросу о безопасности
Назад - Экземпляры служб