<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Dmitry Konishchev&#39;s small blog</title>
    <link>https://konishchev.ru/</link>
    <description>Recent content on Dmitry Konishchev&#39;s small blog</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>ru</language>
    <lastBuildDate>Mon, 05 Jan 2026 15:37:29 +0300</lastBuildDate>
    <atom:link href="https://konishchev.ru/rss.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>nftables и TCP Explicit Congestion Notification – или как роботы Яндекса внезапно потеряли доступ к моему блогу</title>
      <link>https://konishchev.ru/posts/nftables-tcp-ecn/</link>
      <pubDate>Mon, 05 Jan 2026 20:56:25 +0300</pubDate>
      
      <guid>https://konishchev.ru/posts/nftables-tcp-ecn/</guid>
      <description>&lt;p&gt;Пару недель назад мне пришло письмо от &lt;a href=&#34;https://webmaster.yandex.ru/&#34;&gt;Яндекс Вебмастера&lt;/a&gt; с уведомлением о том, что мой блог стал недоступен. Каких-либо подробностей ни в письме, ни в UI в таких случаях, к сожалению, не предоставляется, а инструменты анализа robots.txt и sitemap.xml и вовсе вводили в заблуждение словами &amp;ldquo;Server returns HTTP code 502 (expected code 200)&amp;rdquo; – хотя я явно не видел никаких обращений роботов в логах web-сервера. При этом, судя по тем же логам, пользователи как приходили раньше, так и продолжали приходить + &lt;a href=&#34;https://search.google.com/search-console&#34;&gt;Google Search Console&lt;/a&gt; тоже не видел никаких проблем.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Пару недель назад мне пришло письмо от <a href="https://webmaster.yandex.ru/">Яндекс Вебмастера</a> с уведомлением о том, что мой блог стал недоступен. Каких-либо подробностей ни в письме, ни в UI в таких случаях, к сожалению, не предоставляется, а инструменты анализа robots.txt и sitemap.xml и вовсе вводили в заблуждение словами &ldquo;Server returns HTTP code 502 (expected code 200)&rdquo; – хотя я явно не видел никаких обращений роботов в логах web-сервера. При этом, судя по тем же логам, пользователи как приходили раньше, так и продолжали приходить + <a href="https://search.google.com/search-console">Google Search Console</a> тоже не видел никаких проблем.</p>
<p>Поддержка Вебмастера ответила, что запросы роботов блокируются с моей стороны и посоветовала обратиться к хостинг-провайдеру.</p>
<h2 id="поиск-причины">Поиск причины</h2>
<p>Первая мысль – &ldquo;неужели где-то перемудрил с <a href="https://github.com/fail2ban/fail2ban">fail2ban</a>&lsquo;ом?&rdquo;, но его отключение ни к чему не привело. К счастью, поддержка подсказала номер <a href="https://en.wikipedia.org/wiki/Autonomous_system_(Internet)">AS</a>, из которой могут исходить запросы роботов Яндекса – <code>AS13238</code>, что сильно облегчило поиск причины на своей стороне.</p>
<p>Получил список сетей Яндекса с помощью <code>bgpq3 -4 -F '%n/%l, ' AS13238</code> и добавил следующее правило в nftables поближе к самому началу обработки всех новых соединений:</p>
<pre tabindex="0"><code>ip saddr {5.45.192.0/18, 5.45.202.0/24, 5.45.205.0/24, 5.45.215.0/24, 5.255.192.0/18, 5.255.197.0/24, 5.255.255.0/24, 37.9.64.0/18, 37.9.64.0/24, 37.9.87.0/24, 37.9.112.0/24, 37.140.128.0/18, 77.88.0.0/18, 77.88.8.0/24, 77.88.44.0/24, 77.88.55.0/24, 84.252.160.0/19, 87.250.224.0/19, 87.250.247.0/24, 90.156.179.0/24, 90.156.180.0/24, 90.156.181.0/24, 90.156.184.0/24, 90.156.185.0/24, 92.255.112.0/20, 93.158.128.0/18, 95.108.128.0/17, 141.8.128.0/18, 178.154.128.0/19, 178.154.131.0/24, 178.154.160.0/19, 185.32.187.0/24, 213.180.192.0/19, 213.180.199.0/24} meta nftrace set 1
</code></pre><p>Запустил <code>nft monitor trace</code> и, воспользовавшись тулзой по анализу robots.txt, тут же получил следующее:</p>
<pre tabindex="0"><code>trace id f15d4066 inet mangle PREROUTING packet: iif &#34;isp&#34; ether saddr 3c:c7:86:12:89:7a ether daddr 08:f1:db:e6:ac:3b ip saddr 5.255.253.45 ip daddr 10.217.4.5 ip dscp cs0 ip ecn ect0 ip ttl 50 ip id 3342 ip protocol tcp ip length 60 tcp sport 61996 tcp dport 443 tcp flags == 0xc2 tcp window 42300
...
trace id f15d4066 inet filter check_packets rule ct state new tcp flags != syn counter packets 0 bytes 0 goto bad_tcp_new_packet (verdict goto bad_tcp_new_packet)
trace id f15d4066 inet filter bad_tcp_new_packet rule meta l4proto tcp counter packets 0 bytes 0 reject with tcp reset comment &#34;Invalid TCP packets&#34; (verdict drop)
</code></pre><p>Ого, это интересно. Мы что-то дропнули вот тут:</p>
<pre tabindex="0"><code># New TCP connections must be started with SYN packets.
#
# NEW but not SYN is the only invalid TCP flag not covered by the
# INVALID state. The reason is because they are rarely malicious packets,
# and they should not just be dropped, but might be an error or attack.
#
# For example, this may be just connections forgotten by conntrack module.
ct state new tcp flags != syn counter goto bad_tcp_new_packet
</code></pre><p>Пробую убрать это правило – и теперь действительно всё работает. Ещё интереснее. :)</p>
<h2 id="распространённая-ошибка">Распространённая ошибка</h2>
<p>Итак – проблема действительно на моей стороне. И это замечательно – меньше всего хотелось бы дебажить подобные вещи через поддержку своего провайдера.</p>
<p>Но вот только пока не очень понятно, в чём именно проблема: это ведь довольно стандартная рекомендация – reject&rsquo;ить все новые TCP-соединения, у которых первый пакет не является SYN-пакетом. Да и что же тогда нам Яндексовый робот такое прислал, если это не SYN-пакет?..</p>
<p>Смотрим более внимательно в нашу трассировку – и видим там следующие TCP-флаги: <code>tcp flags == 0xc2</code>. <code>0xc2</code> – это <code>11000010</code>, что, исходя из <a href="https://en.wikipedia.org/wiki/Transmission_Control_Protocol">структуры TCP-пакета</a>, является <code>SYN + ECE + CWR</code>. Отлично, теперь всё понятно – это вполне себе SYN-пакет, но только какой-то необычный, а правило у нас написано довольно тупо (<code>tcp flags != syn</code>) и совершенно на такой случай не рассчитано. При этом, если для iptables обычно даётся рекомендация вида <code>iptables -t mangle -A PREROUTING -p tcp ! --syn -m conntrack --ctstate NEW -j DROP</code>, где <code>--syn</code> на самом деле является шорткатом для <code>--tcp-flags SYN,RST,ACK,FIN SYN</code>, то для nftables (помимо в целом в разы более куцей информации по его конфигурации) даже <a href="https://wiki.nftables.org/wiki-nftables/index.php/Matching_packet_headers">официальная документация</a> предлагает <code>nft add rule filter input tcp flags != syn counter</code> в качестве примера правила &ldquo;to count packets that are not SYN ones&rdquo; (как и во многих статьях в блогах, в которых правило представлено ровно в том виде, в котором оно используется у меня) – в результате чего возникает ощущение, что я могу быть далеко не единственным, кто столкнётся с этой проблемой (и именно это побудило меня написать данную статью).</p>
<p>Поэтому <code>tcp flags != syn</code> правильнее заменить на <code>tcp flags &amp; (syn|ack|rst|fin) != syn</code>.</p>
<h2 id="explicit-congestion-notification">Explicit Congestion Notification</h2>
<p>А что же всё-таки за такой необычный пакет к нам пришёл? <code>ECE</code> и <code>CWR</code>-флаги ведут нас к <a href="https://en.wikipedia.org/wiki/Explicit_Congestion_Notification">Explicit Congestion Notification</a> – хм, интересно: про <a href="https://en.wikipedia.org/wiki/TCP_congestion_control">TCP congestion control</a> я в курсе, а вот Explicit Congestion Notification – это для меня что-то новое. Отлично – значит день уже прожит не зря. :)</p>
<p>Вкратце, суть его заключается в следующем: если традиционно в TCP текущая загруженность канала определялась неявно через отслеживание количества пакетов, которые потерялись по дороге к адресату (были дропнуты роутером где-то в середине пути), то ECN добавляет возможность роутеру специальным образом маркировать пакеты, тем самым заранее уведомляя участников TCP-соединения о том, что канал перегружен, и он скоро начнёт дропать пакеты. Но, само собой, рекомендую почитать <a href="https://en.wikipedia.org/wiki/Explicit_Congestion_Notification">более полное описание</a>.</p>
<p>Поэтому, видимо, дело в том, что недавно в Яндексе поменялась конфигурация серверов, на которых располагаются поисковые боты, и они стали устанавливать соединения с включенным ECN.</p>
<h2 id="в-итоге">В итоге</h2>
<p>Если вы, как и во многих руководствах по nftables, дропаете/reject&rsquo;ите пакеты по правилу <code>ct state new tcp flags != syn</code>, то его необходимо заменить на <code>ct state new tcp flags &amp; (syn|ack|rst|fin) != syn</code> – иначе когда-нибудь эта довольно подлая бомба замедленного действия сработает и у вас.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>WhatsApp сканирует сеть?</title>
      <link>https://konishchev.ru/posts/whatsapp-scans-network/</link>
      <pubDate>Sun, 07 Sep 2025 12:55:49 +0300</pubDate>
      
      <guid>https://konishchev.ru/posts/whatsapp-scans-network/</guid>
      <description>&lt;p&gt;Совершенно случайно наткнулся на интересное:&lt;/p&gt;
&lt;p&gt;У меня дома стриггерился алерт: мой домашний сервачок (он же роутер) помимо всего прочего отслеживает количество уникальных от-forward&amp;rsquo;енных &lt;code&gt;$src_ip + $dst_ip + $dst_port&lt;/code&gt; – и алертит, когда их количество превышает некоторый порог.&lt;/p&gt;
&lt;p&gt;И вот за последние сутки с моего телефона + телефона жены 2560 + 4082 уникальных пар &lt;code&gt;$dst_ip + $dst_port&lt;/code&gt; (где &lt;code&gt;602x22&lt;/code&gt; ниже – это соединения на 22 порт на 602 разных IP-адреса):&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;kate-mobile.lan (4082 IP+port pairs): 3117 TCP (1534x443, 602x22, 261x80, 237x554, 220x53, 29x23, 28x983, 21x553, 20x179, 12x1443, 9x5222, 6x5228, 4x4460, 4x21, 2x571, 2x9243, 2x240, 2x383, 2x185, 2x260, 2x299, 2x237, 2x336, 2x131, 2x512, 1x464, 1x734, 1x4416, 1x371, 1x10, 1x863, 1x895, 1x759, 1x815, 1x178, 1x830, 1x271, 1x838, 1x707, 1x629, 1x174, 1x1003, 1x894, 1x3237, 1x887, 1x962, 1x603, 1x855, 1x241, 1x494, 1x540, 1x181, 1x352, 1x454, 1x373, 1x654, 1x56, 1x646, 1x175, 1x876, 1x810, 1x556, 1x395, 1x483, 1x697, 1x212, 1x34, 1x588, 1x348, 1x605, 1x680, 1x460, 1x401, 1x224, 1x143, 1x161, 1x104, 1x655, 1x872, 1x521, 1x459, 1x911, 1x705, 1x317, 1x377, 1x807, 1x323, 1x893, 1x866, 1x142, 1x1001, 1x170, 1x920, 1x843, 1x209, 1x463, 1x156, 1x569, 1x952, 1x701, 1x184, 1x597, 1x389, 1x647, 1x8543, 1x487, 1x624, 1x537, 1x814, 1x259, 1x578, 1x26, 1x904, 1x751, 1x652, 1x795, 1x234, 1x671, 1x45, 1x4477, 1x307, 1x635, 1x651, 1x227, 1x806, 1x752, 1x203, 1x220, 1x582, 1x568, 1x153, 1x844, 1x402), 965 UDP (379x443, 278x53, 116x554, 83x123, 38x22, 22x23, 7x2002, 6x983, 4x179, 2x4123, 2x512, 2x21, 2x553, 1x363, 1x652, 1x654, 1x1003, 1x299, 1x307, 1x377, 1x680, 1x807, 1x804, 1x966, 1x685, 1x240, 1x463, 1x655, 1x806, 1x45, 1x383, 1x336, 1x153, 1x260, 1x28, 1x241, 1x603)
mobile.lan (2560 IP+port pairs): 1899 TCP (814x443, 405x22, 171x554, 168x80, 160x53, 23x983, 18x179, 18x1443, 15x23, 10x553, 9x5222, 6x21, 5x7275, 3x5228, 2x19302, 1x759, 1x37, 1x629, 1x685, 1x581, 1x582, 1x10000, 1x142, 1x250, 1x846, 1x125, 1x872, 1x657, 1x8543, 1x604, 1x90, 1x727, 1x567, 1x911, 1x739, 1x810, 1x4477, 1x866, 1x26, 1x491, 1x10, 1x156, 1x626, 1x178, 1x422, 1x977, 1x155, 1x12, 1x402, 1x683, 1x21007, 1x306, 1x595, 1x184, 1x4416, 1x472, 1x14, 1x904, 1x166, 1x165, 1x753, 1x988, 1x4434, 1x11, 1x28, 1x317, 1x622, 1x535, 1x718, 1x686, 1x637, 1x207, 1x244, 1x153, 1x7000, 1x8443, 1x966, 1x383, 1x5223, 1x985, 1x161, 1x994, 1x395, 1x898, 1x39, 1x592, 1x6447), 661 UDP (307x443, 168x53, 81x554, 36x123, 24x22, 10x23, 6x983, 6x179, 4x19302, 3x553, 3x21, 1x153, 1x685, 1x626, 1x155, 1x592, 1x19000, 1x491, 1x306, 1x472, 1x125, 1x8443, 1x28, 1x966)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Поставил себе &lt;a href=&#34;https://play.google.com/store/apps/details?id=com.emanuelef.remote_capture&#34;&gt;PCAPdroid&lt;/a&gt; на телефон, и выяснилось, что WhatsApp (я им совсем не пользуюсь – установлен по необходимости):&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Совершенно случайно наткнулся на интересное:</p>
<p>У меня дома стриггерился алерт: мой домашний сервачок (он же роутер) помимо всего прочего отслеживает количество уникальных от-forward&rsquo;енных <code>$src_ip + $dst_ip + $dst_port</code> – и алертит, когда их количество превышает некоторый порог.</p>
<p>И вот за последние сутки с моего телефона + телефона жены 2560 + 4082 уникальных пар <code>$dst_ip + $dst_port</code> (где <code>602x22</code> ниже – это соединения на 22 порт на 602 разных IP-адреса):</p>
<pre tabindex="0"><code>kate-mobile.lan (4082 IP+port pairs): 3117 TCP (1534x443, 602x22, 261x80, 237x554, 220x53, 29x23, 28x983, 21x553, 20x179, 12x1443, 9x5222, 6x5228, 4x4460, 4x21, 2x571, 2x9243, 2x240, 2x383, 2x185, 2x260, 2x299, 2x237, 2x336, 2x131, 2x512, 1x464, 1x734, 1x4416, 1x371, 1x10, 1x863, 1x895, 1x759, 1x815, 1x178, 1x830, 1x271, 1x838, 1x707, 1x629, 1x174, 1x1003, 1x894, 1x3237, 1x887, 1x962, 1x603, 1x855, 1x241, 1x494, 1x540, 1x181, 1x352, 1x454, 1x373, 1x654, 1x56, 1x646, 1x175, 1x876, 1x810, 1x556, 1x395, 1x483, 1x697, 1x212, 1x34, 1x588, 1x348, 1x605, 1x680, 1x460, 1x401, 1x224, 1x143, 1x161, 1x104, 1x655, 1x872, 1x521, 1x459, 1x911, 1x705, 1x317, 1x377, 1x807, 1x323, 1x893, 1x866, 1x142, 1x1001, 1x170, 1x920, 1x843, 1x209, 1x463, 1x156, 1x569, 1x952, 1x701, 1x184, 1x597, 1x389, 1x647, 1x8543, 1x487, 1x624, 1x537, 1x814, 1x259, 1x578, 1x26, 1x904, 1x751, 1x652, 1x795, 1x234, 1x671, 1x45, 1x4477, 1x307, 1x635, 1x651, 1x227, 1x806, 1x752, 1x203, 1x220, 1x582, 1x568, 1x153, 1x844, 1x402), 965 UDP (379x443, 278x53, 116x554, 83x123, 38x22, 22x23, 7x2002, 6x983, 4x179, 2x4123, 2x512, 2x21, 2x553, 1x363, 1x652, 1x654, 1x1003, 1x299, 1x307, 1x377, 1x680, 1x807, 1x804, 1x966, 1x685, 1x240, 1x463, 1x655, 1x806, 1x45, 1x383, 1x336, 1x153, 1x260, 1x28, 1x241, 1x603)
mobile.lan (2560 IP+port pairs): 1899 TCP (814x443, 405x22, 171x554, 168x80, 160x53, 23x983, 18x179, 18x1443, 15x23, 10x553, 9x5222, 6x21, 5x7275, 3x5228, 2x19302, 1x759, 1x37, 1x629, 1x685, 1x581, 1x582, 1x10000, 1x142, 1x250, 1x846, 1x125, 1x872, 1x657, 1x8543, 1x604, 1x90, 1x727, 1x567, 1x911, 1x739, 1x810, 1x4477, 1x866, 1x26, 1x491, 1x10, 1x156, 1x626, 1x178, 1x422, 1x977, 1x155, 1x12, 1x402, 1x683, 1x21007, 1x306, 1x595, 1x184, 1x4416, 1x472, 1x14, 1x904, 1x166, 1x165, 1x753, 1x988, 1x4434, 1x11, 1x28, 1x317, 1x622, 1x535, 1x718, 1x686, 1x637, 1x207, 1x244, 1x153, 1x7000, 1x8443, 1x966, 1x383, 1x5223, 1x985, 1x161, 1x994, 1x395, 1x898, 1x39, 1x592, 1x6447), 661 UDP (307x443, 168x53, 81x554, 36x123, 24x22, 10x23, 6x983, 6x179, 4x19302, 3x553, 3x21, 1x153, 1x685, 1x626, 1x155, 1x592, 1x19000, 1x491, 1x306, 1x472, 1x125, 1x8443, 1x28, 1x966)
</code></pre><p>Поставил себе <a href="https://play.google.com/store/apps/details?id=com.emanuelef.remote_capture">PCAPdroid</a> на телефон, и выяснилось, что WhatsApp (я им совсем не пользуюсь – установлен по необходимости):</p>
<ul>
<li>За последний месяц съел 23 MB Wi-Fi трафика.</li>
<li>За <strong>сегодняшний день</strong> съел 92 MB Wi-Fi трафика.</li>
<li>Постоянно открывает соединения на разные IP и всякие мутные порты (ssh, ntp, ftp).</li>
</ul>
<p>Хотелось бы верить, что это какая-то очередная защита от блокировок или вроде того, но, учитывая недавние истории про <a href="https://habr.com/ru/articles/915732/">слежку за пользователями на Android</a>, что-то не очень верится. :)</p>
<p>Как-то более глубоко исследовать эту ситуацию, честно говоря, нет желания (да и наверняка он подобными сканированиями занимается только изредка, чтобы не привлекать к себе лишнего внимания) – поэтому всё выше написанное исключительно JFYI, без каких-либо интересных подробностей.</p>
<p>P.S.: Большая просьба не воспринимать это как очередную рекламу в пользу всем известного мессенджера, который сейчас активно продвигается – все совпадения случайны.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Настраиваем VS Code для работы с кодом ядра Linux</title>
      <link>https://konishchev.ru/posts/linux-kernel-vscode/</link>
      <pubDate>Tue, 22 Jul 2025 09:30:09 +0300</pubDate>
      
      <guid>https://konishchev.ru/posts/linux-kernel-vscode/</guid>
      <description>&lt;p&gt;Мне никогда не приходилось заниматься разработкой ядра, но вот в последнее время всё чаще возникает необходимость заглянуть в его исходники, чтобы уточнить для себя, как именно работает тот или иной системный вызов или файл в sysfs/proc. И каждый раз это было жутко неудобно, т. к. IDE сходу не могло адекватно проиндексировать код ядра, чтобы можно было более или менее сносно прыгать по функциям. Поэтому решил потратить какое-то время и разобраться, как можно улучшить эту ситуацию.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Мне никогда не приходилось заниматься разработкой ядра, но вот в последнее время всё чаще возникает необходимость заглянуть в его исходники, чтобы уточнить для себя, как именно работает тот или иной системный вызов или файл в sysfs/proc. И каждый раз это было жутко неудобно, т. к. IDE сходу не могло адекватно проиндексировать код ядра, чтобы можно было более или менее сносно прыгать по функциям. Поэтому решил потратить какое-то время и разобраться, как можно улучшить эту ситуацию.</p>
