Добавлен перевод статьи "IP Accounting and Access Lists with systemd"
This commit is contained in:
601
s4a.tex
601
s4a.tex
@@ -2766,7 +2766,7 @@ PrivateNetwork=yes
|
|||||||
котором настраивается только интерфейс обратной петли.
|
котором настраивается только интерфейс обратной петли.
|
||||||
|
|
||||||
\subsection{Предоставление службам независимых каталогов \texttt{/tmp}}
|
\subsection{Предоставление службам независимых каталогов \texttt{/tmp}}
|
||||||
\label{sec:privatetmp}
|
\label{ssec:privatetmp}
|
||||||
|
|
||||||
Еще одна простая, но мощная опция настройки служб~--- +PrivateTmp=+:
|
Еще одна простая, но мощная опция настройки служб~--- +PrivateTmp=+:
|
||||||
\begin{Verbatim}
|
\begin{Verbatim}
|
||||||
@@ -3781,6 +3781,7 @@ $ journalctl /usr/sbin/vpnc /usr/sbin/dhclient
|
|||||||
Отлично, мы нашли причину проблемы!
|
Отлично, мы нашли причину проблемы!
|
||||||
|
|
||||||
\subsection{Продвинутые методы выборки}
|
\subsection{Продвинутые методы выборки}
|
||||||
|
\label{ssec:metadata}
|
||||||
|
|
||||||
Да, это все, конечно, здорово, но попробуем подняться еще на ступеньку выше.
|
Да, это все, конечно, здорово, но попробуем подняться еще на ступеньку выше.
|
||||||
Чтобы понять описанные ниже приемы, нужно знать, что systemd добавляет к
|
Чтобы понять описанные ниже приемы, нужно знать, что systemd добавляет к
|
||||||
@@ -5402,7 +5403,7 @@ systemd реализует одновременно обе стратегии,
|
|||||||
Для самой службы создается пространство имен монтирования (mount
|
Для самой службы создается пространство имен монтирования (mount
|
||||||
namespace), в котором эти подкаталоги bind-монтируются в +/tmp+
|
namespace), в котором эти подкаталоги bind-монтируются в +/tmp+
|
||||||
и +/var/tmp+ соответственно. См. также раздел
|
и +/var/tmp+ соответственно. См. также раздел
|
||||||
\ref{sec:privatetmp}.}, причем их жизненный цикл привязан к
|
\ref{ssec:privatetmp}.}, причем их жизненный цикл привязан к
|
||||||
жизненному циклу службы: при остановке службы удаляется
|
жизненному циклу службы: при остановке службы удаляется
|
||||||
не~только ее пользователь, но и ее временные каталоги. (Опять же
|
не~только ее пользователь, но и ее временные каталоги. (Опять же
|
||||||
замечу, что эту директиву имеет смысл применять и без
|
замечу, что эту директиву имеет смысл применять и без
|
||||||
@@ -5945,6 +5946,602 @@ DynamicUser=yes
|
|||||||
|
|
||||||
На сегодня все!
|
На сегодня все!
|
||||||
|
|
||||||
|
\sectiona{Учет и фильтрация IP-трафика служб}
|
||||||
|
|
||||||
|
\emph{Коротко о главном: теперь systemd может подсчитывать и фильтровать
|
||||||
|
по подсетям IP-трафик любой службы.}
|
||||||
|
|
||||||
|
В недавно вышедшем выпуске
|
||||||
|
\href{https://lists.freedesktop.org/archives/systemd-devel/2017-October/039589.html}%
|
||||||
|
{systemd 235}, помимо расширения поддержки динамических пользователей
|
||||||
|
(рассмотренной в предыдущей статье), добавлена еще одна важная возможность:
|
||||||
|
учет и фильтрация IP-трафика.
|
||||||
|
|
||||||
|
systemd и раньше поддерживал механизмы управления ресурсами, доступными юнитам:
|
||||||
|
процессорным временем, дисковым вводом-выводом, потреблением памяти, количеством
|
||||||
|
запускаемых процессов. А в версии 235 был добавлен контроль над еще одним видом
|
||||||
|
ресурсов~--- IP-трафиком.
|
||||||
|
|
||||||
|
Для этого были введены три новых директивы конфигурации юнитов:
|
||||||
|
\begin{enumerate}
|
||||||
|
\item \hreftt{https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html\#IPAccounting=}%
|
||||||
|
{IPAccounting=}~--- булева переменная, позволяющая включить
|
||||||
|
подсчет IP-трафика, принятого и полученного службой (как
|
||||||
|
количества пакетов, так и их суммарного объема в байтах).
|
||||||
|
\item \hreftt{https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html\#IPAddressAllow=ADDDRESS[/PREFIXLENGTH]\%E2\%80\%A6}%
|
||||||
|
{IPAddressDeny=} указывает <<черный>> список адресов подсетей
|
||||||
|
или хостов для службы. Весь трафик, отправляемый на эти адреса
|
||||||
|
процессами службы, а также полученный ими с этих адресов,
|
||||||
|
будет заблокирован.
|
||||||
|
\item \hreftt{https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html\#IPAddressAllow=ADDDRESS[/PREFIXLENGTH]\%E2\%80\%A6}%
|
||||||
|
{IPAddressAllow=} имеет противоположный смысл~--- процессы
|
||||||
|
службы могут обмениваться трафиком с перечисленными в этой
|
||||||
|
директиве адресами, даже если они пересекаются с тем, что задано
|
||||||
|
в +IPAddressDeny=+ (т.е. белый список имеет приоритет над
|
||||||
|
черным).
|
||||||
|
\end{enumerate}
|
||||||
|
|
||||||
|
Эти три опции предоставляют интерфейс к новой функциональности ядра Linux,
|
||||||
|
добавленной в выпуске 4.11, а именно, eBPF-хукам контрольных групп. Основную
|
||||||
|
работу берет на себя ядро, а systemd лишь обеспечивает возможность настройки
|
||||||
|
этих механизмов. Обратите внимание, что cgroup/eBPF никак не~относятся к
|
||||||
|
традиционному брандмауэру Linux~--- NetFilter/+iptables+. Вы можете использовать
|
||||||
|
любую из этих технологий, или обе сразу, или вообще не~пользоваться ни~одной из
|
||||||
|
них.
|
||||||
|
|
||||||
|
\subsectiona{Учет IP-трафика}
|
||||||
|
|
||||||
|
Давайте посмотрим, как работает учет трафика. Создадим тестовую службу
|
||||||
|
+/etc/systemd/system/ip-accounting-test.service+:
|
||||||
|
\begin{Verbatim}
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/bin/ping 8.8.8.8
|
||||||
|
IPAccounting=yes
|
||||||
|
\end{Verbatim}
|
||||||
|
|
||||||
|
Этот простой юнит вызывает команду
|
||||||
|
\hreftt{http://man7.org/linux/man-pages/man8/ping.8.html}{ping(8)}, которая
|
||||||
|
отправляет серию ICMP эхо-запросов на IP-адрес 8.8.8.8 (это адрес DNS-сервера
|
||||||
|
Google; он используется в нашем примере потому, что его легко запомнить, он
|
||||||
|
отовсюду доступен и отвечает на эхо-запросы; вы можете использовать любой другой
|
||||||
|
адрес, отвечающий на пинги). Опция +IPAccounting=yes+ включает учет IP-трафика
|
||||||
|
для нашей службы.
|
||||||
|
|
||||||
|
Запустим нашу службу и посмотрим ее состояние:
|
||||||
|
\begin{Verbatim}
|
||||||
|
# systemctl daemon-reload
|
||||||
|
# systemctl start ip-accounting-test
|
||||||
|
# systemctl status ip-accounting-test
|
||||||
|
ip-accounting-test.service
|
||||||
|
Loaded: loaded (/etc/systemd/system/ip-accounting-test.service; static; vendor preset: disabled)
|
||||||
|
Active: active (running) since Mon 2017-10-09 18:05:47 CEST; 1s ago
|
||||||
|
Main PID: 32152 (ping)
|
||||||
|
IP: 168B in, 168B out
|
||||||
|
Tasks: 1 (limit: 4915)
|
||||||
|
CGroup: /system.slice/ip-accounting-test.service
|
||||||
|
└─32152 /usr/bin/ping 8.8.8.8
|
||||||
|
|
||||||
|
Okt 09 18:05:47 sigma systemd[1]: Started ip-accounting-test.service.
|
||||||
|
Okt 09 18:05:47 sigma ping[32152]: PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
|
||||||
|
Okt 09 18:05:47 sigma ping[32152]: 64 bytes from 8.8.8.8: icmp_seq=1 ttl=59 time=29.2 ms
|
||||||
|
Okt 09 18:05:48 sigma ping[32152]: 64 bytes from 8.8.8.8: icmp_seq=2 ttl=59 time=28.0 ms
|
||||||
|
\end{Verbatim}
|
||||||
|
Как видим, программа +ping+ сейчас работает. Судя по логу в конце вывода
|
||||||
|
+systemctl status+, она как раз закончила второй цикл пинга. Однако, для нас
|
||||||
|
сейчас более интересна строка выше, начинающаяся с +IP:+ и содержащая счетчики
|
||||||
|
принятых и полученных байт IP-трафика. Сайчас она показывает, что было
|
||||||
|
отправлено 168 байт, и ровно столько же принято. Это вполне предсказуемо: ICMP
|
||||||
|
эхо-запросы и эхо-ответы должны иметь одинаковый размер\footnote{Прим. перев.:
|
||||||
|
Если не~считать заголовков IP и ICMP, имеющих фиксированную длину, размер пакета
|
||||||
|
определяется только телом сообщения (payload), которое должно полностью
|
||||||
|
копироваться из запроса в ответ.}. Обратите внимание, что эта строка появляется
|
||||||
|
только для юнитов с включенной опцией +IPAccounting=+. Если она выключена,
|
||||||
|
трафик юнита не~подсчитывается\footnote{Прим. перев.: Можно заметить, что для
|
||||||
|
приведенного в примере юнита подсчитывается не~только трафик, но и количество
|
||||||
|
процессов/потоков (строка +Tasks:+). Это связано с введенным лимитом на
|
||||||
|
их количество
|
||||||
|
\hreftt{https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html\#TasksMax=N}%
|
||||||
|
{TasksMax=4915} (+TasksMax=+ автоматически включает
|
||||||
|
\hreftt{https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html\#TasksAccounting=}%
|
||||||
|
{TasksAccounting=}).}.
|
||||||
|
|
||||||
|
Подождем немного, и вызовем +systemctl status+ снова:
|
||||||
|
\begin{Verbatim}
|
||||||
|
# systemctl status ip-accounting-test
|
||||||
|
ip-accounting-test.service
|
||||||
|
Loaded: loaded (/etc/systemd/system/ip-accounting-test.service; static; vendor preset: disabled)
|
||||||
|
Active: active (running) since Mon 2017-10-09 18:05:47 CEST; 4min 28s ago
|
||||||
|
Main PID: 32152 (ping)
|
||||||
|
IP: 22.2K in, 22.2K out
|
||||||
|
Tasks: 1 (limit: 4915)
|
||||||
|
CGroup: /system.slice/ip-accounting-test.service
|
||||||
|
└─32152 /usr/bin/ping 8.8.8.8
|
||||||
|
|
||||||
|
Okt 09 18:10:07 sigma ping[32152]: 64 bytes from 8.8.8.8: icmp_seq=260 ttl=59 time=27.7 ms
|
||||||
|
Okt 09 18:10:08 sigma ping[32152]: 64 bytes from 8.8.8.8: icmp_seq=261 ttl=59 time=28.0 ms
|
||||||
|
Okt 09 18:10:09 sigma ping[32152]: 64 bytes from 8.8.8.8: icmp_seq=262 ttl=59 time=33.8 ms
|
||||||
|
Okt 09 18:10:10 sigma ping[32152]: 64 bytes from 8.8.8.8: icmp_seq=263 ttl=59 time=48.9 ms
|
||||||
|
Okt 09 18:10:11 sigma ping[32152]: 64 bytes from 8.8.8.8: icmp_seq=264 ttl=59 time=27.2 ms
|
||||||
|
Okt 09 18:10:12 sigma ping[32152]: 64 bytes from 8.8.8.8: icmp_seq=265 ttl=59 time=27.0 ms
|
||||||
|
Okt 09 18:10:13 sigma ping[32152]: 64 bytes from 8.8.8.8: icmp_seq=266 ttl=59 time=26.8 ms
|
||||||
|
Okt 09 18:10:14 sigma ping[32152]: 64 bytes from 8.8.8.8: icmp_seq=267 ttl=59 time=27.4 ms
|
||||||
|
Okt 09 18:10:15 sigma ping[32152]: 64 bytes from 8.8.8.8: icmp_seq=268 ttl=59 time=29.7 ms
|
||||||
|
Okt 09 18:10:16 sigma ping[32152]: 64 bytes from 8.8.8.8: icmp_seq=269 ttl=59 time=27.6 ms
|
||||||
|
\end{Verbatim}
|
||||||
|
Как видно из вывода, после 269 пингов счетчики достигли значения 22 килобайт.
|
||||||
|
|
||||||
|
Несмотря на то, что +systemctl status+ показывает только счетчики байт,
|
||||||
|
ведется также учет и количества пакетов. Для просмотра этих значений можно
|
||||||
|
использовать низкоуровневую команду +systemctl show+:
|
||||||
|
\begin{Verbatim}
|
||||||
|
# systemctl show ip-accounting-test -p IPIngressBytes -p IPIngressPackets \
|
||||||
|
-p IPEgressBytes -p IPEgressPackets
|
||||||
|
IPIngressBytes=37776
|
||||||
|
IPIngressPackets=449
|
||||||
|
IPEgressBytes=37776
|
||||||
|
IPEgressPackets=449
|
||||||
|
\end{Verbatim}
|
||||||
|
|
||||||
|
Разумеется, эта информация доступна также и через API D-Bus. Если вы хотите
|
||||||
|
автоматизировать обработку таких данных, использование вызовов D-Bus будет
|
||||||
|
гораздо удобнее, чем разбор вывода +systemctl show+.
|
||||||
|
|
||||||
|
Теперь остановим нашу службу:
|
||||||
|
\begin{Verbatim}
|
||||||
|
# systemctl stop ip-accounting-test
|
||||||
|
\end{Verbatim}
|
||||||
|
|
||||||
|
После остановки службы, для которой включен учет потребления ресурсов, в
|
||||||
|
системный журнал добавляется запись с итоговой суммой потребленных службой
|
||||||
|
ресурсов. Просмотреть ее можно командой +journalctl+:
|
||||||
|
\begin{Verbatim}[fontsize=\small]
|
||||||
|
# journalctl -u ip-accounting-test -n 5
|
||||||
|
-- Logs begin at Thu 2016-08-18 23:09:37 CEST, end at Mon 2017-10-09 18:17:02 CEST. --
|
||||||
|
Okt 09 18:15:50 sigma ping[32152]: 64 bytes from 8.8.8.8: icmp_seq=603 ttl=59 time=26.9 ms
|
||||||
|
Okt 09 18:15:51 sigma ping[32152]: 64 bytes from 8.8.8.8: icmp_seq=604 ttl=59 time=27.2 ms
|
||||||
|
Okt 09 18:15:52 sigma systemd[1]: Stopping ip-accounting-test.service...
|
||||||
|
Okt 09 18:15:52 sigma systemd[1]: Stopped ip-accounting-test.service.
|
||||||
|
Okt 09 18:15:52 sigma systemd[1]: ip-accounting-test.service: Received 49.5K IP traffic, sent 49.5K IP traffic
|
||||||
|
\end{Verbatim}
|
||||||
|
Последняя строка~--- и есть та самая запись о потребленных ресурсах. На самом
|
||||||
|
деле, эта запись является структурной\footnote{Прим. перев.: Подробнее о
|
||||||
|
структуре записей Journal можно почитать в разделе~\ref{ssec:metadata}.}, и
|
||||||
|
содержит поля метаданных, в которых приводится более полная информация:
|
||||||
|
\begin{Verbatim}[fontsize=\small]
|
||||||
|
# journalctl -u ip-accounting-test -n 1 -o verbose
|
||||||
|
-- Logs begin at Thu 2016-08-18 23:09:37 CEST, end at Mon 2017-10-09 18:18:50 CEST. --
|
||||||
|
Mon 2017-10-09 18:15:52.649028 CEST [s=89a2cc877fdf4dafb2269a7631afedad;i=14d7;b=4c7e7adcba0c45b69d612857270716d3;m=137592e75e;t=55b1f81298605;x=c3c9b57b28c9490e]
|
||||||
|
PRIORITY=6
|
||||||
|
_BOOT_ID=4c7e7adcba0c45b69d612857270716d3
|
||||||
|
_MACHINE_ID=e87bfd866aea4ae4b761aff06c9c3cb3
|
||||||
|
_HOSTNAME=sigma
|
||||||
|
SYSLOG_FACILITY=3
|
||||||
|
SYSLOG_IDENTIFIER=systemd
|
||||||
|
_UID=0
|
||||||
|
_GID=0
|
||||||
|
_TRANSPORT=journal
|
||||||
|
_PID=1
|
||||||
|
_COMM=systemd
|
||||||
|
_EXE=/usr/lib/systemd/systemd
|
||||||
|
_CAP_EFFECTIVE=3fffffffff
|
||||||
|
_SYSTEMD_CGROUP=/init.scope
|
||||||
|
_SYSTEMD_UNIT=init.scope
|
||||||
|
_SYSTEMD_SLICE=-.slice
|
||||||
|
CODE_FILE=../src/core/unit.c
|
||||||
|
_CMDLINE=/usr/lib/systemd/systemd --switched-root --system --deserialize 25
|
||||||
|
_SELINUX_CONTEXT=system_u:system_r:init_t:s0
|
||||||
|
UNIT=ip-accounting-test.service
|
||||||
|
CODE_LINE=2115
|
||||||
|
CODE_FUNC=unit_log_resources
|
||||||
|
MESSAGE_ID=ae8f7b866b0347b9af31fe1c80b127c0
|
||||||
|
INVOCATION_ID=98a6e756fa9d421d8dfc82b6df06a9c3
|
||||||
|
IP_METRIC_INGRESS_BYTES=50880
|
||||||
|
IP_METRIC_INGRESS_PACKETS=605
|
||||||
|
IP_METRIC_EGRESS_BYTES=50880
|
||||||
|
IP_METRIC_EGRESS_PACKETS=605
|
||||||
|
MESSAGE=ip-accounting-test.service: Received 49.6K IP traffic, sent 49.6K IP traffic
|
||||||
|
_SOURCE_REALTIME_TIMESTAMP=1507565752649028
|
||||||
|
\end{Verbatim}
|
||||||
|
Нас интересуют поля +IP_METRIC_INGRESS_BYTES=+, +IP_METRIC_INGRESS_PACKETS=+,
|
||||||
|
+IP_METRIC_EGRESS_BYTES=+, +IP_METRIC_EGRESS_PACKETS=+, отображающие значения
|
||||||
|
соответствующих счетчиков.
|
||||||
|
|
||||||
|
Все подобные записи имеют один и тот же идентификатор типа сообщения
|
||||||
|
(\href{https://www.freedesktop.org/software/systemd/man/systemd.journal-fields.html\#MESSAGE_ID=}%
|
||||||
|
{message ID}), при помощи которого их можно легко найти в журнале
|
||||||
|
(+ae8f7b866b0347b9af31fe1c80b127c0+). Вызовем +journalctl+ с указанием этого
|
||||||
|
идентификатора, добавив также ключ +-u+, который ограничит выборку только
|
||||||
|
записями нашей службы:
|
||||||
|
\begin{Verbatim}[fontsize=\small]
|
||||||
|
# journalctl -u ip-accounting-test MESSAGE_ID=ae8f7b866b0347b9af31fe1c80b127c0
|
||||||
|
-- Logs begin at Thu 2016-08-18 23:09:37 CEST, end at Mon 2017-10-09 18:25:27 CEST. --
|
||||||
|
Okt 09 18:15:52 sigma systemd[1]: ip-accounting-test.service: Received 49.6K IP traffic, sent 49.6K IP traffic
|
||||||
|
\end{Verbatim}
|
||||||
|
Приведенный вывод содержит пока только одну запись, так как мы запускали службу
|
||||||
|
всего один раз. Каждый последующий запуск будет добавлять новую запись.
|
||||||
|
|
||||||
|
Учет IP-трафика доступен также и для одноразовых служб, запускаемых через
|
||||||
|
\hreftt{https://www.freedesktop.org/software/systemd/man/systemd-run.html}%
|
||||||
|
{systemd-run}, что позволяет прозрачно выполнять различные команды как службы
|
||||||
|
systemd, и подсчитывать их трафик:
|
||||||
|
\begin{Verbatim}
|
||||||
|
# systemd-run -p IPAccounting=yes --wait \
|
||||||
|
wget https://cfp.all-systems-go.io/en/ASG2017/public/schedule/2.pdf
|
||||||
|
Running as unit: run-u2761.service
|
||||||
|
Finished with result: success
|
||||||
|
Main processes terminated with: code=exited/status=0
|
||||||
|
Service runtime: 878ms
|
||||||
|
IP traffic received: 231.0K
|
||||||
|
IP traffic sent: 3.7K
|
||||||
|
\end{Verbatim}
|
||||||
|
Мы использовали \hreftt{https://linux.die.net/man/1/wget}{wget}, чтобы загрузить
|
||||||
|
\href{https://cfp.all-systems-go.io/en/ASG2017/public/schedule/2.pdf}{PDF с
|
||||||
|
расписанием второго дня} нашей любимой конференции
|
||||||
|
\href{https://all-systems-go.io/}{All Systems Go! 2017}. Эта операция
|
||||||
|
потребовала 231 килобайт входящего трафика и 4 килобайта исходящего.
|
||||||
|
Особого внимания заслуживают параметры командной строки, с которыми мы вызвали
|
||||||
|
+systemd-run+. Первый, +-p IPAccounting=yes+, включает режим учета IP-трафика
|
||||||
|
для нашей одноразовой службы (аналогично тому, как та же строка в юнит-файле
|
||||||
|
делает это для обычной службы). Второй параметр, +--wait+, приказывает
|
||||||
|
+systemd-run+ дождаться завершения работы созданной службы, после чего вывести
|
||||||
|
информацию о результатах ее работы, включая интересующую нас статистику по
|
||||||
|
IP-трафику (разумеется, только при условии, что мы включили его подсчет).
|
||||||
|
|
||||||
|
Еще интереснее получается, если применять учет IP-трафика для
|
||||||
|
\emph{интерактивных} одноразовых служб:
|
||||||
|
\begin{Verbatim}
|
||||||
|
# systemd-run -p IPAccounting=1 -t /bin/sh
|
||||||
|
Running as unit: run-u2779.service
|
||||||
|
Press ^] three times within 1s to disconnect TTY.
|
||||||
|
sh-4.4# dnf update
|
||||||
|
...
|
||||||
|
sh-4.4# dnf install firefox
|
||||||
|
...
|
||||||
|
sh-4.4# exit
|
||||||
|
Finished with result: success
|
||||||
|
Main processes terminated with: code=exited/status=0
|
||||||
|
Service runtime: 5.297s
|
||||||
|
IP traffic received: ...B
|
||||||
|
IP traffic sent: ...B
|
||||||
|
\end{Verbatim}
|
||||||
|
Мы использовали ключ +systemd-run+ +--pty+ (в краткой форме +-t+), который
|
||||||
|
открывает интерактивное псевдо-терминальное подключение к запускаемой службе~---
|
||||||
|
в нашем случае это оболочка +/bin/sh+. Таким образом, мы получаем полноценную
|
||||||
|
оболочку, с управлением фоновыми заданиями и прочими возможностями, запущенную
|
||||||
|
в рамках службы systemd. Так как для этой службы включен учет IP-трафика,
|
||||||
|
при ее завершении мы получаем суммарную статистику по входящим и исходящим
|
||||||
|
данным. (Для краткости я не~стал показывать вывод целиком, а оставил только
|
||||||
|
ключевые моменты. Если вам хочется посмотреть на полный вывод~--- попробуйте
|
||||||
|
запустить что-нибудь сами.)
|
||||||
|
|
||||||
|
Иногда бывает нужно включить учет IP-трафика для юнита, который уже запущен.
|
||||||
|
Это можно сделать командой
|
||||||
|
\begin{Verbatim}
|
||||||
|
systemctl set-property foobar.service IPAccounting=yes
|
||||||
|
\end{Verbatim}
|
||||||
|
Обратите внимание, что она не~имеет обратной силы: трафик учитывается только с
|
||||||
|
момента выполнения этой команды. Аналогичной командой (+IPAccounting=no+) учет
|
||||||
|
трафика можно отключить.
|
||||||
|
|
||||||
|
Если вы хотите подсчитывать IP-трафик сразу для всех служб, вам вовсе
|
||||||
|
не~обязательно добавлять +IPAccounting=yes+ во все юнит-файлы. Достаточно
|
||||||
|
задействовать глобальную опцию
|
||||||
|
\hreftt{https://www.freedesktop.org/software/systemd/man/systemd-system.conf.html\#DefaultCPUAccounting=}%
|
||||||
|
{DefaultIPAccounting=} в файле +/etc/systemd/system.conf+.
|
||||||
|
|
||||||
|
\subsectiona{Фильтрация IP-трафика}
|
||||||
|
|
||||||
|
От учета трафика переходим к его фильтрации. Рассмотрим поподробнее механизм
|
||||||
|
контрольных списков IP-адресов (IP ACL), добавленный в systemd 235. Как уже
|
||||||
|
упоминалось выше, за него отвечают две директивы: +IPAddressAllow=+ и
|
||||||
|
+IPAddressDeny=+. Работают они следующим образом:
|
||||||
|
\begin{enumerate}
|
||||||
|
\item Если адрес источника входящего пакета, либо адрес назначения
|
||||||
|
исходящего пакета соответствует какому-либо из адресов хостов
|
||||||
|
или попадает в одну из подсетей, указанных в +IPAddressAllow=+,
|
||||||
|
пакет проходит свободно.
|
||||||
|
\item Если пакет не~соответствует +IPAddressAllow=+, но подпадает
|
||||||
|
под +IPAddressDeny=+ (аналогично, проверяются исходные адреса
|
||||||
|
входящих пакетов и адреса назначения исходящих), он блокируется.
|
||||||
|
\item Если пакет не~подпадает ни под одну из этих директив, он тоже
|
||||||
|
проходит свободно.
|
||||||
|
\end{enumerate}
|
||||||
|
Иными словами, +IPAddressDeny=+ является черным списком, но белый список
|
||||||
|
+IPAddressAllow=+ имеет над ним приоритет.
|
||||||
|
|
||||||
|
Посмотрим, как это работает. Поменяем предыдущий пример, чтобы получить
|
||||||
|
интерактивную оболочку, работающую в рамках одноразовой службы с настроенными
|
||||||
|
контрольными списками IP-адресов:
|
||||||
|
\begin{Verbatim}
|
||||||
|
# systemd-run -p IPAddressDeny=any -p IPAddressAllow=8.8.8.8 \
|
||||||
|
-p IPAddressAllow=127.0.0.0/8 -t /bin/sh
|
||||||
|
Running as unit: run-u2850.service
|
||||||
|
Press ^] three times within 1s to disconnect TTY.
|
||||||
|
sh-4.4# ping 8.8.8.8 -c1
|
||||||
|
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
|
||||||
|
64 bytes from 8.8.8.8: icmp_seq=1 ttl=59 time=27.9 ms
|
||||||
|
|
||||||
|
--- 8.8.8.8 ping statistics ---
|
||||||
|
1 packets transmitted, 1 received, 0% packet loss, time 0ms
|
||||||
|
rtt min/avg/max/mdev = 27.957/27.957/27.957/0.000 ms
|
||||||
|
sh-4.4# ping 8.8.4.4 -c1
|
||||||
|
PING 8.8.4.4 (8.8.4.4) 56(84) bytes of data.
|
||||||
|
ping: sendmsg: Operation not permitted
|
||||||
|
^C
|
||||||
|
--- 8.8.4.4 ping statistics ---
|
||||||
|
1 packets transmitted, 0 received, 100% packet loss, time 0ms
|
||||||
|
sh-4.4# ping 127.0.0.2 -c1
|
||||||
|
PING 127.0.0.1 (127.0.0.2) 56(84) bytes of data.
|
||||||
|
64 bytes from 127.0.0.2: icmp_seq=1 ttl=64 time=0.116 ms
|
||||||
|
|
||||||
|
--- 127.0.0.2 ping statistics ---
|
||||||
|
1 packets transmitted, 1 received, 0% packet loss, time 0ms
|
||||||
|
rtt min/avg/max/mdev = 0.116/0.116/0.116/0.000 ms
|
||||||
|
sh-4.4# exit
|
||||||
|
\end{Verbatim}
|
||||||
|
Мы задали +IPAddressDeny=any+, чтобы работать по схеме белого списка: для нашей
|
||||||
|
службы разрешен обмен трафика только с теми адресами, которые перечислены в
|
||||||
|
+IPAddressAllow=+. А это, в данном примере, уже знакомый нам адрес 8.8.8.8
|
||||||
|
(указанный без маски подсети, что соответствует адресу хоста, т.е. маске +/32+),
|
||||||
|
и подсеть 127.0.0.0/8. Таким образом, служба может взаимодействовать только с
|
||||||
|
одним из DNS-серверов Google и адресами обратной петли, и больше ни с кем.
|
||||||
|
Команды, запущенные в оболочке, иллюстрируют это. Сначала мы пытаемся пинговать
|
||||||
|
8.8.8.8~--- успешно. Затем, мы пробуем пропинговать 8.8.4.4 (это другой
|
||||||
|
DNS-сервер Google, не~входящий в наш белый список), и видим ошибку <<Operation
|
||||||
|
not permitted>>. Наконец, мы пингуем адрес 127.0.0.2 (принадлежащий подсети
|
||||||
|
обратной петли), и снова успешно.
|
||||||
|
|
||||||
|
Обратите внимание на специальное значение +any+, которое мы использовали в
|
||||||
|
примере выше. Оно является сокращением для <<+0.0.0.0/0 ::/0+>>, то есть
|
||||||
|
\emph{всех} возможных адресов IPv4 и IPv6. Есть и другие сокращения. Например,
|
||||||
|
вместо +127.0.0.0/8+ мы могли бы указать более понятное обозначение +localhost+,
|
||||||
|
соответствующее <<+127.0.0.0/8 ::1/128+>>, то есть IPv4 и IPv6 подсетям обратной
|
||||||
|
петли.
|
||||||
|
|
||||||
|
Возможность настраивать контрольные списки IP-адресов независимо для каждого
|
||||||
|
юнита~--- это уже неплохо. Однако, для большего удобства, существует
|
||||||
|
возможность задавать их для целых групп юнитов, или даже для всей системы. Это
|
||||||
|
можно сделать при помощи юнитов
|
||||||
|
\hreftt{https://www.freedesktop.org/software/systemd/man/systemd.slice.html}%
|
||||||
|
{.slice} (для тех, кто не~очень хорошо разбирается в systemd: slice-юниты
|
||||||
|
предоставляют возможность организовать юниты в группы для управления системными
|
||||||
|
ресурсами\footnote{Прим. перев.: Чуть более развернутое определение:
|
||||||
|
slice-юниты соответствуют <<промежуточным>> уровням иерархии контрольных групп,
|
||||||
|
которые не~содержат непосредственно процессов, а объединяют другие +.slice+,
|
||||||
|
+.service+ и +.scope+-юниты. Последние два типа юнитов уже соответствуют
|
||||||
|
контрольным группам процессов, причем группы +.service+ создаются автоматически
|
||||||
|
при запуске служб systemd, а +.scope+~--- это специально сформированные
|
||||||
|
контрольные группы процессов, запущенных другими программами, что позволяет
|
||||||
|
<<перекидывать>> отдельные процессы в другие точки иерархии контрольных
|
||||||
|
групп. В частности, процессы, созданные в рамках пользовательских сессий, а
|
||||||
|
также деревья процессов контейнеров, автоматически перемещаются из контрольных
|
||||||
|
групп породивших их служб (например, +kdm.service+) в специальные scope-юниты
|
||||||
|
(например, +session-1.scope+).}): контрольные списки адресов юнита формируются
|
||||||
|
путем объединения списков самого юнита, а также всех slice-юнитов, в которые он
|
||||||
|
вложен.
|
||||||
|
|
||||||
|
По умолчанию, все системные службы помещаются в группу
|
||||||
|
\hreftt{https://www.freedesktop.org/software/systemd/man/systemd.special.html\#system.slice}%
|
||||||
|
{system.slice}, которая, в свою очередь, входит в корневую группу
|
||||||
|
\hreftt{https://www.freedesktop.org/software/systemd/man/systemd.special.html\#-.slice}%
|
||||||
|
{-.slice}. Соответственно, при помощи любой из этих групп можно заблокировать
|
||||||
|
\emph{все} системные службы разом. Разница состоит в том, что ограничения
|
||||||
|
+system.slice+ будут применяться только к системным службам, а +-.slice+
|
||||||
|
действует сразу на все процессы системы, включая не~только службы, но и процессы
|
||||||
|
пользовательских сеансов (объединенные в группу +user.slice+, которая находится
|
||||||
|
в корневой группе).
|
||||||
|
|
||||||
|
Воспользуемся этим:
|
||||||
|
\begin{Verbatim}
|
||||||
|
# systemctl set-property system.slice IPAddressDeny=any IPAddressAllow=localhost
|
||||||
|
# systemctl set-property apache.service IPAddressAllow=10.0.0.0/8
|
||||||
|
\end{Verbatim}
|
||||||
|
Две приведенные команды имеют очень мощный эффект: сначала мы отключаем
|
||||||
|
взаимодействие через IP (кроме обратной петли) для всех служб системы, а затем
|
||||||
|
добавляем белый список 10.0.0.0/8 (скажем, это локальная сеть вашей компании)
|
||||||
|
только для службы веб-сервера Apache.
|
||||||
|
|
||||||
|
\subsectiona{Практическое применение}
|
||||||
|
|
||||||
|
Вот несколько идей по практическому применению рассмотренных возможностей:
|
||||||
|
\begin{enumerate}
|
||||||
|
\item Механизм контрольных списков IP адресов можно рассматривать как
|
||||||
|
современную альтернативу классической технологии
|
||||||
|
\href{https://en.wikipedia.org/wiki/TCP_Wrapper}{TCP Wrapper}.
|
||||||
|
Однако, в отличие от нее, контрольные списки применяются сразу
|
||||||
|
ко всем IP-сокетам службы, и не~требуют никакой поддержки со
|
||||||
|
стороны приложения. С другой стороны, TCP Wrapper предоставляет
|
||||||
|
ряд опций, которые остутствуют в нашей схеме, в частности,
|
||||||
|
возможность указания DNS-имен вместо IP-адресов (лично я считаю
|
||||||
|
это весьма сомнительной опцией~--- выполнять сетевые операции
|
||||||
|
(разрешение имен), причем незащищенные, для того, чтобы
|
||||||
|
ограничить работу сети).
|
||||||
|
\item В некоторых аспектах наши механизмы могут заменить или хотя бы
|
||||||
|
дополнить классический брандмауэр Linux~---
|
||||||
|
NetFilter/+iptables+. На текущий момент, контрольные списки
|
||||||
|
IP-адресов systemd предоставляют гораздо меньше возможностей,
|
||||||
|
чем NetFilter, однако имеют перед ним серьезное преимущество:
|
||||||
|
они работают на уровне \emph{приложений}, а не~абстрактных
|
||||||
|
TCP/UDP-портов. Классические брандмауэры, в частности,
|
||||||
|
NetFilter, вынуждены делать предположение о принадлежности
|
||||||
|
пакетов тем или иным службам, основываясь только на номерах
|
||||||
|
портов\footnote{Прим. перев.: В ядрах Linux 2.4-2.6
|
||||||
|
существовала теоретическая возможность выборки пакетов по
|
||||||
|
идентификатору процесса (+-m owner --pid-owner PID+), однако она
|
||||||
|
была удалена в выпуске 2.6.14. Много лет спустя, в ядре 3.14 был
|
||||||
|
добавлен модуль netfilter +xt_cgroup+, позволяющий выбирать
|
||||||
|
пакеты по индексу класса, присвоенного cgroup-контроллером
|
||||||
|
+net_class+. Кстати, поддержка соответствующей опции
|
||||||
|
+NetClass=+ существовала в systemd с 227 по 229 версию, но была
|
||||||
|
удалена при миграции на
|
||||||
|
\href{https://www.kernel.org/doc/Documentation/cgroup-v2.txt}%
|
||||||
|
{cgroup v2}, где такого контроллера уже нет, зато есть
|
||||||
|
вышеописанный механизм контрольных списков.}, однако на практике
|
||||||
|
порты часто выбираются динамически. Например, клиенты
|
||||||
|
BitTorrent для передачи данных могут использовать любые удобные
|
||||||
|
им порты, что крайне затрудняет корректную выборку таких
|
||||||
|
пакетов. В случае с контрольными списками все просто: достаточно
|
||||||
|
настроить разрешения для юнита службы BitTorrent, и дело в
|
||||||
|
шляпе.
|
||||||
|
|
||||||
|
Замечу что, по большей части, сравнение NetFilter и контрольных
|
||||||
|
списков systemd~--- это сравнение теплого с мягким. Контрольные
|
||||||
|
списки ориентированы исключительно на конечные машины (клиенты и
|
||||||
|
серверы), так как работают только с локальными службами. В то
|
||||||
|
время как NetFilter отлично работает на промежуточных
|
||||||
|
маршрутизаторах, передающих чистый IP-трафик, никак
|
||||||
|
не~привязанный к их локальным процессам.
|
||||||
|
\item Контрольные списки предоставляют простой способ обеспечить
|
||||||
|
безопасность служб <<из коробки>>. Например, если вы
|
||||||
|
сопровождаете пакет службы, которая не~должна требовать доступа
|
||||||
|
к сети, добавьте в ее юнит-файл +IPAddressDeny=any+ (и, при
|
||||||
|
необходимости, +IPAddressAllow=localhost+), и она будет помещена
|
||||||
|
в песочницу, из которой не~сможет сбежать. В systemd подобные
|
||||||
|
ограничения уже введены по умолчанию для целого ряда служб,
|
||||||
|
например, для системного журнала
|
||||||
|
+systemd-journald.service+\footnote{Прим. перев.: Знаменитый
|
||||||
|
HTTP-сервер вынесен в отдельную службу
|
||||||
|
\href{https://www.freedesktop.org/software/systemd/man/systemd-journal-gatewayd.html}%
|
||||||
|
{systemd-journal-gatewayd(8)}, которая
|
||||||
|
\hreftt{https://github.com/systemd/systemd/blob/master/system-preset/90-systemd.preset}%
|
||||||
|
{отключена} по умолчанию, а в ряде дистрибутивов вообще
|
||||||
|
поставляется в отдельном опциональном пакете (в частности,
|
||||||
|
RHEL/CentOS, Debian/Ubuntu).}, менеджера логинов
|
||||||
|
+systemd-logind+ и обработчика дампов памяти
|
||||||
|
+systemd-coredump@.service+, так как мы знаем, что этим службам
|
||||||
|
ни~при каких условиях не~нужна сеть.
|
||||||
|
\item Механизм контрольных списков IP-адресов можно сочетать с
|
||||||
|
одноразовыми службами, что позволяет быстро и эффективно
|
||||||
|
изолировать произвольные команды, и даже включать их в конвееры.
|
||||||
|
Предположим, что мы не~доверяем нашему бинарнику
|
||||||
|
\hreftt{https://linux.die.net/man/1/curl}{curl} (может быть, он
|
||||||
|
модифицирован хакером, и обращается на подконтрольные ему
|
||||||
|
серверы?), но все равно хотим использовать его для загрузки
|
||||||
|
\href{http://0pointer.de/public/casync-kinvolk2017.pdf}{презентации
|
||||||
|
с моего последнего доклада по casync}, чтобы распечатать их, и
|
||||||
|
при этом быть уверенными, что он не~будет связываться ни~с кем,
|
||||||
|
кроме нужного нам сервера (а чтобы было еще интереснее и
|
||||||
|
безопаснее, включим описанный в предыдущей главе механизм
|
||||||
|
динамических пользователей):
|
||||||
|
\begin{Verbatim}
|
||||||
|
# systemd-resolve 0pointer.de
|
||||||
|
0pointer.de: 85.214.157.71
|
||||||
|
2a01:238:43ed:c300:10c3:bcf3:3266:da74
|
||||||
|
-- Information acquired via protocol DNS in 2.8ms.
|
||||||
|
-- Data is authenticated: no
|
||||||
|
# systemd-run --pipe -p IPAddressDeny=any \
|
||||||
|
-p IPAddressAllow=85.214.157.71 \
|
||||||
|
-p IPAddressAllow=2a01:238:43ed:c300:10c3:bcf3:3266:da74 \
|
||||||
|
-p DynamicUser=yes \
|
||||||
|
curl http://0pointer.de/public/casync-kinvolk2017.pdf | lp
|
||||||
|
\end{Verbatim}
|
||||||
|
\end{enumerate}
|
||||||
|
|
||||||
|
Как и в прошлой главе, в силу масштабности обсуждаемой концепции, приведенный
|
||||||
|
список применений не~претендует на полноту, а лишь является затравкой для вашего
|
||||||
|
воображения.
|
||||||
|
|
||||||
|
\subsectiona{Рекомендации сопровождающим пакетов}
|
||||||
|
|
||||||
|
Механизмы учета и контроля IP-трафика ориентированы прежде всего на системных
|
||||||
|
администраторов, а не~разработчиков. Тем не~менее, как я уже заметил выше, для
|
||||||
|
служб, которые никак и никогда не~требуют сети, целесообразно добавлять
|
||||||
|
настройку +IPAddressDeny=any+ (и опционально +IPAddressAllow=localhost+), чтобы
|
||||||
|
повысить безопасность системы сразу <<из коробки>>.
|
||||||
|
|
||||||
|
Для специализированных дистрибутивов, ориентированных на максимальную
|
||||||
|
безопасность, можно предложить более радикальный подход: добавить
|
||||||
|
+IPAddressDeny=any+ сразу в +-.slice+ или +system.slice+, чтобы администратор,
|
||||||
|
когда ему нужно выпустить в сеть какую-либо службу, вручную командовал
|
||||||
|
+systemctl set-property ... IPAddressAllow=...+. Разумеется, это вариант для
|
||||||
|
тех дистрибутивов, которые не~боятся ломать обратную совместимость.
|
||||||
|
|
||||||
|
\subsectiona{Дополнительные замечания}
|
||||||
|
|
||||||
|
И еще несколько замечаний:
|
||||||
|
\begin{enumerate}
|
||||||
|
\item Описанные механизмы учета и фильтрации IP-трафика можно совмещать
|
||||||
|
с сокет-активацией. При этом, целесообразно настраивать их
|
||||||
|
и для сокета, и для активируемой им службы, так как это
|
||||||
|
разные юниты с независимыми настройками. Обратите внимание, что
|
||||||
|
учет и фильтрация трафика, настроенные для сокет-юнита,
|
||||||
|
применяются для всех сокетов, созданных в рамках этого юнита,
|
||||||
|
включая переданные активированным службам. Как следствие, трафик
|
||||||
|
учитывается именно для юнита сокета, а не~службы. Тот факт, что
|
||||||
|
для сокетов, созданных под эгидой сокет-юнита (т.е. при
|
||||||
|
сокет-активации), и для сокетов, созданных из кода службы,
|
||||||
|
используются \emph{разные} контрольные списки, открывает весьма
|
||||||
|
интересные возможности. Например, можно настроить относительно
|
||||||
|
свободный доступ для сокета, при этом полностью запретив
|
||||||
|
IP-трафик для активируемых им служб~--- в результате эти службы
|
||||||
|
смогут взаимодейстовать с внешним миром только через переданный
|
||||||
|
им при активации сокет.
|
||||||
|
\item Учет и фильтрация IP-трафика работают только с IP-сокетами. В
|
||||||
|
частности, сокеты типа +AF_PACKET+ (так называемые <<сырые
|
||||||
|
сокеты>>) под эти ограничения не~подпадают\footnote{Прим.
|
||||||
|
перев.: Они также <<обходят>> и netfilter~--- именно поэтому
|
||||||
|
бесполезно пытаться фильтровать трафик DHCP-клиента с той же
|
||||||
|
машины.}. Поэтому целесообразно дополнять контрольные списки
|
||||||
|
опцией
|
||||||
|
\hreftt{https://www.freedesktop.org/software/systemd/man/systemd.exec.html\#RestrictAddressFamilies=}%
|
||||||
|
{RestrictAddressFamilies=AF\_UNIX AF\_INET AF\_INET6}.
|
||||||
|
\item Вы также можете поинтересоваться, может ли журнальная запись о
|
||||||
|
потребленных ресурсах, а также +systemd-run+ с ключом +--wait+
|
||||||
|
показывать статистику по другим видам ресурсов?
|
||||||
|
Ответ утвердительный: например, если вы зададите для юнита
|
||||||
|
\hreftt{https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html\#CPUAccounting=}%
|
||||||
|
{CPUAccounting=yes}, то журнальная запись и +systemd-run+ будут
|
||||||
|
показывать статистику по потребленному процессорному времени. В
|
||||||
|
ближайшее время мы планируем добавить аналогичную поддержку для
|
||||||
|
\hreftt{https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html\#IOAccounting=}%
|
||||||
|
{IOAccounting=}.
|
||||||
|
\item Обратите внимание, что учет и фильтрация IP-трафика сами могут
|
||||||
|
потреблять определенные ресурсы. Чтобы эти функции работали,
|
||||||
|
systemd вставляет в путь прохождения IP-трафика специальную
|
||||||
|
eBPF-программу. Однако, в последних версиях ядра выполнение eBPF
|
||||||
|
очень неплохо оптимизировано, и работа по оптимизации
|
||||||
|
продолжается, так что серьезных проблем с производительностью
|
||||||
|
ожидать не~стоит.
|
||||||
|
\item Учет трафика не~является иерархическим, то есть, счетчики
|
||||||
|
slice-юнита не~суммируют результаты вложенных в него юнитов. Это
|
||||||
|
одна из задач, которую мы намерены решить, однако она требует
|
||||||
|
доработки кода ядра.
|
||||||
|
\item У вас может возникнуть вопрос~--- как соотносятся механизмы
|
||||||
|
\hreftt{https://www.freedesktop.org/software/systemd/man/systemd.exec.html\#PrivateNetwork=}%
|
||||||
|
{PrivateNetwork=yes} и +IPAccessDeny=any+? На первый взгляд, они
|
||||||
|
работают одинаково: блокируют доступ к сети для службы. Однако,
|
||||||
|
при более пристальном рассмотрении обнаруживается ряд отличий.
|
||||||
|
Механизм +PrivateNetwork=+ реализован на основе сетевых
|
||||||
|
пространств имен ядра Linux. Он полностью изолирует от хоста
|
||||||
|
все сетевые операции службы~--- не~только обычные IP-сокеты, но
|
||||||
|
и другие виды сетевого взаимодействия (в частности, сырые
|
||||||
|
сокеты). Для этого он создает службе отдельное сетевое
|
||||||
|
пространство имен, в котором она может общаться только сама
|
||||||
|
с собой (через свой личный интерфейс обратной петли). Однако,
|
||||||
|
при использовании опции
|
||||||
|
\hreftt{https://www.freedesktop.org/software/systemd/man/systemd.unit.html\#JoinsNamespaceOf=}%
|
||||||
|
{JoinsNamespaceOf=} в ту же песочницу могут быть помещены другие
|
||||||
|
службы~--- в результате, службы из одной песочницы смогут
|
||||||
|
взаимодействовать между собой, но при этом будут изолированы от
|
||||||
|
остального мира. С другой стороны, +IPAddressAllow=+ и
|
||||||
|
+IPAccessDeny=+ действуют не~столь жестко. Прежде всего, они
|
||||||
|
работают только с IP-сокетами и IP-адресами. Кроме того, служба
|
||||||
|
с выключенным режимом +PrivateNetwork=+, но включенным
|
||||||
|
+IPAccessDeny=any+, все равно способна прочитать список сетевых
|
||||||
|
интерфейсов хоста и узнать присвоенные им адреса, хотя и
|
||||||
|
не~сможет пересылать данные по протоколу IP. В то же время, с
|
||||||
|
включенным режимом +PrivateNetwork=+ служба может видеть только
|
||||||
|
свой личный интерфейс +lo+. Короче: в зависимости от конкретной
|
||||||
|
ситуации, для изоляции службы можно использовать тот или иной
|
||||||
|
вариант, или оба сразу, или вообще ни~одного. Когда это
|
||||||
|
возможно, для максимальной безопасности лучше использовать обе
|
||||||
|
эти защиты, как это делаем мы для всех основных служб из состава
|
||||||
|
systemd.
|
||||||
|
\end{enumerate}
|
||||||
|
|
||||||
|
На этом пока все.
|
||||||
|
|
||||||
|
\newpage
|
||||||
\appendix
|
\appendix
|
||||||
|
|
||||||
\section{FAQ (часто задаваемые вопросы)\sfnote{Перевод статьи
|
\section{FAQ (часто задаваемые вопросы)\sfnote{Перевод статьи
|
||||||
|
|||||||
Reference in New Issue
Block a user