<h2 id="cc-extension">C/C++ extension</h2>
<p>Если погуглить, то наиболее частая рекомендация сводится к использованию VS Code с расширениями <a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools">C/C++</a> и <a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.makefile-tools">Makefile Tools</a> с примерно следующим <code>.vscode/c_cpp_properties.json</code>:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    &#34;configurations&#34;: [
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            &#34;name&#34;: <span style="color:#a31515">&#34;Linux&#34;</span>,
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            &#34;includePath&#34;: [
</span></span><span style="display:flex;"><span>                <span style="color:#a31515">&#34;${workspaceFolder}/arch/x86/include/generated&#34;</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#a31515">&#34;${workspaceFolder}/arch/x86/include&#34;</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#a31515">&#34;${workspaceFolder}/include&#34;</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#a31515">&#34;${workspaceFolder}/**&#34;</span>
</span></span><span style="display:flex;"><span>            ],
</span></span><span style="display:flex;"><span>            &#34;forcedInclude&#34;: [
</span></span><span style="display:flex;"><span>                <span style="color:#a31515">&#34;${workspaceFolder}/include/generated/autoconf.h&#34;</span>
</span></span><span style="display:flex;"><span>            ],
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            &#34;dotConfig&#34;: <span style="color:#a31515">&#34;${workspaceFolder}/.config&#34;</span>,
</span></span><span style="display:flex;"><span>            &#34;configurationProvider&#34;: <span style="color:#a31515">&#34;ms-vscode.makefile-tools&#34;</span>,
</span></span><span style="display:flex;"><span>            &#34;compileCommands&#34;: <span style="color:#a31515">&#34;${workspaceFolder}/compile_commands.json&#34;</span>,
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            &#34;compilerPath&#34;: <span style="color:#a31515">&#34;/usr/bin/gcc&#34;</span>,
</span></span><span style="display:flex;"><span>            &#34;intelliSenseMode&#34;: <span style="color:#a31515">&#34;linux-gcc-x64&#34;</span>,
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            &#34;cStandard&#34;: <span style="color:#a31515">&#34;gnu11&#34;</span>,
</span></span><span style="display:flex;"><span>            &#34;cppStandard&#34;: <span style="color:#a31515">&#34;gnu++11&#34;</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    ],
</span></span><span style="display:flex;"><span>    &#34;version&#34;: 4
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>В итоге оно работает, но очень ограниченно: навигация по коду постоянно тормозит и либо вовсе не находит часть символов, либо для части функций находит только их объявление в заголовочном файле, но не реализацию – и нормально работать в таком режиме просто невозможно, т. к. постоянно приходится переключаться на обычный поиск по содержимому файлов.</p>
<h2 id="clangd">clangd</h2>
<p>Но, как оказалось, у вышеупомянутых расширений от Microsoft есть очень достойная альтернатива в виде расширения <a href="https://marketplace.visualstudio.com/items?itemName=llvm-vs-code-extensions.vscode-clangd">clangd</a>, которое работает поверх полноценного language server&rsquo;а <a href="https://clangd.llvm.org/">clangd</a>. И это очень хорошая альтернатива, особенно учитывая то, что с относительно недавних пор ядро Linux <a href="https://docs.kernel.org/kbuild/llvm.html">поддерживает</a> сборку clang&rsquo;ом.</p>
<p>И оно действительно <strong>работает</strong>. Да – первоначальное построение индекса занимает какое-то время, но вот зато потом IDE видит все символы и, что не менее важно, осуществляет очень быструю навигацию по ним. Проблемы возникают разве что с заголовочными файлами, для которых в силу понятных причин не может быть однозначной информации, с какими флагами компиляции они будут использоваться.</p>
<h2 id="приступаем-к-работе">Приступаем к работе</h2>
<p>Клонируем репозиторий:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git clone git@github.com:torvalds/linux.git
</span></span></code></pre></div><p>Чтобы самому не возиться с конфигурацией ядра, а также работать именно с той, которая будет использоваться в реальной жизни, берём конфигурацию из текущей системы:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>cp <span style="color:#a31515">&#34;/boot/config-</span><span style="color:#00f">$(</span>uname -r<span style="color:#00f">)</span><span style="color:#a31515">&#34;</span> .config
</span></span></code></pre></div><p>Устанавливаем пакеты, которые нам понадобятся для сборки:</p>
<ul>
<li>Fedora: <code>sudo dnf install bc bison clang clangd elfutils-libelf-devel flex lld llvm make ncurses-devel openssl-devel xz zstd</code></li>
<li>Ubuntu: <code>sudo apt install bc bison clang clangd flex libelf-dev libncurses-dev libssl-dev lld llvm make xz-utils zstd</code></li>
</ul>
<p>В <code>.vscode/settings.json</code> задаём общие настройки проекта:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    &#34;[c]&#34;: {
</span></span><span style="display:flex;"><span>        &#34;editor.tabSize&#34;: 8,
</span></span><span style="display:flex;"><span>        &#34;editor.insertSpaces&#34;: <span style="color:#00f">false</span>,
</span></span><span style="display:flex;"><span>        &#34;editor.detectIndentation&#34;: <span style="color:#00f">false</span>,
</span></span><span style="display:flex;"><span>        &#34;editor.rulers&#34;: [80, 100]
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    &#34;files.associations&#34;: {
</span></span><span style="display:flex;"><span>        &#34;*.h&#34;: <span style="color:#a31515">&#34;c&#34;</span>
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    &#34;files.exclude&#34;: {
</span></span><span style="display:flex;"><span>        &#34;**/modules.order&#34;: <span style="color:#00f">true</span>,
</span></span><span style="display:flex;"><span>        &#34;**/.*.*.cmd&#34;: <span style="color:#00f">true</span>,
</span></span><span style="display:flex;"><span>        &#34;**/*.a&#34;: <span style="color:#00f">true</span>,
</span></span><span style="display:flex;"><span>        &#34;**/*.o&#34;: <span style="color:#00f">true</span>,
</span></span><span style="display:flex;"><span>        &#34;**/*.ko&#34;: <span style="color:#00f">true</span>,
</span></span><span style="display:flex;"><span>        &#34;**/*.mod&#34;: <span style="color:#00f">true</span>,
</span></span><span style="display:flex;"><span>        &#34;**/*.mod.c&#34;: <span style="color:#00f">true</span>,
</span></span><span style="display:flex;"><span>        &#34;**/*.symvers&#34;: <span style="color:#00f">true</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Далее устанавливаем расширение <a href="https://marketplace.visualstudio.com/items?itemName=llvm-vs-code-extensions.vscode-clangd">clangd</a> – либо через интерфейс VS Code, либо с помощью команды <code>code --install-extension llvm-vs-code-extensions.vscode-clangd</code>.</p>
<p>И я очень рекомендую добавить следующий параметр в настройки VS Code, в котором после <code>-j</code> указать количество ядер в вашем процессоре, т. к. по какой-то причине clangd по умолчанию при индексации использует только часть из них, что на таком большом проекте, как ядро Linux, приводит к невероятно долгой первичной индексации:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    &#34;clangd.arguments&#34;: [<span style="color:#a31515">&#34;-j&#34;</span>, <span style="color:#a31515">&#34;10&#34;</span>]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>clangd ожидает, что в корне проекта у нас будет файл <a href="https://clang.llvm.org/docs/JSONCompilationDatabase.html">compile_commands.json</a>, в котором будут указаны опции компиляции каждого <code>*.c</code>-файла проекта. Именно благодаря этому файлу он может правильно проиндексировать код. Поэтому запускаем следующую команду:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>make -j <span style="color:#a31515">&#34;</span><span style="color:#00f">$(</span>nproc<span style="color:#00f">)</span><span style="color:#a31515">&#34;</span> LLVM=1 compile_commands.json
</span></span></code></pre></div><p>&hellip; и идём заниматься своими делами – работать оно будет довольно долго.</p>
<p>Как только <code>make</code> отработает, и у нас появится <code>compile_commands.json</code>, открываем в VS Code любой <code>*.c</code>-файл, чтобы стриггерить clangd – и он тут же начнёт индексировать наш проект (в status bar будет соответствующая информация о прогрессе индексации). Тут тоже придётся подождать какое-то время, но только в первый раз – при последующем переоткрытии проекта оно будет подхватываться гораздо быстрее.</p>
<p>Как только индексация закончится, должны заработать все стандартные средства навигации по коду.</p>
<h2 id="работа-с-кодом-ядра-из-macos">Работа с кодом ядра из MacOS</h2>
<p>В качестве рабочего инструмента у меня MacBook Pro M1 и, казалось бы, это не самая удобная конфигурация для того, чтобы копаться в исходниках ядра, но, к счастью, это не так.</p>
<p>Устанавливаем <a href="https://orbstack.dev/">OrbStack</a> и создаём себе виртуальную машину:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>orbctl create ubuntu kernel
</span></span></code></pre></div><p>При этом, несмотря на то, что OrbStack поддерживает эмуляцию x86 через <a href="https://developer.apple.com/documentation/apple-silicon/about-the-rosetta-translation-environment">Rosetta</a>, в ней нет необходимости, т. к. при сборке ядра мы можем использовать кросс-компиляцию.</p>
<p>Заходим на нашу виртуальную машину:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>orb
</span></span></code></pre></div><p>и клонируем наш репозиторий.</p>
<p>Добавляем в самое начало <code>~/.vscode/ssh/config</code> следующую строку:</p>
<pre tabindex="0"><code>Include ~/.orbstack/ssh/config
</code></pre><p>А затем с помощью расширения <a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-ssh">Remote - SSH</a> подключаемся VS Code&rsquo;ом к нашей виртуальной машине и выполняем все действия, которые были перечислены в предыдущем разделе, но за одним исключением – при вызове <code>make</code> необходимо указать <code>ARCH=x86</code>, чтобы наша ARM&rsquo;овая виртуалка собрала ядро под целевую архитектуру (x86-64):</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>make -j <span style="color:#a31515">&#34;</span><span style="color:#00f">$(</span>nproc<span style="color:#00f">)</span><span style="color:#a31515">&#34;</span> LLVM=1 ARCH=x86 compile_commands.json
</span></span></code></pre></div>]]></content:encoded>
    </item>
    
    <item>
      <title>Мониторим потребление памяти в Linux-системе</title>
      <link>https://konishchev.ru/posts/linux-memory-monitoring/</link>
      <pubDate>Wed, 02 Apr 2025 19:22:25 +0300</pubDate>
      
      <guid>https://konishchev.ru/posts/linux-memory-monitoring/</guid>
      <description>&lt;p&gt;Сколько себя помню, меня всегда привлекали счётчики памяти в Linux: смотришь в условный &lt;code&gt;htop&lt;/code&gt; – в плане потребления CPU вроде всё +/- понятно, а вот память всегда считалась как-то не так, как ты это на первый взгляд ожидаешь, и долгое время у меня было довольно наивное и ошибочное представление о механизмах её работы.&lt;/p&gt;
&lt;p&gt;Со временем некоторые вещи прояснялись, приходило понимание, как именно оно работает под капотом (до определённой степени). В какой-то момент возникла рабочая необходимость понять, куда уходит память на реальной системе – и этот случай в очередной раз показал, что местами оно устроено довольно неочевидно, и на этот вопрос не всегда просто дать ответ. Ну а помимо рабочей необходимости у меня дома давно стоит сервер, обвешанный метриками, и всегда хотелось высветить себе их в понятной форме, чтобы потом в реальном времени наблюдать, как ведёт себя система, когда в ней происходят те или иные процессы.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Сколько себя помню, меня всегда привлекали счётчики памяти в Linux: смотришь в условный <code>htop</code> – в плане потребления CPU вроде всё +/- понятно, а вот память всегда считалась как-то не так, как ты это на первый взгляд ожидаешь, и долгое время у меня было довольно наивное и ошибочное представление о механизмах её работы.</p>
<p>Со временем некоторые вещи прояснялись, приходило понимание, как именно оно работает под капотом (до определённой степени). В какой-то момент возникла рабочая необходимость понять, куда уходит память на реальной системе – и этот случай в очередной раз показал, что местами оно устроено довольно неочевидно, и на этот вопрос не всегда просто дать ответ. Ну а помимо рабочей необходимости у меня дома давно стоит сервер, обвешанный метриками, и всегда хотелось высветить себе их в понятной форме, чтобы потом в реальном времени наблюдать, как ведёт себя система, когда в ней происходят те или иные процессы.</p>
<p>В этой статье я попробую разобрать, как сделать такой мониторинг и как интерпретировать его результаты. Сразу оговорюсь, что никогда не занимался разработкой ядра – вся информация ниже исключительно из личного опыта, поверхностного чтения исходников ядра и обильного гугления. Поэтому не исключено, что где-то могу быть неточным или вовсе неправым, но будем надеяться, что не сильно.</p>
<h2 id="ликбез-по-организации-памяти-в-linux">Ликбез по организации памяти в Linux</h2>
<p>Смотреть на счётчики ядра без понимания, что именно они измеряют, нет никакого смысла – поэтому начнём с описания базовых принципов того, как оно вообще работает под капотом (упрощая – иначе нужно писать не статью, а целую книгу).</p>
<h3 id="free-память">Free-память</h3>
<p>Пожалуй, первое, о чём стоит упомянуть – так это о том, что если вы посмотрите на систему, которая проработала какое-то ненулевое количество времени не вхолостую, то у неё, как правило, будет очень маленькое количество free-памяти. И это полностью нормально, т. к. под free-памятью Linux понимает именно память, которая полностью свободна, и в ней ничего не хранится. Но это слишком ценный ресурс, чтобы просто так простаивать без дела – и ядро всегда пытается использовать всю свободную память с пользой и занять её какими-нибудь кэшами, которые будут ускорять работу системы, но при необходимости всегда могут быть быстро освобождены. Поэтому, как правило, на большинстве систем всю свободную память занимает page cache.</p>
<h3 id="page-cache">Page cache</h3>
<p>Если вы напишете программу, которая записывает в файл какие-то данные, а затем эта (или даже другая) программа будет его читать, то можно заметить интересную особенность: даже если файл очень большой (гигабайты), но меньше объёма свободной памяти, то и операции записи, и операции чтения из файла будут происходить очень быстро – гораздо быстрее, чем может работать диск под ними.</p>
<p>Всё дело в том, что когда программа пишет данные на диск (выполняет системный вызов <a href="https://man7.org/linux/man-pages/man2/write.2.html">write(2)</a>), работа этого системного вызова как правило заключается в том, что он просто записывает данные в память – и сразу же возвращает управление. И уже только потом (асинхронно) ядро записывает эти данные на диск. При этом после записи данные как правило продолжают оставаться в памяти, и последующий вызов <a href="https://man7.org/linux/man-pages/man2/read.2.html">read(2)</a> на том же файле сможет считать их оттуда моментально, совершенно не обращаясь к диску.</p>
<p>Данная подсистема ядра называется page cache, и смысл её работы (упрощённо) следующий: при работе с любым блочным устройством все (если специально не попросить обратного – см <code>O_DIRECT</code> в <a href="https://man7.org/linux/man-pages/man2/open.2.html">open(2)</a>)  операции чтения и записи происходят через page cache. Если ядру необходимо считать какую-то информацию с диска, то оно считывает её в page cache страницами по 4 KiB, и уже затем доступ к данным происходит через него. При записи также сначала информация попадает в page cache, и только потом асинхронно сбрасывается на диск (если не попросить это сделать досрочно через вызов <a href="https://man7.org/linux/man-pages/man2/fsync.2.html">fsync(2)</a>). Это приводит к интересной особенности – в общем случае, когда нет memory pressure (о нём поговорим ниже), запись всегда происходит мгновенно (т. к. мы по сути пишем в память, а не на диск), а вот чтение может быть долгим (если файл ещё не закэширован в page cache).</p>
<p>Вообще, page cache – это невероятно универсальный и крутой механизм, который используется практически повсюду. К примеру:</p>
<ol>
<li>С помощью него можно за-<a href="https://man7.org/linux/man-pages/man2/mmap.2.html">mmap(2)</a>&lsquo;ить файл в память и работать с ним так, как будто вы работаете с обычной памятью. При этом ОС автоматом будет подгружать данные, когда вы обращаетесь к конкретному участку памяти и даже больше – ещё до того, как попытаетесь обратиться – за счёт механизмов prefetching.</li>
<li>Знаете ли вы, как программы загружаются на исполнение? Наивный (и на первый взгляд логичный) ответ звучал бы так, что ядро выделяет блок памяти и считывает туда бинарь с диска, а затем передает управление на исполнение. Но на самом деле всё устроено гораздо интереснее: когда вы запускаете какое-либо приложение, Linux по сути просто <a href="https://man7.org/linux/man-pages/man2/mmap.2.html">mmap(2)</a>&lsquo;ит бинарник в память процесса – и передаёт управление на нужную инструкцию, а дальше уже по мере того, как процессор прыгает по этим инструкциям, подгружает данные в page cache и отдаёт оттуда. Поэтому даже очень толстые бинари потребляют небольшое количество памяти, если по факту исполняется только небольшая часть их кода. Правда, есть и другой side effect: в случае memory pressure (см. ниже) система может сбросить page cache загруженных бинарей, и даже если у вас в системе нет swap-файла, у вас будут точно такие же задержки, как и в случае, когда ОС выгрузила ваши данные на диск.</li>
</ol>
<p>Очень рекомендую почитать <a href="https://biriukov.dev/docs/page-cache/0-linux-page-cache-for-sre/">Linux Page Cache for SRE</a> для более глубокого погружения в тему.</p>
<h3 id="buffers">Buffers</h3>
<p>Вот этот счётчик, на удивление, является самым запутанным по сравнению со всеми остальными. Вся информация, которая сходу гуглится по нему, довольно противоречивая, а в документации и вовсе написано &ldquo;Relatively temporary storage for raw disk blocks shouldn&rsquo;t get tremendously large (20MB or so)&rdquo; – при том, что я регулярно вижу как он показывает гигабайты.</p>
<p>Но на самом деле всё довольно просто: в ранних версиях Linux ту работу, которую сейчас выполняет подсистема Page Cache, выполняли две отдельные подсистемы. Теперь это уже не так, но в <code>/proc/meminfo</code> сохранено старое поведение, и счётчики <code>Cached</code> и <code>Buffers</code> отображают статистику разного типа кэшей:</p>
<ul>
<li><code>Cached</code> отвечает за кэширование содержимого файлов, когда обращение к ним производится через файловую систему.</li>
<li><code>Buffers</code> же отвечает за кэширование всего остального: блоков, которые содержат в себе метаданные файловой системы, разбивки диска, а также просто raw-блоки, когда вы читаете диск напрямую.</li>
</ul>
<p>Другими словами:</p>
<ul>
<li><code>Cached</code> увеличивается в результате следующих команд: <code>cat /dev/urandom &gt; out</code>, <code>cat big_file &gt; /dev/null</code>;</li>
<li><code>Buffers</code> увеличивается в случае <code>ls -laR / &gt; /dev/null</code> и <code>dd if=/dev/sda of=/dev/null bs=10M status=progress</code>.</li>
</ul>
<h3 id="анонимная-память">Анонимная память</h3>
<p>Технически, page cache – это страницы памяти, которые кэшируют содержимое блочного устройства и таким образом привязаны к нему. В противоположность page cache&rsquo;у существует анонимная (anonymous) память, у которой нет никакого backing-файла, и которая существует сама по себе.</p>
<p>Если не вдаваться в лишние в данном случае детали, то правильнее всего будет сказать, что анонимная память – это по сути вся userspace-память процессов: стек, глобальные переменные и куча.</p>
<h3 id="shmem-shared-memory">shmem (shared memory)</h3>
<p>Под shared memory подразумеваются следующие вещи:</p>
<ol>
<li>Анонимные блоки памяти, которые создаются с помощью <a href="https://man7.org/linux/man-pages/man2/mmap.2.html">mmap(2)</a> + <code>MAP_ANONYMOUS | MAP_SHARED</code> для совместного использования несколькими процессами.</li>
<li><code>tmpfs</code> – виртуальная файловая система, которая полностью находится в памяти. К примеру, <code>/run</code> – это <code>tmpfs</code>. Некоторые дистрибутивы также монтируют <code>tmpfs</code> в <code>/tmp</code> – поэтому никогда не сохраняйте туда большие файлы! Для этого существует <code>/var/tmp</code>, который всегда лежит на диске.</li>
<li>POSIX IPC API  (<a href="https://man7.org/linux/man-pages/man7/shm_overview.7.html">shm_overview(7)</a>,  <a href="https://man7.org/linux/man-pages/man7/sem_overview.7.html">sem_overview(7)</a>), которое на самом деле в Linux реализовано поверх того же <code>tmpfs</code>, который монтируется в <code>/dev/shm</code>.</li>
</ol>
<p>Кстати, интересный и очень неочевидный факт: в Linux все файловые системы работают поверх page cache&rsquo;а – и по этой причине всё, что вы размещаете в <code>tmpfs</code>, засчитывается как page cache. Просто это такой несколько необычный page cache – под которым нет никакого файла, а есть только &ldquo;закэшированные&rdquo; страницы в памяти.</p>
<h3 id="swap">swap</h3>
<p>swap – это опциональный раздел или файл на диске, куда ядро может выгружать анонимную и tmpfs-память, когда она долгое время не используется, либо когда ядро ощущает нехватку свободной памяти.</p>
<p>При этом, когда процесс обращается к памяти, которая выгружена в swap, ядро не сразу перемещает эту память из swap&rsquo;а в оперативную. Вначале оно просто подгружает нужные страницы в память, не удаляя их из swap&rsquo;а (чтобы в случае чего можно было быстро опять от них избавиться, если их не успеют поменять) – такое состояние страниц называется swap cached.</p>
<p>Есть ещё различные дополнительные варианты, в число которых входит <a href="https://wiki.archlinux.org/title/Zswap">zswap</a> – когда перед реальным swap&rsquo;ом строится in-memory cache, в котором выгруженные страницы хранятся в сжатом виде. Очень интересная вещь – рекомендую.</p>
<p>Вообще, тема (необходимости) swap&rsquo;а – довольно холиварная и тянет на отдельную статью. Поэтому я мог бы тут порекомендовать почитать, к примеру, <a href="https://chrisdown.name/2018/01/02/in-defence-of-swap.html">In defence of swap: common misconceptions</a>.</p>
<h3 id="page-tables">Page tables</h3>
<p>Как вы вероятно уже знаете, каждый процесс работает в собственном виртуальном пространстве памяти. Виртуальная память – это абстракция, реализованная на уровне железа (<a href="https://en.wikipedia.org/wiki/Memory_management_unit">MMU</a>): для каждого процесса ядро составляет таблицы отображения виртуальных адресов в физические, и во время свой работы процессор использует их, виртуализируя память для текущего процесса.</p>
<p>Данные таблицы – многоуровневые, чтобы минимизировать их размер для типичного случая, когда процесс использует лишь малую часть своего виртуального пространства – и если не принимать во внимание специфические случаи вроде того, когда куча процессов шарят между собой одни и те же куски памяти, то можно воспринимать page tables как фиксированный налог на используемую физическую память, а следовательно их размер всегда будет предсказуемым и относительно небольшим.</p>
<p>Более подробно про page tables можно почитать в <a href="https://docs.kernel.org/mm/page_tables.html">документации к ядру</a>.</p>
<h3 id="activeinactiveunevictable">Active/inactive/unevictable</h3>
<p>Вся анонимная, page cache и swap cache-память классифицируется на:</p>
<ul>
<li>active – страницы, к которым недавно производился доступ;</li>
<li>inactive – страницы, к которым давно никто не обращался;</li>
<li>unevictable – страницы анонимной памяти, которые нельзя выгрузить в swap. Как правило, это страницы, которые явно были залочены в памяти через вызов <a href="https://man7.org/linux/man-pages/man2/mlock.2.html">mlock(2)</a>.</li>
</ul>
<p>В случае нехватки памяти ядро старается в первую очередь избавляться от inactive-страниц и только потом уже переходить к active-страницам.</p>
<p>Упрощённо, деление на active/inactive происходит следующим образом:</p>
<ol>
<li>У каждой страницы есть <code>accessed</code>-бит (на уровне page table).</li>
<li>Если ядро обрабатывает доступ к странице по какой-либо причине, то проставляет этот флаг.</li>
<li>Если процесс обращается к странице, минуя ядро, то тогда этот флаг проставляет <a href="https://en.wikipedia.org/wiki/Memory_management_unit">MMU</a>.</li>
<li>Ядро периодически сканирует страницы и если видит выставленный <code>accessed</code>-бит, то зануляет его + promote&rsquo;ит данную страницу из inactive в active.</li>
</ol>
<p>Более подробно про этот механизм можно почитать в <a href="https://www.kernel.org/doc/html/v4.18/admin-guide/mm/idle_page_tracking.html#implementation-details">документации к ядру</a>.</p>
<p>При этом важно отметить, что не стоит воспринимать inactive-страницы как &ldquo;страницы, к которым не было доступа в течение N секунд&rdquo; – это работает не так. Цель деления на active/inactive не в том, чтобы иметь хронометрическую статистику по активности страниц, а в том, чтобы приоритизировать все имеющиеся страницы для reclaim&rsquo;а. Поэтому данное деление на самом деле очень условное и больше отражает относительную востребованность страниц между собой, нежели фактическую. Общее представление о механизме балансирования страниц между этими двумя списками можно получить <a href="https://biriukov.dev/docs/page-cache/4-page-cache-eviction-and-page-reclaim/#theory">тут</a>.</p>
<h3 id="slab-kmalloc-vmalloc">slab, kmalloc, vmalloc</h3>
<p>Чтобы выполнять свою работу, ядру необходимо где-то аллоцировать память под различные структуры, в которых оно хранит текущее состояние системы. Для этих целей в ядре есть <a href="https://hammertux.github.io/slab-allocator">slab-аллокатор</a>, суть которого заключается в следующем: под наиболее часто используемые структуры (пример – структура, описывающая процесс, <code>task_struct</code>) заводится свой пул, к которому можно обратиться и попросить аллоцировать память под новый объект. При этом когда мы возвращаем объект в пул, память не освобождается сразу (из расчёта, что через мгновение может прийти запрос на новую аллокацию), и таким образом в slab-аллокаторе, как правило, находится некоторое количество памяти, которую можно в любой момент быстро освободить, если она понадобится в другом месте.</p>
<p>Помимо этого, сам тип объекта в slab&rsquo;е может быть помечен как reclaimable – т. е. что он по сути является кэшем, и такие объекты можно при необходимости удалить. Пожалуй самый типичный пример таких объектов – это dentry/inode-кэши, которые, в отличие от page cache&rsquo;а, кэшируют уже не данные, а метаданные файлов и содержимое директорий. Благодаря им, когда вы делаете что-то вроде <code>open(&quot;/a/b/c/d/e&quot;, ...)</code>, системе не нужно каждый раз проходиться по всем директориям в поисках конечного файла (при этом кэшируются также и негативные результаты поиска).</p>
<p>Как правило, slab-аллокатор используется для структур, которые создаются часто и в большом количестве, но если нужно просто аллоцировать что-то по месту, то используется <code>kmalloc()</code> (по смыслу напоминающий <a href="https://man7.org/linux/man-pages/man3/malloc.3.html">malloc(3)</a>), который на самом деле является надстройкой над slab-аллокатором. А для выделения больших блоков памяти (но без гарантий физической последовательности страниц) используется <code>vmalloc()</code>, который уже никак не связан со slab&rsquo;ом и аллоцирует память напрямую из <a href="https://grimoire.carcano.ch/blog/memory-management-the-buddy-allocator/">buddy-аллокатора</a>.</p>
<p>Более подробно про типы аллокаторов можно почитать в <a href="https://www.kernel.org/doc/html/v5.0/core-api/memory-allocation.html">документации к ядру</a>. Посмотреть разбивку по текущему использованию slab&rsquo;а можно командой <code>slabtop -sc</code>.</p>
<h3 id="reclaimable-память-и-memory-pressure">Reclaimable-память и memory pressure</h3>
<p>В процессе своей работы ядро мониторит текущее состояние памяти и:</p>
<ol>
<li>Вытесняет неиспользуемую память в swap, чтобы её можно было использовать с большей пользой (к примеру, под page cache);</li>
<li>Проактивно освобождает память, если есть признаки того, что скоро она может закончиться;</li>
<li>Агрессивно освобождает память, если её осталось совсем мало.</li>
</ol>
<p>С этой целью в ядре:</p>
<ol>
<li>Есть конфигурируемые пороги на количество свободной памяти, которые триггерят проактивное и агрессивное освобождение (reclaim) памяти. Проактивное – когда память заранее reclaim&rsquo;ит выделенный background-тред; агрессивное – когда уже сами процессы вынуждены тратить своё процессорное время на reclaim, чтобы найти свободную страничку памяти.</li>
<li>Есть представление о том, что в случае нехватки памяти можно сбросить все кэши, о которых шла речь выше (правда, если это грязный page cache, то его сначала придётся записать на диск).</li>
<li>Есть деление на active и inactive-память, и ядро старается в первую очередь вытеснять inactive-страницы.</li>
</ol>
<p>Общий принцип работы memory pressure очень хорошо описан в <a href="https://www.yugabyte.com/blog/linux-performance-tuning-memory-disk-io/#linux-free-memory">Linux Performance Tuning: Dealing with Memory and Disk IO</a> + <a href="https://habr.com/ru/companies/otus/articles/765824/">перевод</a> – рекомендую почитать.</p>
<h3 id="kernel-stack">Kernel stack</h3>
<p>Как вы, вероятно, знаете, у каждого треда в рамках процесса есть свой стек. Но, что интересно, на самом деле у него их два – user space и kernel space. Помню, когда-то давно мне казалось, что системный вызов – это как вызов API некоторого сервиса: мы послали запрос, его положили в очередь на обработку, а нас поставили на паузу, пока на той стороне не найдут время его обработать. Но ядро работает не так. :) Когда вы делаете системный вызов, то происходит <a href="https://en.wikipedia.org/wiki/Context_switch">переключение контекста</a> – и по сути продолжается работа вашего треда – только уже выполняется код ядра, который обрабатывает системный вызов, и этому коду нужен свой стек для работы.</p>
<p>Кстати, потребление CPU как раз по этой причине делится на <code>user</code> и <code>system</code>: т. е. это и правда время работы конкретного процесса/треда (task в терминах ядра), но в разных контекстах.</p>
<h2 id="мониторим-procmeminfo">Мониторим /proc/meminfo</h2>
<p>Под мониторингом в данном случае я подразумеваю просвечивание метрик в <a href="https://prometheus.io/">Prometheus</a> / <a href="https://victoriametrics.com/">VictoriaMetrics</a>, чтобы впоследствии можно было видеть, что происходило с системой в конкретный момент времени.</p>
<p>Сразу скажу, что не буду в рамках этой статьи пытаться ставить какие-то особо амбициозные цели замониторить всё, что только можно с максимальной детализацией – и ограничусь той информацией, которую даёт нам <code>/proc/meminfo</code>. <a href="https://en.wikipedia.org/wiki/Non-uniform_memory_access">NUMA</a>, <a href="https://utcc.utoronto.ca/~cks/space/blog/linux/KernelMemoryZones">zones</a>, <a href="https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/6/html/performance_tuning_guide/s-memory-transhuge#s-memory-configure_hugepages">huge pages</a>, фрагментацию и пр. тоже учитывать не будем – это переусложнит задачу, а реальная необходимость в этом есть только в довольно специфичных случаях.</p>
<p>К счастью, в плане сбора метрик всё уже сделано за нас – <a href="https://github.com/prometheus/node_exporter">Prometheus Node Exporter</a> высвечивает все счётчики из <code>/proc/meminfo</code> в виде метрик <code>node_memory_*</code>. А вот с интерпретацией получившихся значений придётся поработать самим: во-первых, часть из них пересекается по учитываемым ими страницам памяти – и не всегда очевидно, что именно нужно вычитать/суммировать, чтобы получить разбивку, а во-вторых, документация к ним местами крайне расплывчатая, и сходу не всегда понятно, что именно они считают.</p>
<p>Поэтому вооружаемся <a href="https://github.com/torvalds/linux/blob/91fe0e4d044044592e667fd4784edc39fe53bbd8/Documentation/filesystems/proc.rst#meminfo">документацией</a>, <a href="https://github.com/torvalds/linux/blob/6e1fa555ec772046ec3b903f507ff7fed5323796/fs/proc/meminfo.c#L34">исходниками</a> и информацией, представленной выше – и идём смотреть, что же мы можем отсюда почерпнуть.</p>
<p>Ниже каждый заголовок представляет собой отдельный график, из которых мы будем формировать наш dashboard.</p>
<h3 id="memory-usage">Memory usage</h3>
<p>Первым делом попробуем сделать график максимально общей картины с высоты птичьего полёта, на котором отделим &ldquo;реально используемую&rdquo; память от всякого рода кэшей:</p>
<ul>
<li><code>Free = MemFree</code></li>
<li><code>Caches = Cached - Shmem + Buffers + KReclaimable</code></li>
<li><code>Used = MemTotal - Free - Caches</code></li>
</ul>
<h3 id="used-memory">Used memory</h3>
<p>Тут детализируем всю память, которая не является кэшами:</p>
<ul>
<li><code>Anonymous = AnonPages</code></li>
<li><code>Slab = SUnreclaim</code></li>
<li><code>Swap cached = SwapCached</code></li>
<li><code>zswap = Zswap</code> – память, которую потребляет подсистема zswap</li>
<li><code>Page tables = PageTables + SecPageTables</code></li>
<li><code>Kernel stacks = KernelStack</code></li>
<li><code>vmalloc = VmallocUsed - KernelStack</code> (в современных ядрах kernel stack <a href="https://docs.kernel.org/mm/vmalloced-kernel-stacks.html">выделяется через vmalloc</a>)</li>
<li><code>percpu = Percpu</code></li>
<li><code>shmem = Shmem</code></li>
</ul>
<h3 id="caches">Caches</h3>
<p>Здесь более детально отобразим, какие именно кэши сколько у нас занимают:</p>
<ul>
<li><code>Page cache = Cached - Shmem</code></li>
<li><code>Buffers = Buffers</code></li>
<li><code>Slab = SReclaimable</code></li>
<li><code>Misc = KReclaimable - SReclaimable</code></li>
</ul>
<p>Запускаем <code>sync &amp;&amp; echo 3 &gt; /proc/sys/vm/drop_caches</code> (см. <a href="https://github.com/torvalds/linux/blob/609706855d90bcab6080ba2cd030b9af322a1f0c/Documentation/admin-guide/sysctl/vm.rst#drop_caches">документацию</a>) – и наблюдаем, как много из этих кэшей система может освободить на самом деле (спойлер: далеко не всё).</p>
<p>При этом стоит отметить, что может быть и обратная ситуация – когда reclaimable памяти на самом деле больше, чем видно на первый взгляд. Очень показательным примером такого случая являются <a href="https://blogs.oracle.com/linux/post/zombie-memcg-issues">zombie memory cgroups</a>.</p>
<h3 id="unknown">Unknown</h3>
<p>Как бы нам ни хотелось детализировать абсолютно всю память, на 100% это сделать невозможно – просто потому, что некоторые аллокации делаются напрямую из buddy-аллокатора и не крутят никакие счётчики. Поэтому какая-то часть выделенной памяти (как правило, небольшая) будет вне наших подсчётов, и важно тут не забыть про неё:</p>
<p><code>Unknown = MemTotal - MemFree - (AnonPages + SwapCached + Zswap + SUnreclaim + VmallocUsed + PageTables + SecPageTables + Percpu) - (Cached + Buffers + KReclaimable)</code></p>
<p>Тут, кстати, будет не лишним уточнить, что же такое на самом деле <code>MemTotal</code>. При запуске ядро обнаруживает всю память, доступную в системе, и под каждую физическую страницу создаёт структуру <a href="https://github.com/torvalds/linux/blob/d7b8f8e20813f0179d8ef519541a3527e7661d3a/include/linux/mm_types.h#L73">struct page</a>, которыми затем оперирует buddy-аллокатор. <code>MemTotal</code> при этом отражает общий размер памяти, доступный для аллокации, т. е., грубо говоря, количество <code>struct page</code>, умноженное на размер страницы (и не включает в себя память, занятую самим buddy-аллокатором, а также зарезервированную под аппаратное обеспечение и память, в которой находится код ядра). Именно поэтому он всегда меньше размера физической памяти.</p>
<h3 id="available-memory">Available memory</h3>
<p>Здесь у нас будет всего одна метрика – <code>MemAvailable</code>. Она представляет из себя оценочное количество памяти, которое доступно приложениям для аллокации без ухода в swap.</p>
<p>Вычисляется она <a href="https://github.com/torvalds/linux/blob/e8c1a296b8066734ef20797ab77e03a90b0c9be8/mm/show_mem.c#L32">следующим образом</a>: берутся все свободные страницы, к ним добавляется весь page cache и reclaimable-память ядра + делаются поправки на watermark&rsquo;и memory pressure и тот факт, что абсолютно всю память по-reclaim&rsquo;ить всё равно не получится, т. к. по факту какая-то её часть всё равно нужна для нормальной работы системы.</p>
<h3 id="memory-swappiness">Memory swappiness</h3>
<p>Тут попробуем отобразить, как ядро видит пользовательскую память с точки зрения возможности reclaim&rsquo;а:</p>
<ul>
<li><code>Active anonymous = Active(anon)</code></li>
<li><code>Inactive anonymous = Inactive(anon)</code></li>
<li><code>Active page cache = Active(file)</code></li>
<li><code>Inactive page cache = Inactive(file)</code></li>
<li><code>Unevictable = Unevictable</code></li>
</ul>
<h3 id="page-cache-writeback">Page cache writeback</h3>
<p>На этом графике будем наблюдать за объёмом грязного page cache&rsquo;а и тем, как система справляется с синхронизацией его страниц на диск.</p>
<ul>
<li><code>Writeback = Writeback</code> – объём грязного page cache&rsquo;а, который в данный момент пишется на диск</li>
<li><code>Dirty = Dirty</code> – грязные страницы, которые пока только ждут своей очереди</li>
</ul>
<h3 id="swap-usage">Swap usage</h3>
<ul>
<li><code>Cashed = SwapCached</code> – закэшированные в памяти страницы swap&rsquo;а</li>
<li><code>zswapped = Zswapped</code> – объём памяти, выгруженный в zswap</li>
<li><code>Swapped out = SwapTotal - SwapFree - SwapCached - Zswapped</code> – выгруженная (и не закэшированная) на диск память</li>
</ul>
<h3 id="zswap">zswap</h3>
<p>Тут у меня есть свой <a href="https://github.com/KonishchevDmitry/server-metrics/blob/347236b21cd7c3a923e3c932a28dae52de01e52e/internal/zswap/metrics.go">zswap exporter</a>, который позволяет дополнительно следить за:</p>
<ol>
<li>Степенью сжатия, которую мы на самом деле получаем;</li>
<li>Заполнением zswap-пула;</li>
<li>Причинами, по которым страницы пролетели мимо zswap.</li>
</ol>
<h3 id="результат">Результат</h3>
<p>В итоге получаем вот такую красоту:
<a href="dashboard.png"><img loading="lazy" src="/posts/linux-memory-monitoring/dashboard.png" type="" alt="Dashboard"  /></a></p>
<h2 id="согласованность-данных">Согласованность данных</h2>
<p>Это может быть довольно неочевидным, но счётчики, которые выдаёт <code>/proc/meminfo</code> (наравне со многими другими файлами в proc/sysfs), могут быть несогласованными между собой. А именно: при сборе статистики ядро атомарно читает/считает отдельные значения, но не берёт какую-либо блокировку, которая гарантировала бы согласованность полученных значений. Это осознанный компромисс разработчиков ядра, чтобы сбор данных для мониторинга (для которого такая несогласованность обычно не критична) не замедлял работу системы. Поэтому стоит учитывать данный факт, если вы используете их для каких-то более ответственных целей, нежели рисования графиков (к примеру, при вычитании одних чисел из других периодически могут получаться отрицательные значения).</p>
<p>Наглядный пример: изменение <code>nr_huge_pages</code> + <code>surplus_huge_pages</code> + <code>free_huge_pages</code> <a href="https://github.com/torvalds/linux/blob/2942242dde896ea8544f321617c86f941899c544/mm/hugetlb.c#L3809">всегда производится под <code>hugetlb_lock</code></a>, но вот при сборе статистики ядро <a href="https://github.com/torvalds/linux/blob/2942242dde896ea8544f321617c86f941899c544/mm/hugetlb.c#L5231">полагается исключительно на атомарное чтение отдельных <code>long</code>-переменных</a>.</p>
<h2 id="заключение">Заключение</h2>
<p>Выше мы с вами мониторили всю систему целиком. Это, безусловно, очень интересно, но сразу же возникает желание пойти дальше – и видеть, какой именно сервис триггерит то или иное движение на графиках. И это можно сделать благодаря systemd + cgroups! systemd нарезает всю систему на понятные группы процессов (у которых, в отличие от pid&rsquo;ов, есть человеческое имя и, что самое важное – их конечное количество) – и если вы готовы к кратному увеличению количества метрик, то можно мониторить каждый сервис в отдельности, что выводит observability вашей системы на качественно новый уровень.</p>
<p>В рамках данной статьи я не буду пытаться покрыть всё, до чего могу только дотянуться – поэтому лишь намекну на такую возможность – возможно когда-нибудь это станет темой очередной статьи. А пока лишь могу поделиться своим exporter&rsquo;ом, которым сам пользуюсь для этой цели – <a href="https://github.com/KonishchevDmitry/server-metrics">server-metrics</a>. На данный момент он писался без мысли о том, что им может пользоваться кто-то кроме меня, но возможно для кого-то он сможет послужить источником вдохновения или рабочим примером того, как можно организовать такой мониторинг.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Cloudflare, ECH, или почему в последнее время у вас может не открываться часть зарубежных сайтов</title>
      <link>https://konishchev.ru/posts/cloudflare-ech/</link>
      <pubDate>Sat, 07 Dec 2024 16:16:27 +0300</pubDate>
      
      <guid>https://konishchev.ru/posts/cloudflare-ech/</guid>
      <description>&lt;p&gt;На прошлой неделе я заметил, что у меня ни с того ни с сего перестала открываться в браузере часть зарубежных сайтов. Поначалу это не выглядело как массовое явление: хотел зайти на &lt;a href=&#34;https://prometheus.io/docs/&#34;&gt;prometheus.io/docs&lt;/a&gt; – а он не открывается по таймауту. Ну ладно, думаю – прилёг, с кем не бывает. День лежит, второй лежит – уже выглядит подозрительно. При этом в процессе гугления каких-то совсем других проблем заметил, что также подвисают некоторые блоги.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>На прошлой неделе я заметил, что у меня ни с того ни с сего перестала открываться в браузере часть зарубежных сайтов. Поначалу это не выглядело как массовое явление: хотел зайти на <a href="https://prometheus.io/docs/">prometheus.io/docs</a> – а он не открывается по таймауту. Ну ладно, думаю – прилёг, с кем не бывает. День лежит, второй лежит – уже выглядит подозрительно. При этом в процессе гугления каких-то совсем других проблем заметил, что также подвисают некоторые блоги.</p>
<p>Посмотрел повнимательнее – curl открывает сайты бодро, Firefox – тоже, а вот в Яндекс Браузере по прежнему таймаут соединения без каких-либо объяснений причин в сетевой вкладке Developer Tools.</p>
<p>Ну что ж, Developer Tools ничего не говорят – пойдёмте смотреть в <a href="https://www.wireshark.org/">Wireshark</a>, что там такого интересного происходит&hellip;</p>
<h2 id="sni-esni-ech-wtf">SNI, ESNI, ECH, WTF?</h2>
<p>Для начала краткий ликбез: когда мы устанавливаем HTTPS-соединение, то, несмотря на то, что весь HTTP-запрос у нас шифруется, имя домена в TLS handshake идёт plain text&rsquo;ом, т. к. оно необходимо для того, чтобы сообщить балансировщику, какой именно сайт вы хотите открыть (и для какого домена балансировщик, терминирующий TLS, должен выдать вам сертификат). Данное расширение TLS, в рамках которого передаётся имя домена, называется <a href="https://www.cloudflare.com/learning/ssl/what-is-sni/">SNI (Server Name Indication)</a>.</p>
<p>Именно благодаря SNI Роскомнадзор может селективно блокировать HTTPS-ресурсы не по IP-адресам (задевая при этом ещё кучу других сайтов), а по доменным именам.</p>
<p>Так вот, давайте посмотрим, что нам расскажет Wireshark, когда мы набираем <a href="https://prometheus.io/">prometheus.io</a> в адресной строке браузера.</p>
<p>Видим два DNS-запроса:</p>
<pre tabindex="0"><code>DNS Standard query 0x6d3e A prometheus.io
DNS Standard query 0x169c HTTPS prometheus.io
</code></pre><ol>
<li>Стандартная <code>A</code>-запись – тут всё понятно и никаких сюрпризов;</li>
<li>А вот вторая необычная – <code>HTTPS</code>-запись. Я о такой даже и не слышал – как интересно. :)</li>
</ol>
<p>Вот ответ DNS-сервера:</p>
<pre tabindex="0"><code>prometheus.io: type HTTPS, class IN
    Name: prometheus.io
    Type: HTTPS (65) (HTTPS Specific Service Endpoints)
    Class: IN (0x0001)
    Time to live: 277 (4 minutes, 37 seconds)
    Data length: 136
    SvcPriority: 1
    TargetName: &lt;Root&gt;
    SvcParam: alpn=h3,h2
        SvcParamKey: alpn (1)
        SvcParamValue length: 6
        ALPN length: 2
        ALPN: h3
        ALPN length: 2
        ALPN: h2
    SvcParam: ipv4hint=104.21.60.220,172.67.201.240
        SvcParamKey: ipv4hint (4)
        SvcParamValue length: 8
        IP: 104.21.60.220
        IP: 172.67.201.240
    SvcParam: ech
        SvcParamKey: ech (5)
        SvcParamValue length: 71
        ECHConfigList length: 69
        ECHConfig: id=131 cloudflare-ech.com
    SvcParam: ipv6hint=2606:4700:3030::6815:3cdc,2606:4700:3030::ac43:c9f0
        SvcParamKey: ipv6hint (6)
        SvcParamValue length: 32
        IP: 2606:4700:3030::6815:3cdc
        IP: 2606:4700:3030::ac43:c9f0
</code></pre><p>А вот наш TLS handshake с балансером, за которым находится <a href="https://prometheus.io/">prometheus.io</a>:</p>
<pre tabindex="0"><code>TLSv1 Record Layer: Handshake Protocol: Client Hello
    Content Type: Handshake (22)
    Version: TLS 1.0 (0x0301)
    Length: 512
    Handshake Protocol: Client Hello
        Handshake Type: Client Hello (1)
        Length: 508
        Version: TLS 1.2 (0x0303)
        ...
        Extension: server_name (len=23) name=cloudflare-ech.com
            Type: server_name (0)
            Length: 23
            Server Name Indication extension
                Server Name list length: 21
                Server Name Type: host_name (0)
                Server Name length: 18
                Server Name: cloudflare-ech.com
        ...
</code></pre><p>Ого, всё ещё интереснее: несмотря на то, что мы идём на <a href="https://prometheus.io/">https://prometheus.io/</a>, самому серверу мы в SNI указываем какой-то cloudflare-ech.com, который как раз нам выдал <code>HTTPS</code> DNS-запрос.</p>
<p>Идём гуглить – и <a href="https://blog.cloudflare.com/speeding-up-https-and-http-3-negotiation-with-dns/">узнаём</a>, что <code>HTTPS</code> DNS-запись призвана:</p>
<ol>
<li>Уменьшить latency загрузки сайта, т. к. сразу предоставляет <code>A</code> и <code>AAAA</code>-записи, а также список поддерживаемых протоколов (HTTP/2, HTTP/3).</li>
<li>Увеличивает безопасность соединения при первичном открытии сайта, когда в браузере ещё нет закешированного <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security">HSTS</a>.</li>
<li>ECH stands for Encrypted Client Hello.</li>
</ol>
<p>Когда-то давно я читал про <a href="https://www.cloudflare.com/learning/ssl/what-is-encrypted-sni/">ESNI (Encrypted Server Name Indication)</a>, который призван решить проблему plain text&rsquo;овости домена в TLS, и даже не раз приходилось слышать, что Роскомнадзор блокирует ESNI как раз по той причине, то он не позволяет фильтровать HTTPS-трафик. Но вот <a href="https://blog.cloudflare.com/encrypted-client-hello/">ECH (Encrypted Client Hello)</a>, который по сути является логическим развитием ESNI, как-то проходил мимо меня – как что-то из ещё неопределённого будущего.</p>
<p>Почитав про <a href="https://blog.cloudflare.com/encrypted-client-hello/">ECH</a>, мы узнаём, что схема работает следующим образом:</p>
<ol>
<li>Браузер запрашивает <a href="https://blog.cloudflare.com/speeding-up-https-and-http-3-negotiation-with-dns/">HTTPS</a> DNS-запись.</li>
<li>Данная запись помимо IP-адресов включает в себя публичный домен-заглушку cloudflare-ech.com + открытый ключ.</li>
<li>При установке соединения с сервером, браузер в <a href="https://www.cloudflare.com/learning/ssl/what-is-sni/">SNI</a> (в открытой части Client Hello) передаёт ни о чём не говорящий cloudflare-ech.com, а в закрытой – уже в зашифрованном виде, настоящий домен.</li>
<li>РКН видит это, расстраивается, что ему не видно конечный домен – и <a href="https://habr.com/ru/news/856342/">блокирует соединение</a>.</li>
</ol>
<p>Ну а, собственно, проблемы начались из-за того, что Cloudflare, за которым находится просто бесчисленное множество сайтов, недавно <a href="https://github.com/net4people/bbs/issues/393">начал массово включать ECH</a>.</p>
<h2 id="как-обойти">Как обойти</h2>
<p>Если хочется сохранить доступ к сайтам, находящимся за Cloudflare, <del>не привлекая внимания санитаров</del> не заворачивая куда-либо абсолютно весь трафик до него, то первая мысль, которая приходит в голову – это пойти и отключить ECH в настройках браузера. Но браузеров много, устройств много, а помимо браузеров есть множество других программ, которые рано или поздно тоже начнут поддерживать ECH.</p>
<p>Если же хочется какого-то более универсального решения, то достаточно вспомнить о том, с чего всё началось, а именно – с <code>HTTPS</code>-записи. Поэтому решение, которое выбрал я (по крайней мере пока) – это просто взять и заблокировать <code>HTTPS</code> DNS-запись на своём роутере. В моём случае это решается добавлением <code>filter-rr=HTTPS</code> в конфиг <a href="https://thekelleys.org.uk/dnsmasq/doc.html">dnsmasq</a>.</p>
<p>Имея такую конфигурацию, наш DNS-сервер, находящийся на роутере, будет reject&rsquo;ить запросы <code>HTTPS</code>-записей, и браузер будет получать только обычные <code>A</code> и <code>AAAA</code>-записи, что вынудит его работать по старинке без всяких ECH. РКН будет видеть, что вы действительно заходите на незаблокированный ресурс и не будет резать соединение.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Автоматическая установка приложений из GitHub-релизов</title>
      <link>https://konishchev.ru/posts/binup/</link>
      <pubDate>Sun, 04 Aug 2024 14:51:41 +0300</pubDate>
      
      <guid>https://konishchev.ru/posts/binup/</guid>
      <description>&lt;p&gt;По мере распространения Go и Rust появляется всё больше программ, которые состоят из одного бинарника без каких-либо нестандартных зависимостей, и которые мы устанавливаем руками, скачивая релиз с GitHub: либо потому, что данного приложения ещё нет в вашем дистрибутиве, либо потому, что просто хочется всегда иметь актуальную версию, а не ждать, когда её затянут в дистрибутив.&lt;/p&gt;
&lt;p&gt;Ставить (а особенно обновлять) такие приложения руками – занятие неблагодарное, особенно когда их количество становится больше одного-двух – и хочется какой-то автоматизации. У меня таких программ около десятка, и довольно долгое время я пользовался различными наколеночными скриптами вроде &lt;a href=&#34;https://github.com/KonishchevDmitry/dotfiles/blob/61c8e48e5eb41db1df4e55ab1e97f6c74552b6bc/bin/get-github-release&#34;&gt;этого&lt;/a&gt; для поддержания их актуальности. Но bash-скрипт – это всё-таки как-то несерьёзно, и поэтому всегда хотелось чего-то более управляемого в виде нормального приложения. Найти что-то готовое, что удовлетворяло бы всем моим потребностям, мне сходу не удалось – поэтому некоторое время назад решил пойти моим излюбленным путём и написать своё приложение под эту конкретную задачу.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>По мере распространения Go и Rust появляется всё больше программ, которые состоят из одного бинарника без каких-либо нестандартных зависимостей, и которые мы устанавливаем руками, скачивая релиз с GitHub: либо потому, что данного приложения ещё нет в вашем дистрибутиве, либо потому, что просто хочется всегда иметь актуальную версию, а не ждать, когда её затянут в дистрибутив.</p>
<p>Ставить (а особенно обновлять) такие приложения руками – занятие неблагодарное, особенно когда их количество становится больше одного-двух – и хочется какой-то автоматизации. У меня таких программ около десятка, и довольно долгое время я пользовался различными наколеночными скриптами вроде <a href="https://github.com/KonishchevDmitry/dotfiles/blob/61c8e48e5eb41db1df4e55ab1e97f6c74552b6bc/bin/get-github-release">этого</a> для поддержания их актуальности. Но bash-скрипт – это всё-таки как-то несерьёзно, и поэтому всегда хотелось чего-то более управляемого в виде нормального приложения. Найти что-то готовое, что удовлетворяло бы всем моим потребностям, мне сходу не удалось – поэтому некоторое время назад решил пойти моим излюбленным путём и написать своё приложение под эту конкретную задачу.</p>
<h2 id="binup">binup</h2>
<p>Пара недель кодинга по вечерам – и родилась утилита <a href="https://github.com/KonishchevDmitry/binup">binup</a>. Сегодня я зарелизил версию 1.0.0 и полностью перешёл на неё со своих скриптов. Буду рад, если получившаяся тула будет полезна кому-то кроме меня.</p>
<p>Вот как она работает: вы создаёте конфигурационный файл <code>~/.config/binup/config.yaml</code> с примерно следующим содержимым, в котором описываете конкретное приложение (как его найти на GitHub):</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>tools:
</span></span><span style="display:flex;"><span>  binup:
</span></span><span style="display:flex;"><span>    project: KonishchevDmitry/binup
</span></span><span style="display:flex;"><span>    release_matcher: binup-linux-x64-*
</span></span></code></pre></div><p>&hellip; запускаете <code>binup install</code> или <code>binup upgrade</code> – и тула устанавливает, либо обновляет указанные вами приложения.</p>
<p>Работает <code>binup</code> довольно просто: она нигде не хранит никакую информацию об установленных приложениях, а вместо этого при запуске смотрит на их текущий статус: если нужного бинарника нет, то устанавливает его; если же есть, то пробует запустить приложение с <code>--version</code>, чтобы определить текущую версию приложения и сравнить её с последним релизом на GitHub. Если же версию определить не удалось (к примеру, программа вовсе может не поддерживать флаг <code>--version</code>), то <code>binup</code> ориентируется на время модификации файла, которое при установке приложения задаёт равным времени модификации релиза.</p>
<h2 id="конфигурация">Конфигурация</h2>
<p>Вот пример конфигурационного файла со всеми доступными на данный момент опциями:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#008000"># Path where to install the binaries (the default is ~/.local/bin)</span>
</span></span><span style="display:flex;"><span>path: /usr/local/bin
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>tools:
</span></span><span style="display:flex;"><span>  <span style="color:#008000"># Binary name</span>
</span></span><span style="display:flex;"><span>  prometheus:
</span></span><span style="display:flex;"><span>    <span style="color:#008000"># GitHub project name</span>
</span></span><span style="display:flex;"><span>    project: prometheus/prometheus
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#008000"># Changelog URL (will be printed on app upgrade)</span>
</span></span><span style="display:flex;"><span>    changelog: https://github.com/prometheus/prometheus/blob/main/CHANGELOG.md
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#008000"># Release archive pattern:</span>
</span></span><span style="display:flex;"><span>    <span style="color:#008000"># * By default shell-like glob matching is used (https://docs.rs/globset/latest/globset/#syntax)</span>
</span></span><span style="display:flex;"><span>    <span style="color:#008000"># * Pattern started with &#39;~&#39; is treated as regular expression (https://docs.rs/regex/latest/regex/#syntax)</span>
</span></span><span style="display:flex;"><span>    release_matcher: prometheus-*.linux-amd64.tar.gz
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#008000"># Binary path to look for inside the release archive. If it&#39;s not specified, the tool name will be used.</span>
</span></span><span style="display:flex;"><span>    binary_matcher: <span style="color:#a31515">&#34;*/prometheus&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#008000"># Post-install script</span>
</span></span><span style="display:flex;"><span>    post: systemctl restart prometheus
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#008000"># If you have a lot of tools, you may hit GitHub API rate limits for anonymous requests at some moment.</span>
</span></span><span style="display:flex;"><span><span style="color:#008000"># So it&#39;s recommended to obtain GitHub token (https://github.com/settings/tokens) and specify it here.</span>
</span></span><span style="display:flex;"><span><span style="color:#008000"># No permissions are required for the token – it&#39;s needed just to make API requests non-anonymous.</span>
</span></span><span style="display:flex;"><span>github:
</span></span><span style="display:flex;"><span>  token: $token
</span></span></code></pre></div><h2 id="последующее-развитие">Последующее развитие</h2>
<p>Развивать её в какой-то полноценный пакетный менеджер вроде <a href="https://brew.sh/">Homebrew</a> я точно не планирую, но вот в рамках решения вышеописанной задачи – вполне.</p>
<p>Пока что из наиболее явных потенциальных фичей видится поддержка GitLab, если возникнет такая необходимость (лично у меня пока что нет ни одного приложения с него), а также явно напрашивается генерация какого-то дефолтного <code>release_matcher</code>&lsquo;а в зависимости от текущей ОС и архитектуры.</p>
<p>Прямо сейчас она закрывает все мои потребности, но вполне вероятно, что в процессе использования будут возникать новые – и тогда буду закрывать и их.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Настраиваем сервер исходящей почты для отправки уведомлений</title>
      <link>https://konishchev.ru/posts/send-only-mail-server/</link>
      <pubDate>Sat, 18 May 2024 17:05:01 +0300</pubDate>
      
      <guid>https://konishchev.ru/posts/send-only-mail-server/</guid>
      <description>&lt;p&gt;Моя ситуация: есть домашний сервер, на котором настроено несколько десятков Prometheus-алертов + cron job&amp;rsquo;ы, рассылающие уведомления на почту. Хочется, чтобы все эти уведомления попадали на мою Gmail-почту и не помечались как спам.&lt;/p&gt;
&lt;p&gt;Лет десять назад я себе это всё как-то настроил, но с тех пор прошло слишком много времени – у почтовых сервисов появился ряд требований, которым должен удовлетворять сервер исходящей почты, поэтому пришло время обновить свою конфигурацию, чтобы она соответствовала современным стандартам. Поделюсь набором шагов, как это можно сделать.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Моя ситуация: есть домашний сервер, на котором настроено несколько десятков Prometheus-алертов + cron job&rsquo;ы, рассылающие уведомления на почту. Хочется, чтобы все эти уведомления попадали на мою Gmail-почту и не помечались как спам.</p>
<p>Лет десять назад я себе это всё как-то настроил, но с тех пор прошло слишком много времени – у почтовых сервисов появился ряд требований, которым должен удовлетворять сервер исходящей почты, поэтому пришло время обновить свою конфигурацию, чтобы она соответствовала современным стандартам. Поделюсь набором шагов, как это можно сделать.</p>
<p>На всякий случай сразу оговорюсь, что почти всё написанное ниже подразумевает, что у вас есть свой личный домен, с которого вы будете отправлять почту, т. к. ни один уважающий себя почтовый сервис не будет серьёзно относиться к письмам, отправленным с условного localhost&rsquo;а, и будет (хотя бы периодически) расценивать их как спам. Домен стоит не так уж и дорого – конкретно в моём случае это 250 руб. в год, что является вполне приемлемой суммой даже для домашнего сервера.</p>
<p>Настройка ниже предполагает, что мы хотим только отсылать почту, но не принимать её извне. В случае рассылки алертов это абсолютно не нужно, а получение почты извне добавляет массу ненужной сложности (к примеру, борьбу со спамом). И, как правило, если мы действительно хотим получать письма извне, то самым лучшим вариантом тут будет воспользоваться почтой для домена от одного из почтовых сервисов, т. к., как минимум, вместе с ней у вас будет удобный web-интерфейс и вполне сносная защита от спама. При этом такая почта для домена может без проблем сосуществовать с приведённой ниже конфигурацией: живые люди будут пользоваться её web-интерфейсом, а сервисы на вашем сервере – рассылать алерты через настроенный почтовый сервер.</p>
<p>Пример настройки будет описан для Ubuntu, но завязок на конкретный дистрибутив, как таковых, не будет. В качестве имени домена везде ниже буду использовать example.com.</p>
<h2 id="postfix">Postfix</h2>
<p>В качестве почтового сервера будем использовать <a href="https://www.postfix.org/">Postfix</a>. Рекомендую почитать на сайте их документацию – она достаточно подробная и даёт понимание, как оно работает под капотом.</p>
<p>Итак, ставим пакет:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo apt install postfix
</span></span></code></pre></div><p>В случае с Debian/Ubuntu он запустит интерактивный скрипт <code>dpkg-preconfigure</code>, который сгенерит нам базовую конфигурацию. В отобразившемся диалоге выбираем &ldquo;Internet Site&rdquo; и вводим имя своего домена. Особого смысла в этой конфигурации для нас нет, т. к. мы всё равно ниже всё переконфигурим, и пожалуй единственное что от неё останется – это Debian-specific файл <code>/etc/mailname</code>, в который будет прописано имя нашего домена, чтобы его могли использовать почтовые клиенты.</p>
<p>У Postfix два основных конфигурационных файла:</p>
<ul>
<li><code>/etc/postfix/master.cf</code>, который описывает, как именно будут запускаться различные демоны, из которых состоит почтовый сервер. Его лучше не трогать и менять только тогда, когда вы действительно знаете, что делаете.</li>
<li><code>/etc/postfix/main.cf</code> – в нём будут храниться все основные настройки.</li>
</ul>
<p>Заменяем <code>/etc/postfix/main.cf</code> следующим содержимым:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-cfg" data-lang="cfg"><span style="display:flex;"><span><span style="color:#008000"># man 5 postconf</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#008000"># See http://www.postfix.org/COMPATIBILITY_README.html</span>
</span></span><span style="display:flex;"><span><span style="color:#008000"># Restart postfix server and look into /var/log/mail.log for complains about new defaults</span>
</span></span><span style="display:flex;"><span>compatibility_level = <span style="color:#a31515">3.8</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#008000"># Used in default values of many variables. Specifies what domain to use for outbound mail</span>
</span></span><span style="display:flex;"><span>myhostname = <span style="color:#a31515">example.com</span>
</span></span><span style="display:flex;"><span>mydomain = <span style="color:#a31515">$myhostname</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#008000"># Domains which will be delivered locally instead of forwarding to another machine</span>
</span></span><span style="display:flex;"><span>mydestination = <span style="color:#a31515">localhost $myhostname</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#008000"># Rewrite (possibly local) sender domain to our external domain</span>
</span></span><span style="display:flex;"><span>sender_canonical_maps = <span style="color:#a31515">static:@$myorigin</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#008000"># Interfaces to listen on</span>
</span></span><span style="display:flex;"><span>inet_interfaces = <span style="color:#a31515">loopback-only</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#008000"># Forward mail from local host only</span>
</span></span><span style="display:flex;"><span>mynetworks_style = <span style="color:#a31515">host</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#008000"># Redefine the default to disable NIS support</span>
</span></span><span style="display:flex;"><span>alias_maps = <span style="color:#a31515">hash:/etc/aliases</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#008000"># A little hardening</span>
</span></span><span style="display:flex;"><span>allow_mail_to_files =
</span></span><span style="display:flex;"><span>allow_mail_to_commands =
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#008000"># Relay mail only via TLS. Since it&#39;s too strict setting for generic server which should be able to send mail to any</span>
</span></span><span style="display:flex;"><span><span style="color:#008000"># server on the Internet, it&#39;s reasonable when we send messages only to well-known servers like Gmail.</span>
</span></span><span style="display:flex;"><span>smtp_tls_security_level = <span style="color:#a31515">verify</span>
</span></span><span style="display:flex;"><span>smtp_tls_CApath=<span style="color:#a31515">/etc/ssl/certs</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#008000"># Generate delayed mail warnings</span>
</span></span><span style="display:flex;"><span>delay_warning_time = <span style="color:#a31515">4h</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#008000"># Errors to notify postmaster@ about</span>
</span></span><span style="display:flex;"><span>notify_classes = <span style="color:#a31515">2bounce, data, delay, policy, protocol, resource, software</span>
</span></span></code></pre></div><p>Здесь необходимо прописать в <code>myhostname</code> ваш домен, а также перечислить в <code>mydestination</code> все домены, с которых локальные сервисы могут прислать почту.</p>
<p>Логика тут следующая: если в <code>To:</code> письма указано только имя пользователя (скажем <code>dmitry</code>), то Postfix автоматом понимает, что это письмо предназначено для нашего домена (локальной доставки), но если в качестве имени пользователя указано, скажем, <code>dmitry@server</code>, то необходимо проинструктировать Postfix, что <code>server</code> – это на самом деле тоже наш домен. Необходимость в этом возникает, к примеру, когда hostname вашего сервера (значение в <code>/etc/hostname</code>) отличается от почтового домена, и неправильно настроенная программа отправки почты может генерировать <code>To</code>-адреса используя это имя сервера.</p>
<p>Далее заводим пользователя <code>postmaster</code>:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo useradd -d /nonexistent -s /usr/sbin/nologin -r postmaster
</span></span></code></pre></div><p>Это well-known имя пользователя, которому Postfix будет посылать сообщения о различных ошибках (например, доставки почты).</p>
<p>Заменяем <code>/etc/aliases</code> следущим содержимым:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-cfg" data-lang="cfg"><span style="display:flex;"><span><span style="color:#008000"># man 5 aliases</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>root:   example@gmail.com
</span></span><span style="display:flex;"><span>dmitry: example@gmail.com
</span></span></code></pre></div><p>Логика тут следующая: у нас есть два активных пользователя – <code>root</code> и <code>dmitry</code>, которым система может слать почтовые сообщения. В моём случае это преимущественно <code>cron</code>, который отсылает stdout/stderr своих джоб пользователю по почте, но, к примеру, их также шлёт <code>sudo</code>, если 3 раза неправильно ввести пароль. Данные директивы говорят Postfix о том, что необходимо принимать почту для этих двух пользователей не локально, а пересылать её на наш Gmail-ящик.</p>
<p>Если же Postfix не сможет доставить почту по данным адресам (Gmail отклонит её по какой-то причине), то информация об этом будет направлена пользователю <code>postmaster</code>.</p>
<p>Несмотря на то, что в <code>main.cf</code> у нас прописан <code>/etc/aliases</code>, Postfix будет считывать файл <code>/etc/aliases.db</code>, который является скомпилированным бинарным представлением этого конфига. Поэтому после каждой правки <code>/etc/aliases</code> необходимо запускать <code>sudo newaliases</code>, чтобы она создала <code>/etc/aliases.db</code>.</p>
<p>На этом базовая настройка закончена. Перезапускаем сервер и смотрим в его лог:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo systemctl restart postfix &amp;&amp; sudo tail -f /var/log/mail.log
</span></span></code></pre></div><h3 id="проверка-пересылки">Проверка пересылки</h3>
<p>Проверяем:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo apt install mutt
</span></span><span style="display:flex;"><span>mutt -s <span style="color:#a31515">&#39;Test mail&#39;</span> <span style="color:#a31515">&#34;</span><span style="color:#00f">$(</span>whoami<span style="color:#00f">)</span><span style="color:#a31515">&#34;</span> &lt;&lt;&lt; <span style="color:#a31515">&#39;Message body&#39;</span>
</span></span></code></pre></div><p>– письмо должно уйти на наш Gmail-аккаунт.</p>
<h3 id="проверка-локальной-доставки">Проверка локальной доставки</h3>
<p>Чтобы быть в курсе всех проблем с отправкой почты, я выставил пользователю <code>root</code> переменную окружения <code>MAIL=/var/mail/postmaster</code>: своего ящика у него всё равно больше нет (вся почта перенаправляется в Gmail), но зато теперь bash будет автоматически уведомлять меня о всех новых сообщениях пользователя <code>postmaster</code>.</p>
<p>Проверяем работу данной схемы:</p>
<ol>
<li>Логинимся под <code>root</code>.</li>
<li>От любого пользователя запускаем <code>mutt -s 'Test mail' postmaster &lt;&lt;&lt; 'Message body'</code>.</li>
<li>root&rsquo;овый bash должен написать в терминал <code>You have mail in /var/mail/postmaster</code>.</li>
<li>Смотрим почту (к примеру, при помощи того же <code>mutt</code>).</li>
</ol>
<h2 id="spf-dkim-dmark">SPF, DKIM, DMARK</h2>
<p>Базовая настройка завершена, почта успешно уходит в Gmail, но с большой вероятностью будет попадать в спам. Всё потому, что наш сервер не удовлетворяет <a href="https://support.google.com/a/answer/81126">требованиям Gmail</a>.</p>
<p>Рекомендую почитать какой-нибудь tutorial по SPF/DKIM/DMARK, о которых пойдёт речь ниже, чтобы было понимание, что именно мы будем делать. К примеру, вот <a href="https://dmarcly.com/blog/how-to-implement-dmarc-dkim-spf-to-stop-email-spoofing-phishing-the-definitive-guide">этот</a>.</p>
<h3 id="ptr">PTR</h3>
<p>Первым делом стоит удостовериться, что у вашего сервера правильная <a href="https://en.wikipedia.org/wiki/Reverse_DNS_lookup">PTR-запись</a>. Не всегда есть возможность её поменять – у меня, к примеру, в силу определённых причин её нету, и мне (пока?) это не мешает, но почтовые сервисы на неё точно смотрят, и желательно, чтобы она указывала на ваш домен.</p>
<h3 id="spf">SPF</h3>
<p>С помощью <a href="https://en.wikipedia.org/wiki/Sender_Policy_Framework">SPF</a> мы зададим список IP-адресов, с которых можно отсылать письма от имени нашего домена.</p>
<p>Добавляем следующую DNS-запись:</p>
<pre tabindex="0"><code>example.com. IN TXT &#34;v=spf1 a ~all&#34;
</code></pre><p>которая разрешает отправлять их только с тех IP, в которые резолвится наш <code>example.com</code>. Я использую <code>~all</code> вместо <code>-all</code>, т. к. в моём случае вряд ли кто-то будет заниматься спуфингом от моего имени, и в случае какой-то мисконфигурации я бы предпочёл получить свои письма в спам, чем не получить их вовсе.</p>
<p>Если вам хочется задать другие правила, то можно воспользоваться <a href="https://dmarcly.com/tools/spf-record-generator">SPF-генератором</a>.</p>
<h3 id="dkim">DKIM</h3>
<p><a href="https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail">DKIM</a> – это механизм подписи всех исходящих писем, с помощью которого получающая сторона может убедиться, что данные письма были присланы именно нашим сервером.</p>
<p>Реализовывать его будем с помощью <a href="http://www.opendkim.org/">OpenDKIM</a>.</p>
<p>Ставим пакет:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo apt install opendkim
</span></span></code></pre></div><p>Генерируем ключи:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo opendkim-genkey --domain example.com --selector server --nosubdomains --restrict --directory /etc/dkimkeys
</span></span></code></pre></div><p><code>server</code> здесь – это DKIM selector – произвольный идентификатор ключа. Нужен он потому, что их может быть несколько: у каждого сервера по ключу, либо в целях периодической ротации ключей.</p>
<p>Смотрим в <code>/etc/dkimkeys/server.txt</code> и прописываем получившуюся запись в DNS. Тут, правда, вас может ждать неудача: запись получается довольно длинная, и некоторые хостеры (к примеру, мой) просто отказываются её принимать. В таком случае можно сгенерировать чуть менее безопасный ключ, который будет меньшего размера:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo opendkim-genkey --domain example.com --selector server --bits 1024 --nosubdomains --directory /etc/dkimkeys
</span></span></code></pre></div><p>Проверяем, что всё прописалось правильно:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo opendkim-testkey -d example.com -s server -k /etc/dkimkeys/server.private
</span></span></code></pre></div><p>Меняем права на ключи:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo chown opendkim:opendkim /etc/dkimkeys/server.{private,txt}
</span></span></code></pre></div><p>и начинаем конфигурировать сам сервис.</p>
<p>В Debian/Ubuntu Postfix запускается в chroot&rsquo;е, и если UNIX-сокет OpenDKIM-сервера будет находиться вне его, то они просто не смогут общаться между собой. Поэтому содаём следующую директорию:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo install -d -o opendkim -g postfix -m 710 /var/spool/postfix/opendkim
</span></span><span style="display:flex;"><span>sudo chmod g+s /var/spool/postfix/opendkim
</span></span></code></pre></div><p>Создаём <code>/etc/systemd/system/opendkim.service</code> (конфигурация сервиса, которая поставляется с дистрибутивом, довольно странная – и лучше написать свою):</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-systemd" data-lang="systemd"><span style="display:flex;"><span><span style="color:#00f">[Unit]</span>
</span></span><span style="display:flex;"><span>Description=<span style="color:#a31515">OpenDKIM Milter</span>
</span></span><span style="display:flex;"><span>Before=<span style="color:#a31515">postfix.service</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#00f">[Service]</span>
</span></span><span style="display:flex;"><span>User=<span style="color:#a31515">opendkim</span>
</span></span><span style="display:flex;"><span>NoNewPrivileges=<span style="color:#a31515">true</span>
</span></span><span style="display:flex;"><span>MemoryDenyWriteExecute=<span style="color:#a31515">true</span>
</span></span><span style="display:flex;"><span>ProtectSystem=<span style="color:#a31515">true</span>
</span></span><span style="display:flex;"><span>PrivateTmp=<span style="color:#a31515">true</span>
</span></span><span style="display:flex;"><span>ExecStart=<span style="color:#a31515">/usr/sbin/opendkim</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#00f">[Install]</span>
</span></span><span style="display:flex;"><span>WantedBy=<span style="color:#a31515">multi-user.target</span>
</span></span></code></pre></div><p>Заменяем <code>/etc/opendkim.conf</code> на:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-cfg" data-lang="cfg"><span style="display:flex;"><span>Background no
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Syslog yes
</span></span><span style="display:flex;"><span>SyslogSuccess yes
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Mode s
</span></span><span style="display:flex;"><span>Domain example.com
</span></span><span style="display:flex;"><span>Selector server
</span></span><span style="display:flex;"><span>KeyFile /etc/dkimkeys/server.private
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#008000"># Postfix chroots into /var/spool/postfix, so we have to create SGID directory there and use permissive umask</span>
</span></span><span style="display:flex;"><span>Socket local:/var/spool/postfix/opendkim/opendkim.sock
</span></span><span style="display:flex;"><span>UMask 007
</span></span></code></pre></div><p>Добавляем в <code>/etc/postfix/main.cf</code>:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-cfg" data-lang="cfg"><span style="display:flex;"><span><span style="color:#008000"># DKIM signing</span>
</span></span><span style="display:flex;"><span>smtpd_milters = <span style="color:#a31515">unix:opendkim/opendkim.sock</span>
</span></span><span style="display:flex;"><span>non_smtpd_milters = <span style="color:#a31515">unix:opendkim/opendkim.sock</span>
</span></span></code></pre></div><p>Проверяем, работоспособность всей схемы:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo systemctl daemon-reload &amp;&amp; sudo systemctl restart opendkim postfix &amp;&amp; sudo tail -f /var/log/mail.log
</span></span><span style="display:flex;"><span>mutt -s <span style="color:#a31515">&#39;Test mail&#39;</span> <span style="color:#a31515">&#34;</span><span style="color:#00f">$(</span>whoami<span style="color:#00f">)</span><span style="color:#a31515">&#34;</span> &lt;&lt;&lt; <span style="color:#a31515">&#39;Message body&#39;</span>
</span></span></code></pre></div><h3 id="dmark">DMARK</h3>
<p>Ну и последний штрих, который нам остался – это <a href="https://en.wikipedia.org/wiki/DMARC">DMARK</a>. Воспользуемся <a href="https://dmarcly.com/tools/dmarc-generator">DMARK-генератором</a> и получим следующую DNS-запись:</p>
<pre tabindex="0"><code>_dmarc IN TXT &#34;v=DMARC1; p=quarantine; sp=none; aspf=s; adkim=s;&#34;
</code></pre><p>Как и с SPF, для наших целей лучше подойдёт <code>p=quarantine</code>.</p>
<p>Email для aggregate/forensic reports не указываем, т. к. это опять-таки не наш случай:</p>
<ul>
<li>Aggregate Reports – это ежедневная рассылка XML-файлов, которая имеет смысл только если вы – какая-то большая организация и перенаправляете их в соответствующий сервис для последующего анализа.</li>
<li>Forensic Reports и вовсе не поддерживаются в Gmail.</li>
</ul>
<h2 id="заключительные-шаги">Заключительные шаги</h2>
<p>После того, как мы всё настроили, самое время пойти посмотреть, что про наши письма думают почтовые сервисы:</p>
<ol>
<li>Нажимаем на вертикальное троеточие в сообщении в Gmail и выбираем пункт &ldquo;Show original&rdquo; – там в самом верху будут статусы вида &ldquo;SPF/DKIM/DMARC: PASS&rdquo;, а также проверяем, что само сообщение не помечается красным флажком &ldquo;No encryption&rdquo; (в настройках Postfix мы указали отправку только по защищённому каналу). Есть ещё <a href="https://postmaster.google.com/">Google Postmaster Tools</a>, но они выглядят какими-то полузаброшенными: дашборд с графиками объявлен устаревшим и упорно отказывается показывать хоть какие-то графики даже после того, как я в течение недели слал себе каждую минуту какие-то сообщения (утверждается, что они могут не отображаться при небольшом количестве сообщений); в Compliance status в итоге все зелёные кружочки у меня загорелись, но только спустя неделю.</li>
<li>Заходим на <a href="https://www.mail-tester.com/">mail-tester.com</a> и отправляем сообщение на адрес, который он нам предложит, используя команду вида <code>mutt -s 'Test mail' test-m2rbwe4su@srv1.mail-tester.com &lt;&lt;&lt; 'Message body'</code>. Высокого рейтинга нам в нём ждать не стоит: он проверяет содержимое сообщения на спам (и тогда надо отсылать что-то реальное, чтобы не поджечь их лампочки), а также у нас нет <code>MX</code>-записи для входящей почты, что в его понимании выглядит крайне подозрительным. Но наша цель тут – не попытаться получить максимальный рейтинг по всем параметрам, а убедиться, что его устраивает всё, что касается только отправки писем.</li>
</ol>
<p>В итоге, имеем конфигурацию, которая полностью устраивает Gmail. И если раньше в случае резкого всплеска сообщений от какой-то сломавшейся cron&rsquo;ячки он начинал периодически помещать их в спам, то теперь, сколько бы тестовых сообщений я не пытался отправить, такого не происходит.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>BBR: когда TCP &#34;быстрее&#34; UDP</title>
      <link>https://konishchev.ru/posts/tcp-bbr/</link>
      <pubDate>Fri, 19 Apr 2024 21:25:58 +0300</pubDate>
      
      <guid>https://konishchev.ru/posts/tcp-bbr/</guid>
      <description>&lt;p&gt;Некоторое время назад, в связи со всем известными событиями, я решил защитить свой текущий тоннель до VPS в Нидерландах, для которого до этого использовал обычный &lt;a href=&#34;https://www.wireguard.com/&#34;&gt;WireGuard&lt;/a&gt;. Как это часто со мной бывает, я решил пойти не самым простым, но зато самым любимым мной путём – и написал свой тоннель. :) Идея эта была привлекательна тем, что давала мне возможность познакомиться с &lt;a href=&#34;https://tokio.rs/&#34;&gt;Tokio&lt;/a&gt;, побольше узнать о принципах работы tun/tap–интерфейсов в Linux, почитать исходники &lt;a href=&#34;https://shadowsocks.org/&#34;&gt;Shadowsocks&lt;/a&gt;, ну и в процессе даже удалось найти и поправить &lt;a href=&#34;https://github.com/systemd/systemd/pull/30504&#34;&gt;небольшую багу в networkd&lt;/a&gt;. Но на самом деле речь в данном посте пойдёт не об этом.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Некоторое время назад, в связи со всем известными событиями, я решил защитить свой текущий тоннель до VPS в Нидерландах, для которого до этого использовал обычный <a href="https://www.wireguard.com/">WireGuard</a>. Как это часто со мной бывает, я решил пойти не самым простым, но зато самым любимым мной путём – и написал свой тоннель. :) Идея эта была привлекательна тем, что давала мне возможность познакомиться с <a href="https://tokio.rs/">Tokio</a>, побольше узнать о принципах работы tun/tap–интерфейсов в Linux, почитать исходники <a href="https://shadowsocks.org/">Shadowsocks</a>, ну и в процессе даже удалось найти и поправить <a href="https://github.com/systemd/systemd/pull/30504">небольшую багу в networkd</a>. Но на самом деле речь в данном посте пойдёт не об этом.</p>
<p>Когда я писал свой тоннель, у меня было стойкое убеждение, что в качестве транспорта нужно прежде всего ориентироваться на UDP, а на TCP откатываться только в том случае, когда UDP перестаёт работать в силу тех или иных &ldquo;причин&rdquo;. И это вроде бы логично: в плане производительности для тоннеля UDP всегда предпочтительнее, т. к. он реализует именно то, что нам нужно – максимально тонкую обёртку над пакетами, а с TCP у нас начинается целый ворох проблем, начиная с <a href="https://openvpn.net/faq/what-is-tcp-meltdown/">TCP Meltdown</a> и заканчивая <a href="https://en.wikipedia.org/wiki/Head-of-line_blocking">head-of-line blocking</a>.</p>
<p>Но вот когда я начал думать, как можно подтюнить получившийся тоннель помимо включения довольно очевидных вещей вроде <a href="https://man7.org/linux/man-pages/man7/tcp.7.html">TCP_NODELAY</a>, то набрёл на BBR, который стал для меня очень приятным открытием.</p>
<h2 id="bbr">BBR</h2>
<p><a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=0f8782ea14974ce992618b55f0c041ef43ed0b78">BBR (Bottleneck Bandwidth and RTT)</a> – это алгоритм <a href="https://en.wikipedia.org/wiki/TCP_congestion_control">TCP congestion control</a>, разработанный в Google. В отличие от традиционных алгоритмов, которые ориентируются на потери пакетов, BBR пытается судить о загруженности канала, наблюдая за тем, как меняется скорость передачи данных и RTT. Сейчас сети уже не те, что были в 80-ых годах, и такой подход работает гораздо лучше, а особенно – в случае трансконтинентальных соединений, где у нас может быть довольно широкий канал, но при этом небольшой процент потерь пакетов является нормой (как в моём случае с дешёвым VPS).</p>
<p>В результате получается, что если у вас такой канал, где нередки небольшие потери пакетов, то это сильно сказывается на скорости TCP-соединения, т. к. эти потери расцениваются традиционными алгоритмами как сигнал к тому, что необходимо сбросить скорость до тех пор, пока они не сойдут на нет. К тому же, даже в случае когда потерь нет, но при этом TCP-соединение способно утилизировать весь канал передачи данных, традиционные алгоритмы склонны снижать скорость только тогда, когда буфер роутера (зачастую довольно большой) уже переполнен, и роутер начинает дропать пакеты, что по факту является сильно запоздалой реакцией, которая приводит к увеличению latency, а BBR как раз пытается этой ситуации избежать. Этой проблеме даже посвящен целый сайт <a href="https://www.bufferbloat.net/projects/">bufferbloat.net</a>.</p>
<p>Польза, которую BBR способен вам нанести по сравнению со стандартным CUBIC&rsquo;ом, сильно зависит от многих факторов, но вот, к примеру, отчёты Amazon и Google, которые свидетельствуют о том, что после включения BBR у них стабильно улучшились bandwidth и RTT: <a href="https://aws.amazon.com/blogs/networking-and-content-delivery/tcp-bbr-congestion-control-with-amazon-cloudfront/">Amazon</a>, <a href="https://cloud.google.com/blog/products/networking/tcp-bbr-congestion-control-comes-to-gcp-your-internet-just-got-faster">Google</a>. В определённых случаях можно ожидать и кратного увеличения скорости TCP-соединения.</p>
<p>Т. к. BBR является алгоритмом TCP congestion control, то включается он на отправляющей стороне. Т. е., включив его на своём ноутбуке, вы улучшите upload данных, а чтобы улучшить download, он должен быть включён на сервере. При этом, само собой, включение его на одной стороне не требует какой-либо поддержки на другой, т. к. меняются только эвристики внутри TCP-протокола, а не сам протокол.</p>
<h2 id="критика">Критика</h2>
<p>Не обошлось правда и без <a href="https://huitema.wordpress.com/2019/01/12/will-transport-innovation-collapse-the-internet/">критики</a> данного алгоритма. Всё дело в том, что по сравнению со стандартным CUBIC&rsquo;ом, BBR ведёт себя достаточно агрессивно, и может получиться так, что если вы, скажем, включите его на своем Linux-ноутбуке и начнёте заливать большие файлы в сеть, то ваш BBR-ноутбук может запросто &ldquo;задушить&rdquo; TCP-соединения соседних устройств, использующих традиционные алгоритмы TCP congestion control (а в MacOS, к примеру, BBR и вовсе недоступен).</p>
<p>Есть инициатива в виде <a href="https://datatracker.ietf.org/meeting/112/materials/slides-112-iccrg-bbrv2-update-00">BBRv2</a>, которая пытается решить эту проблему, но пока что в ядре используется первая версия, и надо эту особенность иметь в виду.</p>
<h2 id="эффект-от-bbr-применительно-к-tcp-тоннелю">Эффект от BBR применительно к TCP-тоннелю</h2>
<p>Так вот, почитав всё это, я с одной стороны обрадовался (какая многообещающая штука – надо пробовать!), а с другой – тут же взгрустнул: включать BBR нужно на конечных устройствах (сервер + клиент), а на роутере его включать бесполезно, т. к. при forwarding&rsquo;е пакетов все эти алгоритмы по понятным причинам не задействованы. Но на серверную часть я повлиять не могу, а в качестве клиентской у меня ноутбук с MacOS, в котором BBR и вовсе нет. С другой стороны – подумал я – у меня ведь по факту два TCP-соединения: одно между ноутбуком и конечным сервером, а другое – между двумя точками тоннеля, которые находятся на Linux-серверах, и вот на это соединение я повлиять могу.</p>
<p>Чтож, попробуем&hellip; Включил его для TCP-соединения своего тоннеля – и тут же получил 2.5x скорость при скачивании файлов! Если раньше что с UDP, что с TCP-транспортом у меня абсолютным максимумом было 30 Mbit/s, то после включения BBR оно тут же скакнуло до 75 Mbit/s.</p>
<p>Лично я такие результаты объясняю себе следующим: гоняя тоннельный траффик по TCP, я маскирую все потери пакетов между двумя точками тоннеля для TCP-соединений, которые в него заворачиваются. И даже если они используют традиционные алгоритмы, основанные на потерях пакетов, то теперь они этих потерь не замечают и не сбрасывают скорость почём зря. Ну а дальше уже эти данные передаются по TCP-соединению тоннеля с BBR, который максимизирует скорость передачи данных.</p>
<p>В итоге я пришёл к следующему: если раньше я всегда в качестве транспорта использовал UDP, и переключение на TCP вызывало заметные глазу подтормаживания при загрузке страниц в браузере, то теперь, когда у меня TCP работает с BBR, картина поменялась на противоположную: UDP выдаёт вполне приемлемый результат, но если переключиться на TCP, то невооружённым глазом становится видно, что всё начинает подгружаться ещё быстрее. Учитывая то, что TCP-тоннель ещё и можно замаскировать под обычное HTTPS-соединение, получается, что в использовании UDP и вовсе нет смысла. Единственное, в чём UDP по прежнему обходит TCP + BBR – это в latency: если запустить ping и начать грузить канал, то в случае с UDP latency ping&rsquo;ов практически не меняется, в то время как с TCP (с BBR и без) оно может увеличиваться до четырёх раз. Но т. к. это довольно синтетический тест, а при реальном использовании с браузером, как я описал выше, я вижу противоположные результаты, то для меня это не выглядит проблемой.</p>
<h2 id="включение">Включение</h2>
<p>Включается BBR очень просто:</p>
<p><code>sysctl net.ipv4.tcp_available_congestion_control</code> показывает список доступных алгоритмов. Скорее всего, по умолчанию BBR там не будет:</p>
<pre tabindex="0"><code>$ sudo sysctl net.ipv4.tcp_available_congestion_control
net.ipv4.tcp_available_congestion_control = reno cubic
</code></pre><p>– это потому, что не загружен соответствующий модуль ядра.</p>
<ol>
<li>Загружаем модуль – <code>modprobe tcp_bbr</code> (или <code>echo tcp_bbr &gt; /etc/modules-load.d/bbr.conf</code>, чтобы он загружался автоматом при старте системы).</li>
<li>Включаем BBR – <code>sysctl net.ipv4.tcp_congestion_control=bbr</code> (или <code>echo 'net.ipv4.tcp_congestion_control = bbr' &gt; /etc/sysctl.d/bbr.conf</code>, чтобы он включался автоматом при старте системы). На всякий случай замечу, что, несмотря на префикс <code>net.ipv4.*</code>, включение происходит как для IPv4, так и для IPv6.</li>
</ol>
<p>При этом его можно включить не для всех, а только для отдельных TCP-соединений, передав опцию <a href="https://man7.org/linux/man-pages/man7/tcp.7.html">TCP_CONGESTION</a> в <a href="https://man7.org/linux/man-pages/man2/setsockopt.2.html">setsockopt(2)</a>. И даже у <code>iperf</code> есть опция <code>-C bbr</code>, с помощью которой можно протестировать поведение различных алгоритмов TCP congestion control конкретно для вашего случая.</p>
<h2 id="last-but-not-least">Last, but not least</h2>
<p>В процессе изучения всего вышеописанного я абсолютно случайно для себя узнал, что в то время как во всех современных дистрибутивах благодаря systemd уже давно в качестве <a href="https://www.linuxjournal.com/content/queueing-linux-network-stack">queueing discipline</a> по умолчанию включена <code>fq_codel</code>, которая считается наиболее оптимальным general purpose вариантом, то в Debian/Ubuntu меинтейнеры не смогли преодолеть свою внутреннюю бюрократию – и даже в самых современных Debian/Ubuntu по умолчанию используется <code>pfifo_fast</code>: они не включают в systemd-пакет его <a href="https://github.com/systemd/systemd/blob/main/sysctl.d/50-default.conf">стандартный конфиг</a>, но в то же время не смогли найти &ldquo;правильное место&rdquo;, куда можно было бы положить аналогичные разумные default&rsquo;ы – и в результате используется значение по умолчанию, которое установлено в ядре.</p>
<p><code>pfifo_fast</code> – это самая простая queueing discipline, которая никак не приоритезирует пакеты между различными сетевыми соединениями, и может получиться так, что самое активное из них будет сильно увеличивать latency всех остальных.</p>
<p>Поэтому рекомендую всем пользователям Debian/Ubuntu добавить <code>/etc/sysctl.d/00-qdisc.conf</code> со следующим содержимым:</p>
<pre tabindex="0"><code>net.core.default_qdisc = fq_codel
</code></pre><p>чтобы исправить это недоразумение.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>gmailctl: наводим порядок в своей почте</title>
      <link>https://konishchev.ru/posts/gmailctl/</link>
      <pubDate>Mon, 08 Apr 2024 19:25:58 +0300</pubDate>
      
      <guid>https://konishchev.ru/posts/gmailctl/</guid>
      <description>&lt;p&gt;У меня довольно большое количество фильтров в Gmail, которое уже давно не влезает на один экран монитора, и при этом интерфейс управления этими фильтрами в Gmail настолько примитивен, что не даёт абсолютно никаких возможностей их хоть как-то организовать. Поэтому каждый раз, когда раньше возникала необходимость добавить новый фильтр (а гораздо хуже – изменить существующий), я мысленно вздыхал – и шёл в этот ужасно неудобный интерфейс&amp;hellip; пока случайно не наткнулся на gmailctl.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>У меня довольно большое количество фильтров в Gmail, которое уже давно не влезает на один экран монитора, и при этом интерфейс управления этими фильтрами в Gmail настолько примитивен, что не даёт абсолютно никаких возможностей их хоть как-то организовать. Поэтому каждый раз, когда раньше возникала необходимость добавить новый фильтр (а гораздо хуже – изменить существующий), я мысленно вздыхал – и шёл в этот ужасно неудобный интерфейс&hellip; пока случайно не наткнулся на gmailctl.</p>
<p><a href="https://github.com/mbrt/gmailctl">gmailctl</a> – это абсолютно потрясающая утилитка, которая позволяет вовсе не использовать web-интерфейс редактирования правил обработки почты и унести их в локальный конфиг. У неё свой язык описания правил (имеющий свои преимущества), который она затем преобразовывает в формат Gmail и заменяет все существующие фильтры на описанные в вашем конфиге (не забыв при этом показать аккуратный diff изменений).</p>
<p>Правила могут быть как очень простые:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  filter: {from: <span style="color:#a31515">&#34;linkedin.com&#34;</span>},
</span></span><span style="display:flex;"><span>  actions: label(<span style="color:#a31515">&#34;LinkedIn&#34;</span>),
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>так и довольно развесистые:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  filter: {or: [
</span></span><span style="display:flex;"><span>    {and: [{from: <span style="color:#a31515">&#34;notifications@github.com&#34;</span>}, {subject: <span style="color:#a31515">&#34;[KonishchevDmitry/&#34;</span>}, {subject: <span style="color:#a31515">&#34;] Release&#34;</span>}]},
</span></span><span style="display:flex;"><span>    {and: [{from: <span style="color:#a31515">&#34;cloud@support.yandex.ru&#34;</span>}, {or: [
</span></span><span style="display:flex;"><span>      {subject: <span style="color:#a31515">&#34;You have been given a grant&#34;</span>},
</span></span><span style="display:flex;"><span>      {subject: <span style="color:#a31515">&#34;Planned maintenance&#34;</span>},
</span></span><span style="display:flex;"><span>    ]}]},
</span></span><span style="display:flex;"><span>    {and: [{from: <span style="color:#a31515">&#34;noreply@market.yandex.ru&#34;</span>}, {or: [
</span></span><span style="display:flex;"><span>      {subject: <span style="color:#a31515">&#34;Вы оформили и оплатили заказ&#34;</span>},
</span></span><span style="display:flex;"><span>      {and: [{subject: <span style="color:#a31515">&#34;Заказ&#34;</span>}, {subject: <span style="color:#a31515">&#34;доставлен&#34;</span>}]},
</span></span><span style="display:flex;"><span>    ]}]},
</span></span><span style="display:flex;"><span>    {and: [{from: <span style="color:#a31515">&#34;mailer@sender.ozon.ru&#34;</span>}, {subject: <span style="color:#a31515">&#34;Вам понравился заказ?&#34;</span>}]},
</span></span><span style="display:flex;"><span>    {and: [{from: <span style="color:#a31515">&#34;mosenergosbyt.ru&#34;</span>}, {subject: <span style="color:#a31515">&#34;Электронный счёт за&#34;</span>}]},
</span></span><span style="display:flex;"><span>  ]},
</span></span><span style="display:flex;"><span>  actions: <span style="color:#00f">delete</span>(),
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Увидев это чудо, я в тот же день переколбасил все свои правила и полностью перешел на gmailctl. Они по-прежнему занимают несколько экранов монитора, но зато теперь их можно организовать удобным мне образом, объявлять функции и переменные, ставить комментарии, отделять отступами и т. п. В общем, всё как в любых нормальных декларативных языках.</p>
<p>Крайне рекомендую. Невероятно полезная и удобная вещь для всех, у кого больше десятка фильтров в Gmail.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Проблемы обратной совместимости glibc</title>
      <link>https://konishchev.ru/posts/glibc-static-linking/</link>
      <pubDate>Wed, 27 Mar 2024 21:57:31 +0300</pubDate>
      
      <guid>https://konishchev.ru/posts/glibc-static-linking/</guid>
      <description>&lt;p&gt;В своей работе я не раз сталкивался с тем, что, собрав Go/Rust-программу на своей рабочей машине и скопировав её на другую, с более старой версией дистрибутива, есть большой шанс того, что она при запуске упадёт с ошибкой вида &lt;code&gt;/lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.38&#39; not found&lt;/code&gt;. Обычно разбираться всегда было некогда, и я просто пересобирал свою программу на нужной версии дистрибутива, но вот тут стало интересно – и я пошёл посмотреть, как скомпилить свою Rust&amp;rsquo;овую программу статически с glibc, чтобы не иметь таких проблем. В результате узнал для себя что-то новое и делюсь своими находками.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>В своей работе я не раз сталкивался с тем, что, собрав Go/Rust-программу на своей рабочей машине и скопировав её на другую, с более старой версией дистрибутива, есть большой шанс того, что она при запуске упадёт с ошибкой вида <code>/lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.38' not found</code>. Обычно разбираться всегда было некогда, и я просто пересобирал свою программу на нужной версии дистрибутива, но вот тут стало интересно – и я пошёл посмотреть, как скомпилить свою Rust&rsquo;овую программу статически с glibc, чтобы не иметь таких проблем. В результате узнал для себя что-то новое и делюсь своими находками.</p>
<h2 id="glibc">glibc</h2>
<p>Первая мысль была довольно предсказуемой и понятной: &ldquo;пойду-ка посмотрю, как собрать статически Rust&rsquo;овый бинарь с glibc&rdquo; – и, на самом деле, особых проблем с этим нет. Надо всего лишь сделать вот так:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>RUSTFLAGS=<span style="color:#a31515">&#39;-C target-feature=+crt-static&#39;</span> cargo build --release --target x86_64-unknown-linux-gnu
</span></span></code></pre></div><p>– и оно работает, программа запускается без проблем!</p>
<p>Но, если покопать эту тему чуть более подробно, то выясняется, что решение это на самом деле – так себе. Всё дело в том, что glibc так устроена, что она в принципе не особо предназначена для статической линковки: из-за <a href="http://man7.org/linux/man-pages/man5/nsswitch.conf.5.html">NSS</a>, <a href="http://man7.org/linux/man-pages/man3/iconv.3.html"><code>iconv(3)</code></a> и пр. она полагается на <a href="http://man7.org/linux/man-pages/man3/dlopen.3.html"><code>dlopen(3)</code></a>, и определённые функции стандартной библиотеки могут стриггерить, скажем, загрузку NSS-модуля, который является динамически разделяемой библиотекой, к тому же динамически слинкованной с glibc, что в свою очередь может привести к ситуации, когда у нас в адресном пространстве приложения будут загружены две glibc: статическая и динамически подгруженная через зависимость NSS-модуля, что в итоге может привести к разным интересным последствиям (к примеру, как они будут делить буферы <code>stdout</code>?). Внутри неё на самом деле есть различные подпорки, чтобы статическая сборка всё-таки нормально работала в большинстве случаев, но вот только гарантий, что она будет работать во всех возможных сценариях – нет.</p>
<p>В итоге, большинство людей сходятся в том, что статическую сборку с glibc лучше не использовать, т. к. мы тут идём против её дизайна и рискуем получить неожиданные последствия от таких действий. И самый оптимальный вариант тут – использовать для сборки Docker-контейнер с каким-нибудь заведомо не самым свежим LTS-дистрибутивом – и линковаться против glibc его версии.</p>
<p>Но также есть и другие варианты.</p>
<h2 id="musl">musl</h2>
<p>Напомню, что Linux – это только ядро, а не операционная система. Интерфейсом к ядру являются системные вызовы, и поэтому ничто не мешает нам вместо glibc использовать что-то другое. Наиболее распространённом вариантом в данном случае является <a href="https://musl.libc.org/">musl</a>.</p>
<p>Честно говоря, я musl до этого момента ни разу не пользовался, т. к. мне всегда казалось довольно странным использовать что-то нестандартное вместо glibc – обязательно ведь где-нибудь что-нибудь будет работать по-другому, и можно нарваться на какие-нибудь неприятные сюрпризы в самый неподходящий на то момент. Поэтому всегда считал, что её использование имеет смысл разве что в embedded, либо где-нибудь вроде <a href="https://www.alpinelinux.org/">Alpine</a>, где нам по какой-то причине хочется получить максимально компактный образ.</p>
<p>Но если смотреть с позиции статической линковки, использование musl вполне имеет смысл, т. к. для неё, в отличие от glibc, статическая линковка является абсолютно стандартным вариантом использования.</p>
<p>Поэтому (в случае Rust) выполняем:</p>
<div class="highlight"><pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>rustup target add x86_64-unknown-linux-musl
</span></span><span style="display:flex;"><span>cargo build --target x86_64-unknown-linux-musl
</span></span></code></pre></div><p>– и получаем то, что нам нужно. Правда, свои &ldquo;но&rdquo; тут тоже есть&hellip;</p>
<p>musl ставит своей целью быть компактной и простой в реализации, и, как это часто бывает, когда кто-то ставит себе такую цель, то порой оказывается, что некоторые вещи в принципе не должны быть простыми, и слишком простая их реализация приводит к проблемам.</p>
<p>В результате чего нередки ситуации, когда наша программа, которая без проблем работала с glibc, вдруг перестаёт работать из-за того, что <a href="https://purplecarrot.co.uk/post/2021-09-04-does_alpine-resolve_dns_properly/">в musl DNS resolver не поддерживает ответы с большим количеством записей</a> (на самом деле уже <a href="https://www.furorteutonicus.eu/2023-10-02-musl-alpine-dns">исправлено</a>), либо работает в десятки (!) раз медленнее – <a href="https://www.linkedin.com/pulse/testing-alternative-c-memory-allocators-pt-2-musl-mystery-gomes/">раз</a>, <a href="https://andygrove.io/2020/05/why-musl-extremely-slow/">два</a>, <a href="https://twitter.com/theomn/status/1149853793636368384">три</a> (как правило, в случае интенсивной аллокации памяти из нескольких потоков одновременно, но также это может быть связано и с тем, что &ldquo;раздутая&rdquo; glibc использует AVX и прочие инструкции для оптимизации своих функций, а &ldquo;простая и компактная&rdquo; musl – нет), либо у вас какая-то специфическая конфигурация, в которой проявляются <a href="https://github.com/gliderlabs/docker-alpine/blob/460819debdada8db435a3619c688a702bdd3420b/docs/caveats.md">отличия в реализации glibc и musl</a>. Ну и, понятное дело, musl не подойдет, если вам нужен NSS (к примеру, в случае с LDAP).</p>
<p>В итоге, я бы сказал, что если у вас в качестве приложения довольно простая command line-утилита, то скорее всего проблем не будет – и для простоты можно статически линковаться с musl, но если у вас какой-то навороченный/высоконагруженный сервис, то либо стоит быть готовым к сюрпризам, либо использовать более сложную конфигурацию вроде musl + <a href="https://github.com/microsoft/mimalloc">mimalloc</a>/<a href="https://jemalloc.net/">jemalloc</a> в качестве аллокатора.</p>
<h2 id="eyra">Eyra</h2>
<p>Если говорить о Rust, то на самом деле есть ещё один интересный вариант – <a href="https://github.com/sunfishcode/eyra/">Eyra</a>. Данный проект ставит своей целью реализовать все функции стандартной библиотеки на Rust и линковаться в процессе сборки с ними.</p>
<p>Этот вариант я, честно говоря, даже не пробовал (уж больно молодой проект), но идея интересная и многообещающая.</p>
<h2 id="интересный-факт">Интересный факт</h2>
<p>Я этого не знал, но, оказывается, ABI системных вызовов стабильный только у Linux. В Windows и MacOS он может меняться абсолютно непредсказуемым образом (даже в минорных версиях), и стабильным является только API стандартной библиотеки (<code>ntdll.dll</code> в Windows и <code>libSystem.dylib</code> в MacOS). И там такой опции у нас вовсе нет.</p>
<p>Go даже поначалу пытался идти тут против течения, но в итоге <a href="https://golang.org/doc/go1.11#runtime">сдался</a>.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Перезапуск блога</title>
      <link>https://konishchev.ru/posts/blog-again/</link>
      <pubDate>Tue, 26 Mar 2024 13:23:06 +0300</pubDate>
      
      <guid>https://konishchev.ru/posts/blog-again/</guid>
      <description>&lt;p&gt;Больше 11 лет прошло с момента моего последнего поста в блоге &lt;a href=&#34;https://konishchevdmitry.blogspot.com/&#34;&gt;KonishchevDmitry&amp;rsquo;s small blog&lt;/a&gt;, который я вёл когда-то давно и достаточно активно.&lt;/p&gt;
&lt;p&gt;И вот какое-то время назад появилось желание попробовать его возродить в том или ином виде, т. к. всё равно время от времени появляются мысли, которыми хотелось бы поделиться в силу того, что они могут быть кому-то полезны. Причём сейчас такое интересное время, что даже если твой пост не смог пробиться сквозь поисковую выдачу, то кто знает – может какая-то его часть сможет обогатить базу знаний условного ChatGPT, и он потом на какие-то вопросы будет отвечать в своей уверенной манере фразами из твоего блога, тем самым таргетированно нанося кому-то пользу. :)&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Больше 11 лет прошло с момента моего последнего поста в блоге <a href="https://konishchevdmitry.blogspot.com/">KonishchevDmitry&rsquo;s small blog</a>, который я вёл когда-то давно и достаточно активно.</p>
<p>И вот какое-то время назад появилось желание попробовать его возродить в том или ином виде, т. к. всё равно время от времени появляются мысли, которыми хотелось бы поделиться в силу того, что они могут быть кому-то полезны. Причём сейчас такое интересное время, что даже если твой пост не смог пробиться сквозь поисковую выдачу, то кто знает – может какая-то его часть сможет обогатить базу знаний условного ChatGPT, и он потом на какие-то вопросы будет отвечать в своей уверенной манере фразами из твоего блога, тем самым таргетированно нанося кому-то пользу. :)</p>
<p>Свободного времени у меня теперь заметно меньше чем раньше, и вряд ли я буду писать сюда часто, но раз желание появилось – то надо попробовать, а там уж посмотрим, что из этого выйдет.</p>
]]></content:encoded>
    </item>
    
  </channel>
</rss>
