<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>My blog_title_here</title><link href="http://blog.fraggod.net/" rel="alternate"/><link href="http://blog.fraggod.net/feeds/atom.xml" rel="self"/><id>http://blog.fraggod.net/</id><updated>2026-03-09T09:39:00+05:00</updated><entry><title>Best multi-rename tool - editing file-list in text $EDITOR</title><link href="http://blog.fraggod.net/2026/03/09/best-multi-rename-tool-editing-file-list-in-text-editor.html" rel="alternate"/><published>2026-03-09T09:39:00+05:00</published><updated>2026-03-09T09:39:00+05:00</updated><author><name>Mike Kazantsev</name></author><id>tag:blog.fraggod.net,2026-03-09:/2026/03/09/best-multi-rename-tool-editing-file-list-in-text-editor.html</id><summary type="html">&lt;p&gt;Tools like &lt;a class="reference external" href="https://github.com/neurobin/rnm"&gt;rnm&lt;/a&gt; and &lt;a class="reference external" href="https://github.com/igrek51/regex-rename"&gt;regex-rename&lt;/a&gt;, or one-liner shell loop like:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
for p in *.mkv; do n=${p##*/}; n=&amp;lt;string-manipulations&amp;gt;...; echo mv &amp;quot;$p&amp;quot; &amp;quot;$n&amp;quot;; done
&lt;/pre&gt;
&lt;p&gt;...is how I usually rename stuff, as a typical shell-for-file-management
terminal-user.&lt;/p&gt;
&lt;p&gt;It's non-interactive and not &amp;quot;visual&amp;quot;, as in you kinda have to imagine results,
then think …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Tools like &lt;a class="reference external" href="https://github.com/neurobin/rnm"&gt;rnm&lt;/a&gt; and &lt;a class="reference external" href="https://github.com/igrek51/regex-rename"&gt;regex-rename&lt;/a&gt;, or one-liner shell loop like:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
for p in *.mkv; do n=${p##*/}; n=&amp;lt;string-manipulations&amp;gt;...; echo mv &amp;quot;$p&amp;quot; &amp;quot;$n&amp;quot;; done
&lt;/pre&gt;
&lt;p&gt;...is how I usually rename stuff, as a typical shell-for-file-management
terminal-user.&lt;/p&gt;
&lt;p&gt;It's non-interactive and not &amp;quot;visual&amp;quot;, as in you kinda have to imagine results,
then think of all regex-replace manipulations you need to get there, type all
those down, run it, check result, adjust regexps, repeat, ...&lt;/p&gt;
&lt;p&gt;This reminds me a lot of how paper-teletype-era &amp;quot;line editors&amp;quot; like &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Ed_(text_editor)"&gt;ed&lt;/a&gt; worked,
and there's a good reason why most people use interactive editors for text
manipulation tasks like that by now instead.&lt;/p&gt;
&lt;p&gt;So in that spirit, as the need to organize some files arose again, decided to write
a small script to easily delegate mass file/dir renames to an $EDITOR (&lt;a class="reference external" href="https://github.com/mk-fg/emacs-setup"&gt;emacs in my
case&lt;/a&gt;) - &lt;a class="reference external" href="https://github.com/mk-fg/fgtk#hdr-dir-edit"&gt;dir-edit&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You give it files/paths to use, or just run in whatever dir, and plaintext
filename-per-line list opens, to do all sorts of search-replaces, cut/copy-and-pastes,
multi-cursor edits, minor specific tweaks, etc. Any text editor is great for that.&lt;/p&gt;
&lt;p&gt;Tool either waits for $EDITOR to exit, or until file is modified (dir-inotify
triggers for file and contents don't match), then prints a diff of old-to-new
file-list contents, with a prompt to confirm all renames there:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
QUERY: [A]pply changes, [R]eload, call [E]ditor again, [W]ait for more edits, [Q]uit/e[X]it
&lt;/pre&gt;
&lt;p&gt;Unlike with non-interactive tools, you've already seen the results, and done
any number of tweaks/corrections by then, so this is mostly a confirmation step,
in case of any unnoticed editing mishaps.&lt;/p&gt;
&lt;p&gt;Files are still checked for name conflicts afterwards (aborting unless
&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;--replace&lt;/span&gt;&lt;/tt&gt; is used), as well as other sanity-checks for recursive renaming
(like missing dst dirs, cross-device renames, etc).&lt;/p&gt;
&lt;p&gt;File-list is simply matched line-by-line by default, i.e. new filename is
expected to be on the same line, but other plaintext option is to tag lines with
a small case-insensitive file-id on the right (e.g. &lt;tt class="docutils literal"&gt;my file name.txt&amp;nbsp; #xwz3&lt;/tt&gt;,
which allows to reorder or add/remove lines when editing, only picking up names
with those tags intact at the end, which shouldn't be easy to mess-up.&lt;/p&gt;
&lt;p&gt;This works great for complicated names of torrented files, which often include
all sorts of information, typically not needed locally after download.&lt;/p&gt;
&lt;p&gt;Bonus advantage is that while aforementioned rnm/regex-rename ed/grep/sed-style
tools do present some kind of &amp;quot;old -&amp;gt; new&amp;quot; info before enacting renames,
those never bother to include trickier colorized inline diffs, which fancy diff-tools
like &lt;a class="reference external" href="https://dandavison.github.io/delta/"&gt;delta&lt;/a&gt; can easily make (and using &amp;quot;delta&amp;quot; or &amp;quot;colordiff&amp;quot; is picked-up by default
if those are somewhere in $PATH), so it kinda beats those at even non-interactive
&amp;quot;one regexp-tweak at a time&amp;quot; usage.&lt;/p&gt;
&lt;blockquote&gt;
Link: &lt;a class="reference external" href="https://github.com/mk-fg/fgtk#hdr-dir-edit"&gt;dir-edit script in mk-fg/fgtk repo&lt;/a&gt; (github) or &lt;a class="reference external" href="https://codeberg.org/mk-fg/fgtk#hdr-dir-edit"&gt;on codeberg&lt;/a&gt;, &lt;a class="reference external" href="https://fraggod.net/code/git/fgtk/about/dir-edit"&gt;in local git&lt;/a&gt;.&lt;/blockquote&gt;
</content><category term="Uncategorized"/><category term="tools"/><category term="python"/><category term="fs"/></entry><entry><title>Using sharp bitmap fonts in modern GIMP</title><link href="http://blog.fraggod.net/2026/01/12/using-sharp-bitmap-fonts-in-modern-gimp.html" rel="alternate"/><published>2026-01-12T16:08:00+05:00</published><updated>2026-01-12T16:08:00+05:00</updated><author><name>Mike Kazantsev</name></author><id>tag:blog.fraggod.net,2026-01-12:/2026/01/12/using-sharp-bitmap-fonts-in-modern-gimp.html</id><summary type="html">&lt;p&gt;When tinkering with something outside PC/laptop screen, I often need a reminder
pad for a wiring schematic or recipe, or even some hotkeys, maps and other quickref-data
when playing games on main display, and I'm using e-ink pad for that,
combining any kind of text/icons/diagrams/images/etc …&lt;/p&gt;</summary><content type="html">&lt;p&gt;When tinkering with something outside PC/laptop screen, I often need a reminder
pad for a wiring schematic or recipe, or even some hotkeys, maps and other quickref-data
when playing games on main display, and I'm using e-ink pad for that,
combining any kind of text/icons/diagrams/images/etc into one reference card
via common &lt;a class="reference external" href="https://www.gimp.org/"&gt;GIMP&lt;/a&gt; image editor.&lt;/p&gt;
&lt;p&gt;Particular pad that I have is a relatively cheap &lt;a class="reference external" href="https://www.waveshare.com/wiki/7.5inch_NFC-Powered_e-Paper"&gt;7.5&amp;quot; WaveShare NFC-powered e-Paper one&lt;/a&gt;
from &lt;a class="reference external" href="https://aliexpress.com/"&gt;AliExpress&lt;/a&gt;, which is thin and light, as it doesn't have a battery, and is updated by
NFC from a phone (quick demo like &lt;a class="reference external" href="https://www.youtube.com/watch?v=nEP4WfomskQ"&gt;this 30s youtube video&lt;/a&gt; might help to get the idea).&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;As an aside, full process of updating such pad goes something like this:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;Cook up whatever thing in GIMP, Ctrl-E there to save 1-bit PNG.&lt;/li&gt;
&lt;li&gt;Grab phone, tap BT and NFC tiles to enable those on it.&lt;/li&gt;
&lt;li&gt;Run &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;bt-obex&lt;/span&gt; &lt;span class="pre"&gt;-p&lt;/span&gt; &lt;span class="pre"&gt;&amp;lt;phone-addr&amp;gt;&lt;/span&gt; image.png&lt;/tt&gt; from shell history (&lt;a class="reference external" href="https://github.com/khvzak/bluez-tools"&gt;bluez-tools&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Tap &amp;quot;Accept&amp;quot; on the phone, run &lt;a class="reference external" href="https://github.com/mk-fg/nfc-epaper-writer"&gt;nfc-epaper-writer&lt;/a&gt; app and &amp;quot;Load Image&amp;quot; there.&lt;/li&gt;
&lt;li&gt;Put phone on the tablet, wait some 5-10s to upload/refresh epaper display image.&lt;/li&gt;
&lt;li&gt;Disable BT/NFC, put phone away, grab the pad and be off to do something with it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Phone works as a cache for images, which can later be put onto pad in one step,
but other than that, eink pad with updates via USB cable would've had less steps.
So NFC power+updates there sounds neat, but not actually that useful.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Upside of eink for this is that whatever info/reminder always stays there to check
anytime (incl. hours, days or months later), without needing free/clean hands to drop
everything and tinker with small phone screen, which has to be clumsily tapped/scrolled,
goes to sleep, discharges and dies, wash hands afterwards, etc.&lt;/p&gt;
&lt;p&gt;But downside of this particular pad at least, is that it's purely 1-bit monochrome,
i.e. not even grayscale, has only black-or-white pixels.
Which tbf has its own charm with &lt;a class="reference external" href="https://tannerhelland.com/2012/12/28/dithering-eleven-algorithms-source-code.html"&gt;dithering&lt;/a&gt; for images (see &lt;a class="reference external" href="https://github.com/makew0rld/didder"&gt;didder&lt;/a&gt; wrapper at the end),
icons/schematics there just don't need color, and large/bold text is perfectly fine
with it too.&lt;/p&gt;
&lt;p&gt;For a long bunch of text or md data tables however (&lt;a class="reference external" href="https://csvmd.com/"&gt;csvmd&lt;/a&gt; can align those nicely),
fonts tend to be small and can look a bit grating when line thickness in them flips
between 1-2px within same glyphs/letters somewhat arbitrarily after scaling.&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://en.wikipedia.org/wiki/Computer_font#Bitmap_fonts"&gt;Monochrome bitmap fonts&lt;/a&gt; don't have that issue, and work great in this
particular use-case, as they were made for similar low pixel density monochrome
displays, where every dot was placed manually for best human-eye legibility
at that exact size.&lt;/p&gt;
&lt;p&gt;Only problem is that they're kinda out of fashion nowadays, as modern
displays don't need them, and tend to be only supported in &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Terminal_emulator"&gt;terminal emulator apps&lt;/a&gt;
(likely because unix people are used to them), embedded programming with 1-bit LED
panels or similar e-paper displays, and retro-styled pixel-art game engines.&lt;/p&gt;
&lt;blockquote&gt;
I still use &lt;a class="reference external" href="https://invisible-island.net/xterm/"&gt;XTerm&lt;/a&gt; as a day-to-day terminal emulator myself (fast -
compatible - familiar), which still uses bitmap fonts by default,
&lt;a class="reference external" href="https://en.wikipedia.org/wiki/Fixed_(typeface)"&gt;misc-fixed 9x18 font&lt;/a&gt; in particular. It still looks great for me on up
to 1080p displays, with incredibly crisp and distinctive letters,
but HiDPI displays probably have that with any vector fonts too.&lt;/blockquote&gt;
&lt;p&gt;GIMP and all apps based on &lt;a class="reference external" href="https://gtk.org/"&gt;GTK toolkit&lt;/a&gt; have &lt;a class="reference external" href="https://gitlab.gnome.org/GNOME/gimp/-/issues/4411"&gt;dropped support for PCF/BDF
bitmap fonts&lt;/a&gt; some years ago in particular (around 2019-ish), which seem to be
most common formats for these, so I was using worse-looking scaled-down
TTFs/OTFs for smaller monochrome text, until finally bothered to lookup
how to fix the issue.&lt;/p&gt;
&lt;p&gt;One obvious fix can be to grab some old GIMP &lt;a class="reference external" href="https://appimage.org/"&gt;AppImage&lt;/a&gt; - e.g.
&lt;a class="reference external" href="https://github.com/aferrero2707/gimp-appimage/releases/"&gt;aferrero2707/gimp-appimage releases&lt;/a&gt; date back to 2018, so should work -
but modern GIMP has nice features too, and jumping back-and-forth between
the two or only sticking to an ancient version seems kinda silly.&lt;/p&gt;
&lt;p&gt;Another (better) fix can be to edit whatever text in emacs, render it out and
paste into GIMP - monobit-banner tool from &lt;a class="reference external" href="https://github.com/robhagemans/monobit"&gt;monobit project&lt;/a&gt; can do that.&lt;/p&gt;
&lt;p&gt;For all its oddities, interactive text editing in GIMP - using multiple boxes,
reflowing, condensing, etc - is still way nicer than that paste-and-see method,
and looking up options, I've stumbled upon a great (and surprisingly recent)
2025 Libre Graphics Meeting (LGM) &lt;a class="reference external" href="https://media.ccc.de/v/lgm25-upstream-2025-83649-let-s-all-go-back-to-bitmap-fonts-"&gt;&amp;quot;Let's All Go Back To Bitmap Fonts!&amp;quot; talk&lt;/a&gt;
by Nathan Willis, which presents a working solution for this particular problem
as a part of it (and covers other issues related to bitmap fonts too) - convert
font to OpenType Bitmap format (OTB, .otb), which is still widely supported.&lt;/p&gt;
&lt;p&gt;So it looks like support for bitmap fonts isn't completely gone yet,
just need to use that specific format instead of more common old PCF files.&lt;/p&gt;
&lt;p&gt;Same &lt;a class="reference external" href="https://github.com/robhagemans/monobit"&gt;monobit&lt;/a&gt; toolkit works great for converting to OTB as well:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
monobit-convert /usr/share/fonts/misc/9x18.pcf.gz \
  set-property family Fixed set-property subfamily 9x18 \
  to fixed-9x18.otb --overwrite
&lt;/pre&gt;
&lt;p&gt;Resulting .otb file can be dropped into ~/.fonts/ and GIMP will pick it up there
(or from any dir under Edit - Preferences - Folders - Fonts), displaying with
&amp;quot;&amp;lt;family&amp;gt; &amp;lt;subfamily&amp;gt;&amp;quot; name in the font selection dialogs, hence overriding
those to &amp;quot;Fixed 9x18&amp;quot; above, to know specific matching height to pick for it.&lt;/p&gt;
&lt;p&gt;GIMP or its underlying font rendering libs also seem smart about scaling these
OTB fonts only in some discrete steps to avoid loosing their distinctive sharp edges,
but there's probably little practical reason to do that - they're already tiny
(e.g. misc-fixed has 4x6 variant), and vector fonts work fine for larger sizes.&lt;/p&gt;
&lt;p&gt;It's a niche use-case for sure, but still nice that all those hand-crafted
pixel-perfect fonts from past decades of computer history seem to still be usable
with modern tools without too much hassle.&lt;/p&gt;
&lt;hr class="docutils" /&gt;
&lt;p&gt;Side-note on dithering - there's a nice non-interactive &lt;a class="reference external" href="https://github.com/makew0rld/didder"&gt;didder&lt;/a&gt; tool for that,
but usually it's even nicer to interactively tweak strength/brightness
parameters for each specific image, depending on its overall contents and what
it will be used for.&lt;/p&gt;
&lt;p&gt;(e.g. background image can probably have less black pixels, although contrast
text halo/outline usually takes care of any foreground-font legibility issues,
but inherent contrast with subdued outlines looks better)&lt;/p&gt;
&lt;p&gt;My basic ad-hoc solution to turning that non-interactive tool into an interactive
one, is to wrap it into a bash script, using &lt;a class="reference external" href="https://gitlab.gnome.org/GNOME/zenity"&gt;zenity&lt;/a&gt; to display a couple sliders
for those parameters:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="ch"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nv"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;dst&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;
&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$src&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$dst&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Usage: &lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt; image.src.png image.png&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;didder&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./didder_1.3.0_linux_64-bit&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;didder_args&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;# Added to --strength N% --brightness M% from zenity&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-x&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;800&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-y&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;480&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;0 255&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$src&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$dst&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;bayer&lt;span class="w"&gt; &lt;/span&gt;32x32&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;feh&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;feh&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;feh_args&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# used to display and auto-reload image on the second screen&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-ZNxsrd.&lt;span class="w"&gt; &lt;/span&gt;-g&lt;span class="o"&gt;=&lt;/span&gt;1920x1080+1920&lt;span class="w"&gt; &lt;/span&gt;-B&lt;span class="w"&gt; &lt;/span&gt;checks&lt;span class="w"&gt; &lt;/span&gt;--info&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;echo -e &amp;quot; [ %t %wx%h %Ppx %SB %z%% ]\n&amp;quot;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;declare&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;last&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;render&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;open&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;t
fdlinecombine&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;zenity&lt;span class="w"&gt; &lt;/span&gt;--title&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;dither strength&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--scale&lt;span class="w"&gt; &lt;/span&gt;--text&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--print-partial&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--step&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--min-value&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--max-value&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;stdbuf&lt;span class="w"&gt; &lt;/span&gt;-oL&lt;span class="w"&gt; &lt;/span&gt;sed&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;s/^/str /&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;zenity&lt;span class="w"&gt; &lt;/span&gt;--title&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;dither brightness&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--scale&lt;span class="w"&gt; &lt;/span&gt;--text&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--print-partial&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--step&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--min-value&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--max-value&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;stdbuf&lt;span class="w"&gt; &lt;/span&gt;-oL&lt;span class="w"&gt; &lt;/span&gt;sed&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;s/^/br /&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-rt&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.5&lt;span class="w"&gt; &lt;/span&gt;t&lt;span class="w"&gt; &lt;/span&gt;val&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;:&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-z&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$val&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;last&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="nv"&gt;$val&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;render&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;t&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-z&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$render&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;render&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;last&lt;/span&gt;&lt;span class="p"&gt;[str]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;last&lt;/span&gt;&lt;span class="p"&gt;[br]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;continue&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;opts&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--strength&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;last&lt;/span&gt;&lt;span class="p"&gt;[str]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;%&lt;span class="w"&gt; &lt;/span&gt;--brightness&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;last&lt;/span&gt;&lt;span class="p"&gt;[br]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;%&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[ &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;%(%F %T)T&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-1&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; ] Render: &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$didder&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;didder_args&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sleep&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.5
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-z&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$open&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$feh&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;feh_args&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;realpath&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$dst&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;open&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It puts &lt;a class="reference external" href="https://feh.finalrewind.org/"&gt;feh&lt;/a&gt; image-viewer on the second screen and auto-reloads images after every tweak,
with bash running &lt;a class="reference external" href="https://github.com/makew0rld/didder"&gt;didder&lt;/a&gt; in an event-loop after some debouncing.
Small non-posix &lt;a class="reference external" href="https://github.com/vi/fdlinecombine"&gt;fdlinecombine&lt;/a&gt; tool is used there to merge parameter updates from
any number of sliders, but can probably be replaced by tail, subshell or something
more generic, I just tend to use it in a pinch for such dynamic-concatenation needs.&lt;/p&gt;
</content><category term="Uncategorized"/><category term="tools"/></entry><entry><title>Safe rm to restrict file removals to be under specified dir</title><link href="http://blog.fraggod.net/2025/11/02/safe-rm-to-restrict-file-removals-to-be-under-specified-dir.html" rel="alternate"/><published>2025-11-02T17:38:00+05:00</published><updated>2025-11-02T17:38:00+05:00</updated><author><name>Mike Kazantsev</name></author><id>tag:blog.fraggod.net,2025-11-02:/2025/11/02/safe-rm-to-restrict-file-removals-to-be-under-specified-dir.html</id><summary type="html">&lt;p&gt;Using standard &lt;a class="reference external" href="https://man.archlinux.org/man/rm.1"&gt;rm(1)&lt;/a&gt; tool in something like a file-backup script,
with any &amp;quot;untrusted&amp;quot; list of paths OR an untrusted dir is wildly unsafe,
but it's kinda frustrating to me that it doesn't have to be.&lt;/p&gt;
&lt;p&gt;On modern linux, &amp;quot;rm&amp;quot; can fairly easily have some &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;--restrict-to-dir&lt;/span&gt;&lt;/tt&gt;
option, which guarantees that …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Using standard &lt;a class="reference external" href="https://man.archlinux.org/man/rm.1"&gt;rm(1)&lt;/a&gt; tool in something like a file-backup script,
with any &amp;quot;untrusted&amp;quot; list of paths OR an untrusted dir is wildly unsafe,
but it's kinda frustrating to me that it doesn't have to be.&lt;/p&gt;
&lt;p&gt;On modern linux, &amp;quot;rm&amp;quot; can fairly easily have some &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;--restrict-to-dir&lt;/span&gt;&lt;/tt&gt;
option, which guarantees that all removed files will be under that dir,
making it much safer to use in a script which needs to e.g. run &lt;a class="reference external" href="https://man.archlinux.org/man/comm.1"&gt;comm(1)&lt;/a&gt;
on some file-lists and remove a bunch of unneded ones from some storage-dir.&lt;/p&gt;
&lt;p&gt;Without such tool, using old &amp;quot;rm&amp;quot; has many semantical and &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use"&gt;TOCTOU&lt;/a&gt; issues:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;Paths can be plain-bad like &lt;tt class="docutils literal"&gt;/etc/passwd&lt;/tt&gt;.&lt;/li&gt;
&lt;li&gt;Sneakier version of that can be &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;/mnt/storage/../../etc/passwd&lt;/span&gt;&lt;/tt&gt;.&lt;/li&gt;
&lt;li&gt;Or what if &lt;tt class="docutils literal"&gt;/mnt/storage/somedir&lt;/tt&gt; is a symlink to &lt;tt class="docutils literal"&gt;/etc&lt;/tt&gt;,
even if path on the list is nominally &lt;tt class="docutils literal"&gt;/mnt/storage/somedir/passwd&lt;/tt&gt;.&lt;/li&gt;
&lt;li&gt;And even if &lt;tt class="docutils literal"&gt;/mnt/storage/somedir&lt;/tt&gt; checks out to be a real dir to stat()
or such, if you run straight-up &amp;quot;rm&amp;quot; or unlink() on that path, it might be
quickly replaced to be a symlink under that.&lt;/li&gt;
&lt;li&gt;Relative paths add another layer of mess into this.&lt;/li&gt;
&lt;li&gt;In addition to symlinks there are also mountpoints, which do same thing
too, although in less potential scenarios.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To fix all of these issues, &lt;a class="reference external" href="https://lwn.net/Articles/796868/"&gt;linux has openat2() syscall since 5.6&lt;/a&gt;,
which supports using following simple pattern to avoid everything listed above:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p class="first"&gt;Given some &lt;tt class="docutils literal"&gt;path&lt;/tt&gt;, run &lt;tt class="docutils literal"&gt;p = realpath(path)&lt;/tt&gt; on it.&lt;/p&gt;
&lt;p&gt;So it either resolves to a canonical absolute form, with no-symlink
components separated by single slashes in there, or immediately returns errno
code if it's missing or inaccessible.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Check all restrictions on that canonical path &lt;tt class="docutils literal"&gt;p&lt;/tt&gt;, e.g. whether it's under
realpath of the dir you want it to be (&amp;quot;realpaths&amp;quot; are nicely string-comparable).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Run &lt;tt class="docutils literal"&gt;fd = openat2(p, RESOLVE_NO_SYMLINKS)&lt;/tt&gt; to open that path (with optional
&lt;tt class="docutils literal"&gt;RESOLVE_NO_XDEV&lt;/tt&gt; also in there to prevent racy mountpoints), and only use that
&lt;tt class="docutils literal"&gt;fd&lt;/tt&gt; for the file/dir/etc from now on.&lt;/p&gt;
&lt;p&gt;Error here will indicate that something changed since realpath() was used,
and you either have to run it again (where realpath() will likely fail too),
or treat that as an special &amp;quot;file vanished&amp;quot; error.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Afaik this should shutdown any symlink-related race-conditions, as openat2()
ensures that realpath you check is the one you'll end up opening, with nothing
redirecting it in-between these calls.&lt;/p&gt;
&lt;p&gt;For removing files in &amp;quot;rm&amp;quot; tool, you don't really &amp;quot;open&amp;quot; files themselves,
instead open their dirs - e.g. produced by realpath(dirname(file)) -
with same exact check-sequence, and unlinkat() the name there.&lt;/p&gt;
&lt;p&gt;So using openat2() + unlinkat() combo instead of direct unlink(file) allows
to introduce &amp;quot;make sure you only remove stuff under &amp;lt;this-dir&amp;gt;&amp;quot; safety restriction,
which can be nice even to just protect against typos and accidental spaces in
human-input paths (see many examples like &lt;tt class="docutils literal"&gt;rm &lt;span class="pre"&gt;-rf&lt;/span&gt; /usr &lt;span class="pre"&gt;/lib/nvidia-current/xorg/xorg&lt;/span&gt;&lt;/tt&gt;
&lt;a class="reference external" href="https://github.com/MrMEEE/bumblebee-Old-and-abbandoned/issues/123"&gt;in bumblebee years ago&lt;/a&gt;), but especially useful in a script or tool working
with some specific storage dir, which is very common.&lt;/p&gt;
&lt;p&gt;Given proliferation of &amp;quot;rewrite in rust&amp;quot; tools and learning projects, tried
looking up some version of &amp;quot;rm&amp;quot; already doing something like that,
but failed to find one - seems hard enough to even find anyone using openat2(),
despite it being in the kernel for 5+ years by now.
Most &amp;quot;safe rm&amp;quot; tools are for moving files into some kind of &amp;quot;trash&amp;quot; dir,
with somewhat different user-interactive use-case (regret after removal) and priorities.&lt;/p&gt;
&lt;p&gt;As usual, ended up writing it for myself - &lt;a class="reference external" href="https://github.com/mk-fg/fgtk#hdr-rmx.c"&gt;rmx.c in fgtk repo&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It's intended to be used with &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;-d&lt;/span&gt; &amp;lt;dir&amp;gt;&lt;/tt&gt; option, does realpath on that dir and
checks all files' parent dir realpaths against that prefix before removing anything,
uses RESOLVE_NO_SYMLINKS by default, but also has &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;-x&lt;/span&gt;&lt;/tt&gt; option for cross-mount checks.&lt;/p&gt;
&lt;p&gt;For example: &lt;tt class="docutils literal"&gt;rmx &lt;span class="pre"&gt;-f&lt;/span&gt; &lt;span class="pre"&gt;-d&lt;/span&gt; /mnt/storage &lt;span class="pre"&gt;--&lt;/span&gt; &lt;span class="pre"&gt;&amp;quot;${file_list[&amp;#64;]}&amp;quot;&lt;/span&gt;&lt;/tt&gt;&lt;/p&gt;
&lt;p&gt;Doesn't have recursive mode, as I don't really need it atm, and that one
probably has its own bunch of caveats.&lt;/p&gt;
&lt;hr class="docutils" /&gt;
&lt;p&gt;Other general ways to fix similar issues is chroot(), using mount namespaces,
LSM profiles (SELinux/AppArmor/LandLock/etc), idmapping + special uid/gid for that,
and other sandboxing-adjacent techniques.&lt;/p&gt;
&lt;p&gt;That seems excessively complicated for a humble &amp;quot;rm &amp;lt;files&amp;gt;&amp;quot; command,
and they're often lacking in different ways (e.g. chroot is leaky and requires root),
but can be useful to wrap anything more complex that deals with paths a lot into
(like for a &lt;a class="reference external" href="https://codeberg.org/mk-fg/fuse-auto-cleanup-fs"&gt;fuse-filesystem overlay like acfs&lt;/a&gt;, where fixing every access to be
sanitized like this is a bit more work).&lt;/p&gt;
</content><category term="Uncategorized"/><category term="c"/><category term="linux"/><category term="tools"/><category term="sysadmin"/><category term="fs"/></entry><entry><title>Algorithm for N exponentially-spaced retries within timeout T</title><link href="http://blog.fraggod.net/2025/09/15/algorithm-for-n-exponentially-spaced-retries-within-timeout-t.html" rel="alternate"/><published>2025-09-15T14:14:00+05:00</published><updated>2025-09-15T14:14:00+05:00</updated><author><name>Mike Kazantsev</name></author><id>tag:blog.fraggod.net,2025-09-15:/2025/09/15/algorithm-for-n-exponentially-spaced-retries-within-timeout-t.html</id><summary type="html">&lt;p&gt;When retrying some failed check or operation, common ways to algorithmically
wait for next attempt seem to be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p class="first"&gt;Make N retries with a static interval in-between, e.g. &amp;quot;retry every 10s&amp;quot;.&lt;/p&gt;
&lt;p&gt;Works well when it's not a problem to retry too often, or need to know when
stuff starts working …&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;</summary><content type="html">&lt;p&gt;When retrying some failed check or operation, common ways to algorithmically
wait for next attempt seem to be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p class="first"&gt;Make N retries with a static interval in-between, e.g. &amp;quot;retry every 10s&amp;quot;.&lt;/p&gt;
&lt;p&gt;Works well when it's not a problem to retry too often, or need to know when
stuff starts working again ASAP (like within 10s), and when expected downtime
is predictable (something like minutes with 10s retries, not &amp;lt;1s or days/months).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Use exponential backoff to avoid having to predict availability/downtime.&lt;/p&gt;
&lt;p&gt;Idea is to multiply interval each time, so it can start with 1s and rises up
quickly as exponential curve does, reacting well to both quick outages and
at the same time not still retrying needlessly every 10s after days.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Exponential backoff can quickly get out of hand, so cap its max interval.&lt;/p&gt;
&lt;p&gt;Good tweak to set how-late-at-most working state will be detected again.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But vast majority of use-cases for retries I have seem to also need &amp;quot;give up
after some time&amp;quot; limit, so for example a tool fetching a file, will fail with
an error in a few minutes, which I can detect and see if it can be fixed in some
other way than just retrying endlessly (as in give router a kick or something).&lt;/p&gt;
&lt;p&gt;That timeout usually seems like the most important part to set for me - i.e.
&amp;quot;I expect this to finish within 20m, so keep checking and process results when
available&amp;quot;.&lt;/p&gt;
&lt;p&gt;And above algortithms don't work too well under this constraint:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p class="first"&gt;Fixed-interval attempts can be spaced evenly within timeout, but that's
usually suboptimal for same reasons as already mentioned - couple quick
initial attempts/retries are often more useful, and there's no point trying
too often after a while.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;People - me included - are notoriously bad at understanding exponentials,
so it's nigh-impossible to tell with any exponential delay how many attempts
it'd imply within known timeout.&lt;/p&gt;
&lt;p&gt;That is, depending on exact formula, could be that intervals will go up past
half of the timeout span fast and become effectively useless, not trying enough,
or otherwise make too many needless attempts throughout when timeout is known
to be quite long.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Long-time solution I had for this rate-limiting use-case is to not pick
exponential function effectively &amp;quot;at random&amp;quot; or &amp;quot;by intuition&amp;quot; (which again seems
difficult to get right), but instead use a simple chunk of code to calculate it.&lt;/p&gt;
&lt;p&gt;Idea is to be able to say &amp;quot;I want 20 more-and-more delayed retries within 15m&amp;quot;
or &amp;quot;make 100 attempts in 1h&amp;quot; and not worry about further details, where backoff
function constants will be auto-picked to space them out nicely within that interval.&lt;/p&gt;
&lt;p&gt;Specific function I've used in dozens of scripts where such &amp;quot;time-capped retries&amp;quot;
are needed goes something like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;retries_within_timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;tries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;backoff_func&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;slack&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt; &lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="s1"&gt;&amp;#39;Return list of delays to make exactly n retries within timeout&amp;#39;&lt;/span&gt;
  &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;
  &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="n"&gt;delays&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backoff_func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tries&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delays&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;slack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;delays&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Sometimes with adjusted backoff_func or slack value, depending on specific use.&lt;/p&gt;
&lt;p&gt;For e.g. &amp;quot;10 retries within 60s timeout&amp;quot; it'd return a list with delays like this:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
0.0, 0.3, 0.8, 1.5, 2.5, 4.0, 6.2, 9.4, 14.2, 21.1
&lt;/pre&gt;
&lt;p&gt;I.e. retry immediately, then wait 0.3s, then 0.8s, and up to 14s or 21s towards
the end, when it's more obviously not a thing that's fixable by a quick retry.&lt;/p&gt;
&lt;p&gt;As all these will sum up to 60s (with just ±0.01s slack), they can be easily
plugged into a for-loop like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;retries_within_timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;everything_works&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Stuff failed to work within 60s&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Without then needing any extra timeout checks or a wrapper to stop the loop after,
hence also quite convenient and expressive language abstraction imo.&lt;/p&gt;
&lt;p&gt;There's probably an analytic solution for this, to have exact formulas for
calculating coefficients for backoff_func, that'd produce a set of N delays to
fit precisely within timeout, but I've never been interested enough to find
one when it's trivial to find &amp;quot;close enough&amp;quot; values algorithmically via bisection,
like that function does.&lt;/p&gt;
&lt;p&gt;Also usually there's no need to calculate such delays often, as they're either
static or pre-configured, or used on uncommon error-handling code-paths, so having
a heavier solution-bruteforcing algorithm isn't an issue.&lt;/p&gt;
&lt;p&gt;Not sure why, but don't think I've bumped into this kind of &amp;quot;attempts within timeout&amp;quot;
configuration/abstractions anywhere, so might be a quirk of how I think of rate-limits
in my head, or maybe it's just rarely considered to be worth adding 10 lines of code over.&lt;/p&gt;
&lt;p&gt;Still, seem to make much more intuitive sense to me than other common limits above.&lt;/p&gt;
&lt;hr class="docutils" /&gt;
&lt;p&gt;Recently I've also wanted to introduce a min-delay constraint there, which ideally
will discard those initial short-delay values in the example list above, and pick ones
more to the right, i.e. won't just start with delay_min + 0.0, 0.3, 0.8, ... values.&lt;/p&gt;
&lt;p&gt;This proved somewhat error-prone for such bruteforce algorithm, because often
there's simply no curve that fits neatly within narrow-enough min-max range of
values that all sum up to specified timeout, so had to make algo a bit more complicated:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;retries_within_timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delay_min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slack&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="s1"&gt;&amp;#39;Return list of delays to make exactly n retries within timeout&amp;#39;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tries&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;delay_min&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;tries * delay_min ~&amp;gt; timeout&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tries&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay_min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
  &lt;span class="c1"&gt;# delay_calc is picked to produce roughly [0, m] range with n=[1, tries] inputs&lt;/span&gt;
  &lt;span class="n"&gt;delay_calc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;_d&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;tries&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;tries&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;_d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delay_min&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;slack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;slack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;delay_min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;tries&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;bisect_step&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
      &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delays&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delays&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;tries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;td&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;delay_calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;delay_min&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;delays&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;td&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nb"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delays&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;slack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;delays&lt;/span&gt;
      &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;
  &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;OverflowError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;pass&lt;/span&gt; &lt;span class="c1"&gt;# [tries*delay_min, timeout] range can be too narrow&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;delay_min&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;slack&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay_min&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;td&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;td&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt;
    &lt;span class="n"&gt;retries_within_timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;delay_min&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tries&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;slack&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;slack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;(all this is a lot less confusing when graphed, either as values from delay_calc
or sum-of-delays-up-to-N, which can be easy to do with &lt;a class="reference external" href="https://github.com/mk-fg/fgtk/blob/master/metrics/d3-line-chart-boilerplate.html"&gt;a bit of HTML/JS&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;As per comment in the code, delay_calc is hardcoded to produce predictable output
range for known &amp;quot;attempt number&amp;quot; inputs, and a couple pathological-case checks
are needed to avoid asking for impossible values (like &amp;quot;fit 10 retries in 10s
with min 3s intervals&amp;quot; so 10*3 = 30s within 10s) or impossible quadratic curve shapes
(for which there're couple fallbacks at the end).&lt;/p&gt;
&lt;p&gt;Default &lt;tt class="docutils literal"&gt;e=10&lt;/tt&gt; exponent base value makes delays more evenly-spaced,
which get more samey with lower e-values or rise more sharply with higher ones,
e.g. &lt;tt class="docutils literal"&gt;e=1000&lt;/tt&gt; will change result like this:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
retries_within_timeout(10, 60, e=10)  : 0.0, 0.6, 1.4, 2.4, 3.6, 5.2, 7.2, 9.6, 12.8, 16.7
retries_within_timeout(10, 60, e=1000): 0.0, 0.1, 0.2, 0.4, 0.9, 1.9, 3.8, 7.6, 15.2, 30.4
&lt;/pre&gt;
&lt;p&gt;So seem to be good enough for occasional tweaks instead of replacing whole delay_calc formula.&lt;/p&gt;
</content><category term="Uncategorized"/><category term="python"/><category term="rate-limiting"/></entry><entry><title>Modern conky-like ambient network connection monitoring tool</title><link href="http://blog.fraggod.net/2025/08/20/modern-conky-like-ambient-network-connection-monitoring-tool.html" rel="alternate"/><published>2025-08-20T04:06:00+05:00</published><updated>2025-08-20T04:06:00+05:00</updated><author><name>Mike Kazantsev</name></author><id>tag:blog.fraggod.net,2025-08-20:/2025/08/20/modern-conky-like-ambient-network-connection-monitoring-tool.html</id><summary type="html">&lt;p&gt;As a long-time &lt;a class="reference external" href="https://conky.cc/"&gt;conky&lt;/a&gt; user, always liked &amp;quot;system status at a glance&amp;quot; visibility
it provides, so you're pretty much always aware of what's normal resource usage
pattern for various apps and system as a whole, and easily notice anything odd there.&lt;/p&gt;
&lt;p&gt;(though mostly kept conky config same since &lt;a class="reference external" href="/2014/05/19/displaying-any-lm_sensors-data-temperature-fan-speeds-voltage-etc-in-conky.html"&gt;last post …&lt;/a&gt;&lt;/p&gt;</summary><content type="html">&lt;p&gt;As a long-time &lt;a class="reference external" href="https://conky.cc/"&gt;conky&lt;/a&gt; user, always liked &amp;quot;system status at a glance&amp;quot; visibility
it provides, so you're pretty much always aware of what's normal resource usage
pattern for various apps and system as a whole, and easily notice anything odd there.&lt;/p&gt;
&lt;p&gt;(though mostly kept conky config same since &lt;a class="reference external" href="/2014/05/19/displaying-any-lm_sensors-data-temperature-fan-speeds-voltage-etc-in-conky.html"&gt;last post here about sensors data in 2014&lt;/a&gt;,
only updating it to work for new lua syntax and to use lm-sensors cli tool json
output for data from those instead of an extra binary)&lt;/p&gt;
&lt;p&gt;One piece that's been missing for me however, is visibility into apps' network usage -
it's kinda important to know which apps have any kind of &amp;quot;unexpected&amp;quot; connectivity,
like telemetry tracking, &amp;quot;cloud&amp;quot; functionality, or maybe some more sinister
security/privacy issues and leaks even.&lt;/p&gt;
&lt;hr class="docutils" /&gt;
&lt;p&gt;This use-case requires a bunch of filtering/grouping and configurability in general,
as well as getting more useful information about processes initiating connections
than what common &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Netstat"&gt;netstat&lt;/a&gt; / &lt;a class="reference external" href="https://man.archlinux.org/man/ss.8"&gt;ss&lt;/a&gt; tools or conky's /proc/net/tcp lines provide.&lt;/p&gt;
&lt;p&gt;I.e. usual linux 16-byte /proc/&amp;lt;pid&amp;gt;/comm is not good enough, as it just says
&amp;quot;python3&amp;quot;, &amp;quot;ssh&amp;quot;, &amp;quot;curl&amp;quot; or some kind of &amp;quot;socket thread&amp;quot; most of the time in practice,
while I'd want to know which systemd service/scope/unit and uid/user it was started from.&lt;/p&gt;
&lt;p&gt;So that &amp;quot;curl&amp;quot; ran by some game launcher, started from a dedicated gaming-uid
in a game-specific flatpak scope isn't displayed same as &amp;quot;curl&amp;quot; that I run from
terminal or that some system service runs, having all that user/cgroup/etc info
with it front-and-center.&lt;/p&gt;
&lt;p&gt;And wrt configurability - over time there's a lot of normal-usage stuff that
doesn't need to draw much attention, like regular ssh sessions, messengers,
imap, ntp or web browser traffic, which can be noisy while also being least
interesting and important (beyond &amp;quot;it's all there as usual&amp;quot;), but is entirely
user-specific, same as most useful data to display in a conky window for your system.&lt;/p&gt;
&lt;p&gt;Main idea is to have a good mental image of what's &amp;quot;normal&amp;quot; wrt network usage
for local apps, and when e.g. running something new, like a game or a flatpak,
to be able to instantly tell whether it's connecting somewhere and when
(and &lt;a class="reference external" href="https://github.com/mk-fg/systemd-cgroup-nftables-policy-manager"&gt;might need a firewall rule&lt;/a&gt; or &lt;a class="reference external" href="https://github.com/mk-fg/fgtk/blob/master/bpf/cgroup-skb.nonet.c"&gt;net-blocking bpf loaded in its cgroup&lt;/a&gt;),
or is completely offline.&lt;/p&gt;
&lt;p&gt;Haven't found any existing application that does something like this well,
and especially in this kind of &amp;quot;background desktop widget&amp;quot; way as conky does,
but most notable ones for such use-case are systemd-cgtop (comes with systemd),
&lt;a class="reference external" href="https://atoptool.nl/"&gt;atop&lt;/a&gt;'s network monitoring tab (usable with an extra &lt;a class="reference external" href="https://github.com/bytedance/netatop-bpf"&gt;netatop-bpf&lt;/a&gt; component)
and &lt;a class="reference external" href="https://github.com/evilsocket/opensnitch"&gt;OpenSnitch&lt;/a&gt; interactive-firewall app.&lt;/p&gt;
&lt;p&gt;All of course are still for quite different uses, although atop can display a lot
of relevant process information nowadays, like cgroups and full process command.
But its network tab is still a TUI for process traffic counters, which I don't
actually care much about - focus should be on new connections made from new places
and filtering/tailoring.&lt;/p&gt;
&lt;hr class="docutils" /&gt;
&lt;p&gt;Eventually got around to look into this missing piece and writing an app/widget
covering this maybe odd and unorthodox use-case, named in my usual &amp;quot;relevant words
that make an acronym&amp;quot; way - &lt;a class="reference external" href="https://github.com/mk-fg/linux-ebpf-connection-overseer"&gt;linux-ebpf-connection-overseer&lt;/a&gt; or &amp;quot;leco&amp;quot;.&lt;/p&gt;
&lt;p&gt;Same as netatop-bpf and OpenSnitch, it uses eBPF for network connection monitoring,
which is surprisingly simple tool for that - all TCP/UDP connections pass through
same two send/recv tracepoints (that netatop-bpf also uses), with kernel &amp;quot;struct sock&amp;quot;
having almost all needed network-level info, and hooks can identify process/cgroup
running the operation, so it's all just there.&lt;/p&gt;
&lt;p&gt;But eBPF is of course quite limited by design:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;Runs in kernel, while app rendering stuff on-screen has to be in userspace.&lt;/li&gt;
&lt;li&gt;Should be very fast and not do anything itself, to not affect networking in any way.&lt;/li&gt;
&lt;li&gt;Has to be loaded into kernel by root.&lt;/li&gt;
&lt;li&gt;Needs to export/stream data from kernel to userspace, passing its
root-only file descriptors of data maps to an unprivileged application.&lt;/li&gt;
&lt;li&gt;Terse data collected in-kernel, like process and cgroup id numbers has to be
expanded into human-readable names from /proc, /sys/fs/cgroup and such sources.&lt;/li&gt;
&lt;li&gt;Must be written in a subset of C or similar low-level language, unsuitable for other purposes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So basically can't do anything but grab all relevant id/traffic numbers
and put into table/queue for userspace to process and display later.&lt;/p&gt;
&lt;p&gt;Solution to most of these issues is to have a system-level service that
loads eBPF hooks and pulls data from there, resolves/annotates all numbers
and id's into/with userful names/paths, formats in a nice way,
manages any useful caches, etc.&lt;/p&gt;
&lt;p&gt;Problem is that this has to be a &lt;strong&gt;system&lt;/strong&gt; service, like a typical daemon,
where some initial things are even done as root, and under some kind of system
uid beyond that, while any kind of desktop widget would run in a particular
user session/container, ideally with no access to most of that system-level stuff,
and with a typical GUI eventloop to worry about instead.&lt;/p&gt;
&lt;p&gt;Which is how this app ended up with 3 separate main components:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;eBPF code linked into one &amp;quot;loader&amp;quot; binary.&lt;/li&gt;
&lt;li&gt;&amp;quot;pipe&amp;quot; script that reads from eBPF maps and outputs nicely-formatted event info.&lt;/li&gt;
&lt;li&gt;Desktop widget that reads event info lines and displays those in a configurable way.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a class="reference external" href="https://github.com/mk-fg/linux-ebpf-connection-overseer"&gt;README in the project repository&lt;/a&gt; should have a demo video, a better overview
of how it all works together, and how to build/configure and run those.&lt;/p&gt;
&lt;hr class="docutils" /&gt;
&lt;p&gt;Couple things I found interesting about these components, in no particular order:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p class="first"&gt;Nice thing about such breakdown is that first two components (eBPF + pipe)
can run anywhere and produce an efficient overview for some remote system,
VM or container - e.g. can have multiple widgets to look at things happening
in home network for example, not just local machine, though I haven't used that yet.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Another nice thing is that each component can use a language suitable for it,
i.e. for kernel hooks old C is perfectly fine, but it's hard to beat python
as a systems language for an eventloop doing data mangling/formatting,
and for a graphical widget doing custom liteweight font/effects rendering,
a modern low-level language like Nim with fast graphics abstraction lib like &lt;a class="reference external" href="https://libsdl.org/"&gt;SDL&lt;/a&gt;
is the best option, to avoid using significant cpu/memory for a background/overlay
window drawing 60+ frames per second (when visible and updating at least).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Separate components also make is easier to tweak or debug those separately, like
for changing eBPF hooks or data formatting, it's easy to run loader and/or python
script and look at its stdout, or read maps via &lt;a class="reference external" href="https://bpftool.dev/"&gt;bpftool&lt;/a&gt; in case of odd output.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;systemd makes it quite easy to do &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;ExecStart=+...&lt;/span&gt;&lt;/tt&gt; to run eBPF loader as root
and then only pass map file descriptors from that to an unprivileged data-pipe script
(with &lt;tt class="docutils literal"&gt;DynamicUser=yes&lt;/tt&gt; and full sandboxing even), &lt;a class="reference external" href="https://github.com/mk-fg/linux-ebpf-connection-overseer/blob/master/leco%40.service"&gt;all defined and configured
within one .service ini-file&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Not having to use large GUI framework like GTK/QT for graphical widget was quite nice
and refreshing, as those are hellishly complicated, and seem poorly suitable for a
custom things like semi-transparent constantly-updating information overlays anyway
(while adding a ton of unnecessary complexity and maintenance burden).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Most surprising thing was probably that pretty much whole configuration language
for all filtering and grouping &lt;a class="reference external" href="https://github.com/mk-fg/linux-ebpf-connection-overseer#hdr-regular_expressions_in_rx-_sections"&gt;ended up fitting nicely into a list of regexps&lt;/a&gt;,
as displayed network info is just text lines, so regexp-replacing specific string-parts
in those to look nicer or to pick/match things to group by is what regexps do best.&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://github.com/mk-fg/linux-ebpf-connection-overseer/blob/master/widget.ini"&gt;widget.ini&lt;/a&gt; config in project repo has ones that I use and some description,
in addition to README sections there.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Making it configurable how visual effects behave over time is quite easy by using
a list of e.g. &amp;quot;time,transparency ...&amp;quot; values, with some smooth curve auto-connecting
those dots, to only need to specify points where direction changes.&lt;/p&gt;
&lt;p&gt;A &lt;a class="reference external" href="https://github.com/mk-fg/linux-ebpf-connection-overseer/blob/master/spline-editor.html"&gt;simple HTML file&lt;/a&gt; to open in browser allows to edit such curves easily,
like for example making info for new connections quickly fade-in and the
fade-out in a few smooth steps, to easily spot which ones are recent or older.&lt;/p&gt;
&lt;p&gt;I think in gamedev this way of specifying effect magnitude over time is often
&lt;a class="reference external" href="https://www.gamedeveloper.com/programming/jugglers-and-tweens"&gt;referred to as &amp;quot;tweening&amp;quot; or &amp;quot;tweens&amp;quot;&lt;/a&gt; (as in what happens in-between specific
states/sprites).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Was thinking to add fancier effects for the tool, but then realized that the
more plain and non-distracting it looks the better, as it's supposed to be in the
background, not something eye-catching, and smooth easing-in/out is already good for that.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Nice text outline/shadow doesn't actually require blur or any extra pixel-processing,
can just stamp same glyphs in black with -1,-1 then 1,1 and 2,2 offsets plus some
transparency, and it's good enough, esp. for readability over colorful backgrounds.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Always-on-top semi-transparent non-interactable vertical overlay window fits quite
well on the second screen, where most stuff is read-only and unimportant anyway.
Works fine as a typical desktop-background window like conky as well.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;C dependencies that are statically-linked-in during build seem to work fairly
well as &lt;a class="reference external" href="https://git-scm.com/book/en/v2/Git-Tools-Submodules"&gt;git submodules&lt;/a&gt;, being very obvious and explicit, pinned to a specific
supported version, and are easy enough to manage via command line.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Network accounting is quite complicated as usual, &lt;a class="reference external" href="https://github.com/mk-fg/linux-ebpf-connection-overseer#hdr-which_traffic_gets_detected_counted"&gt;hard to even describe in the
README&lt;/a&gt; precisely but succinctly, with all the quirks and caveats there.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Nice DNS names are surprisingly not that important for such overview, as it's
usually fairly obvious where each app connects, especially with the timing of it
(e.g. when clicking some &amp;quot;connect&amp;quot; button or running git-push in a terminal),
and most of the usual connections are easy to regexp-replace with better-than-DNS
names anyway (like say &amp;quot;IRC&amp;quot; instead of whatever longer name).&lt;/p&gt;
&lt;p&gt;Should still be easy enough to fill those in by e.g. adding a python resolver
module to a local &lt;a class="reference external" href="https://www.nlnetlabs.nl/projects/unbound/"&gt;unbound&lt;/a&gt; cache, which would cache queries passing through it
by-IP, and then resolve some special queries with encoded IPs back to names,
which should be way simpler and accurate than getting those from traffic inspection
(esp. with apps using DNS-over-TLS/HTTPS protocols).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Kernel &amp;quot;sock&amp;quot; structs have a nice unique monotonic skc_cookie id number, but it's
basically unusable in tracepoints because it's lazily generated at the worst time,
and bpf_get_socket_cookie helper isn't available there, damnit.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Somewhat surprisingly never bumped into info about eBPF code licensing -
is it linking against kernel's GPL code, interpreted code on top of it,
maybe counts as part of the kernel in some other way?&lt;/p&gt;
&lt;p&gt;Don't particularly care, using GPL is fine and presumably avoids any issues there,
but it just seems like a hairy subject that should've been covered to death somewhere.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Links above all point to &lt;a class="reference external" href="https://github.com/mk-fg/linux-ebpf-connection-overseer"&gt;project repository on github&lt;/a&gt; but it can be also be
found on &lt;a class="reference external" href="https://codeberg.org/mk-fg/linux-ebpf-connection-overseer"&gt;codeberg&lt;/a&gt; or &lt;a class="reference external" href="https://fraggod.net/code/git/linux-ebpf-connection-overseer"&gt;self-hosted&lt;/a&gt;, as who knows how long github will still
be around and not enshittified into the ground.&lt;/p&gt;
</content><category term="Uncategorized"/><category term="desktop"/><category term="monitoring"/><category term="tools"/><category term="linux"/><category term="c"/><category term="nim"/><category term="python"/></entry><entry><title>Migrating from GNU Screen to tmux with same workflow</title><link href="http://blog.fraggod.net/2025/05/14/migrating-from-gnu-screen-to-tmux-with-same-workflow.html" rel="alternate"/><published>2025-05-14T17:57:00+05:00</published><updated>2025-05-14T17:57:00+05:00</updated><author><name>Mike Kazantsev</name></author><id>tag:blog.fraggod.net,2025-05-14:/2025/05/14/migrating-from-gnu-screen-to-tmux-with-same-workflow.html</id><summary type="html">&lt;p&gt;My pattern for using these shell wrappers / terminal multiplexers
for many years have been something like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p class="first"&gt;Run a single terminal window, usually old &lt;a class="reference external" href="https://invisible-island.net/xterm/"&gt;XTerm&lt;/a&gt;, sometimes &lt;a class="reference external" href="https://www.enlightenment.org/about-terminology"&gt;Terminology&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Always run &lt;a class="reference external" href="https://github.com/tmux/tmux/wiki"&gt;tmux&lt;/a&gt; inside that window, with a C-x (ctrl-x) prefix-key.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Open a couple local tmux windows (used as &amp;quot;terminal tabs&amp;quot;) there.&lt;/p&gt;
&lt;p&gt;Usually …&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;</summary><content type="html">&lt;p&gt;My pattern for using these shell wrappers / terminal multiplexers
for many years have been something like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p class="first"&gt;Run a single terminal window, usually old &lt;a class="reference external" href="https://invisible-island.net/xterm/"&gt;XTerm&lt;/a&gt;, sometimes &lt;a class="reference external" href="https://www.enlightenment.org/about-terminology"&gt;Terminology&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Always run &lt;a class="reference external" href="https://github.com/tmux/tmux/wiki"&gt;tmux&lt;/a&gt; inside that window, with a C-x (ctrl-x) prefix-key.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Open a couple local tmux windows (used as &amp;quot;terminal tabs&amp;quot;) there.&lt;/p&gt;
&lt;p&gt;Usually a root console (via &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Su_(Unix)"&gt;su&lt;/a&gt;), and one or two local user consoles.&lt;/p&gt;
&lt;p&gt;Run &amp;quot;screen&amp;quot; (as in &lt;a class="reference external" href="https://www.gnu.org/software/screen/"&gt;GNU Screen&lt;/a&gt;) in all these &amp;quot;tabs&amp;quot;, with its default C-a
prefix-key, and opening more screen-windows whenever I need to run something new,
and current console/prompt is either busy, has some relevant history, or e.g.
being used persistently for something specific in some project dir.
This is for effectively infinite number of local shells within a single &amp;quot;tmux tab&amp;quot;,
easy to switch between via C-a n/p, a (current/last) or number keys.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Whenever I need to ssh into something, which is quite often - remote hosts
or local VMs - always open a new local-tmux tab, ssh into the thing, and always
run &amp;quot;screen&amp;quot; on the other end as the first and only &amp;quot;raw&amp;quot; command.&lt;/p&gt;
&lt;p&gt;Same as with local shells, this allows for any number of ad-hoc shells on that
remote host, grouped under one host-specific tmux tab, with an important feature
of being persistent across any network hiccups/disconnects or any local
desktop/laptop issues.&lt;/p&gt;
&lt;p&gt;If remote host doesn't have &amp;quot;screen&amp;quot; - install and run it, and if that's not possible
(e.g. network switch device), then still at least run &amp;quot;screen&amp;quot; locally and have
(&lt;a class="reference external" href="https://en.wikibooks.org/wiki/OpenSSH/Cookbook/Multiplexing"&gt;multipexed&lt;/a&gt;) ssh sessions open to there in each screen-window within that tmux tab.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Shell running on most local/remote hosts (zsh in my case typically) has a hook
to detect whether it's running under screen and shows red &amp;quot;tty&amp;quot; warning in prompt
when it's not protected/multiplexed like that, so almost impossible to forget to
use those in &amp;quot;screen&amp;quot;, even without a habit of doing so.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So it's a kind of two-dimensional screen-in-tmux setup, with per-host tmux tabs,
in which any number of &amp;quot;screen&amp;quot; windows are contained for that local user or remote host.&lt;/p&gt;
&lt;p&gt;I tend to say &amp;quot;tmux tabs&amp;quot; above, simply to tell those apart from &amp;quot;screen windows&amp;quot;,
even though same concept is also called &amp;quot;windows&amp;quot; in tmux, but here they are used
kinda like tabs in a GUI terminal.&lt;/p&gt;
&lt;p&gt;Unlike GUI terminal tabs however, &amp;quot;tmux tabs&amp;quot; survive GUI terminal app crashing
or being accidentally closed just fine, or a whole window manager / compositor /
Xorg crashing or hanging due to whatever complicated graphical issues
(which tend to be far more common than base kernel panics and such).&lt;/p&gt;
&lt;p&gt;(worst-case can usually either switch to a linux VT via e.g. ctrl-alt-F2,
or ssh into desktop from a laptop, re-attach to that tmux with all consoles,
finish or make a note of whatever I was doing there, etc)&lt;/p&gt;
&lt;p&gt;Somewhat notably, I've never used &amp;quot;window splitting&amp;quot; (panes/layouts) features of screen/tmux,
kinda same as I tend to use only full-screen or half-screen windows on graphical desktop,
with fixed places at app-specific virtual desktop, and not bother with any other
&amp;quot;window management&amp;quot; stuff.&lt;/p&gt;
&lt;p&gt;Most of the work in this setup is done by GNU Screen, which is actually hosting all
the shells on all hosts and interacts with those directly, with tmux being relegated
to a much simpler &amp;quot;keep tabs for local terminal window&amp;quot; role (and can be killed/replaced
with no disruption).&lt;/p&gt;
&lt;hr class="docutils" /&gt;
&lt;p&gt;I've been thinking to migrate to using one tool instead of two for a while, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p class="first"&gt;Using screen/tmux in different roles like this allows to avoid conflicts between the two.&lt;/p&gt;
&lt;p&gt;I.e. reconnecting to a tmux session on a local machine always restores a full
&amp;quot;top-level terminal&amp;quot; window, as there's never anything else in there.
And it's easier to configure each tool for its own role this way in their
separate screenrc/tmux.conf files.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;&amp;quot;screen&amp;quot; is an older and more common tool, available on any systems/distros (to ssh into).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;&amp;quot;screen&amp;quot; is generally more lightweight than tmux.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;I'm more used to &amp;quot;screen&amp;quot; as my own use of it predates tmux,
but tbf they aren't that different.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So was mostly OK using &amp;quot;screen&amp;quot; for now, despite a couple misgivings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p class="first"&gt;It seem to bundle a bunch more legacy functionality and modes which I don't
want or need - multiuser login/password and access-control stuff for that,
serial terminal protocols (e.g. &lt;tt class="docutils literal"&gt;screen /dev/ttyUSB0&lt;/tt&gt;), utmp user-management.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Installs one of a few remaining suid binaries, with &lt;strong&gt;many&lt;/strong&gt; potential issues this implies.&lt;/p&gt;
&lt;p&gt;See e.g. &lt;a class="reference external" href="https://ruderich.org/simon/notes/su-sudo-from-root-tty-hijacking."&gt;su-sudo-from-root-tty-hijacking&lt;/a&gt;, arguments for using &lt;a class="reference external" href="https://mastodon.social/&amp;#64;pid_eins/112353324518585654"&gt;run0&lt;/a&gt; or &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Secure_Shell"&gt;ssh&lt;/a&gt;
to localhost instead of &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Sudo"&gt;sudo&lt;/a&gt;, or endless special-case hacks implemented in
sudo and linux over decades, for stuff like LD_PRELOAD to not default-leak
accross suid change.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Old code there tends to have more issues that tmux doesn't have (e.g. &lt;a class="reference external" href="https://github.com/systemd/systemd/issues/35104"&gt;this
recent terminal title overflow&lt;/a&gt;), but mostly easy to ignore or work around.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Long-running screen sessions use that suid root binary instead of systemd
mechanisms to persist across e.g. ssh disconnects.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr class="docutils" /&gt;
&lt;p&gt;More recently, with a major GNU Screen 5.0.0 update, a bunch of random stuff broke
for me there, which I've mostly worked around by sticking with last 4.9 release,
but that can't last, and after &lt;a class="reference external" href="https://www.openwall.com/lists/oss-security/2025/05/12/1"&gt;most recent batch of security issues in screen&lt;/a&gt;,
finally decided to fully jump to tmux, to at least deal with only one set of issues there.&lt;/p&gt;
&lt;p&gt;By now, tmux seem to be common enough to easily install on any remote hosts,
but it works slightly differently than &amp;quot;screen&amp;quot;, and has couple other problems
with my workflow above:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&amp;quot;session&amp;quot; concept there has only one &amp;quot;active window&amp;quot;, so having to sometimes
check same &amp;quot;screen&amp;quot; on the same remote from different terminals, it'd force
both &amp;quot;clients&amp;quot; to look at the same window, instead of having more independent
state, like with &amp;quot;screen&amp;quot;.&lt;/li&gt;
&lt;li&gt;Starting tmux-in-tmux needs a separate configuration and resolving a couple
conflicts between the two.&lt;/li&gt;
&lt;li&gt;Some different hotkeys and such minor quirks.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Habits can be changed of course, but since tmux is quite flexible and easily
configurable, they actually don't have to change, and tmux can work pretty much
like &amp;quot;screen&amp;quot; does, with just shell aliases and a config file.&lt;/p&gt;
&lt;p&gt;With &amp;quot;screen&amp;quot;, I've always used following aliases in zshrc:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
alias s='exec screen'
alias sr='screen -r'
alias sx='screen -x'
&lt;/pre&gt;
&lt;p&gt;&amp;quot;s&amp;quot; here replaces shell with &amp;quot;shell in screen&amp;quot;, &amp;quot;sr&amp;quot; reconnects to that &amp;quot;screen&amp;quot;
normally, sometimes temporarily (hence no &amp;quot;exec&amp;quot;), and &amp;quot;sx&amp;quot; is same as &amp;quot;sr&amp;quot; but
to share &amp;quot;screen&amp;quot; that's already connected-to (e.g. when something went wrong,
and old &amp;quot;client&amp;quot; is still technically hanging around, or just from a diff device).&lt;/p&gt;
&lt;p&gt;Plus tend to always replace /etc/screenrc with one that disables welcome-screen,
sets larger scrollback and has couple other tweaks enabled, so it's actually
roughly same amount of tweaks as tmux needs to be like &amp;quot;screen&amp;quot; on a new system.&lt;/p&gt;
&lt;hr class="docutils" /&gt;
&lt;p&gt;Differences between the two that I've found so far, to alias/configure around:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p class="first"&gt;To run tmux within tmux for local &amp;quot;nested&amp;quot; sessions, like &amp;quot;screen in tmux&amp;quot;
case above, with two being entirely independent, following things are needed:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;Clear TMUX= env var, e.g. in that &amp;quot;s&amp;quot; alias.&lt;/li&gt;
&lt;li&gt;Use different configuration files, i.e. with different prefix, status line,
and any potential &amp;quot;screen-like&amp;quot; tweaks.&lt;/li&gt;
&lt;li&gt;Have different session socket name set either via &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;-L&lt;/span&gt; screen&lt;/tt&gt;
or &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;-S&lt;/span&gt;&lt;/tt&gt; option with full path.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These tweaks fit nicely with using just aliases + separate config file,
which are already a given.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;To facilitate shared &amp;quot;windows&amp;quot; between &amp;quot;sessions&amp;quot;, but independent &amp;quot;active window&amp;quot;
in each, tmux has &amp;quot;session groups&amp;quot; feature - running &amp;quot;new-session -t &amp;lt;groupname&amp;gt;&amp;quot;
will share all &amp;quot;windows&amp;quot; between the two, adding/removing them in both, but not
other state like &amp;quot;active windows&amp;quot;.&lt;/p&gt;
&lt;p&gt;Again, shell alias can handle that by passing additional parameter, no problem.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;tmux needs to use different &amp;quot;session group&amp;quot; names to create multiple &amp;quot;sessions&amp;quot;
on the same host with different windows, for e.g. running multiple separate local
&amp;quot;screen&amp;quot; sessions, nested in different tmux &amp;quot;tabs&amp;quot; of a local terminal, and not sharing
&amp;quot;windows&amp;quot; between those (as per setup described at the beginning).&lt;/p&gt;
&lt;p&gt;Not a big deal for a shell alias either - just use new group names with &amp;quot;s&amp;quot; alias.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Reconnecting like &amp;quot;screen -r&amp;quot; with &amp;quot;sr&amp;quot; alias ideally needs to auto-pick &amp;quot;detached&amp;quot;
session or group, but unlike &amp;quot;screen&amp;quot;, tmux doesn't care about whether session is
already attached when using its &amp;quot;attach&amp;quot; command.&lt;/p&gt;
&lt;p&gt;This can be checked, sessions printed/picked in &amp;quot;sr&amp;quot; alias, like it was with &amp;quot;screen -r&amp;quot;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Sharing session via &amp;quot;screen -x&amp;quot; or &amp;quot;sx&amp;quot; alias is a tmux default already.&lt;/p&gt;
&lt;p&gt;But detaching from a &amp;quot;shared screen session&amp;quot; actually maps to a &amp;quot;kill-session&amp;quot;
action in tmux, because it's a &amp;quot;session group&amp;quot; that is shared between two &amp;quot;sessions&amp;quot;
there, and one of those &amp;quot;sessions&amp;quot; should just be closed, group will stay around.&lt;/p&gt;
&lt;p&gt;Given that &amp;quot;shared screen sessions&amp;quot; aren't that common to use for me, and
leaving behind detached tmux &amp;quot;session&amp;quot; isn't a big deal, easiest fix seem to
be adding &amp;quot;C-a shift-d&amp;quot; key for &amp;quot;kill-session&amp;quot; command, next to &amp;quot;C-a d&amp;quot; for
regular &amp;quot;detach-client&amp;quot;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Any extra tmux key bindings spread across keyboard like landmines to fatfinger
at the worst moment possible, and then have no idea how to undo whatever it did!&lt;/p&gt;
&lt;p&gt;Easy to fix in the config - run &lt;tt class="docutils literal"&gt;tmux &lt;span class="pre"&gt;list-keys&lt;/span&gt;&lt;/tt&gt; to dump them all,
pick only ones you care about there for config file, and put e.g.
&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;unbind-key&lt;/span&gt; &lt;span class="pre"&gt;-T&lt;/span&gt; prefix &lt;span class="pre"&gt;-a&lt;/span&gt;&lt;/tt&gt; + &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;unbind-key&lt;/span&gt; &lt;span class="pre"&gt;-T&lt;/span&gt; root &lt;span class="pre"&gt;-a&lt;/span&gt;&lt;/tt&gt; before those bindings
to reliably wipe out the rest.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Status-line needs to be configured in that separate tmux-screen config to be
different from the one in the wrapper tmux, to avoid confusion.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of these actually change the simple &amp;quot;install tmux + config + zshrc aliases&amp;quot;
setup that I've had with &amp;quot;screen&amp;quot;, so it's a pretty straightforward migration.&lt;/p&gt;
&lt;p&gt;zshrc aliases got a bit more complicated than 3 lines above however, but eh, no big deal:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# === tmux session-management aliases&lt;/span&gt;
&lt;span class="c1"&gt;# These are intended to mimic how &amp;quot;screen&amp;quot; and its -r/-x options work&lt;/span&gt;
&lt;span class="c1"&gt;# I.e. sessions are started with groups, and itended to be connected to those&lt;/span&gt;

s_tmux&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;e&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;!&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;e&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;shift&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;TMUX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tmux&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;/etc/tmux.screen.conf&lt;span class="w"&gt; &lt;/span&gt;-L&lt;span class="w"&gt; &lt;/span&gt;screen&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;

s&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;!&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;sg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;?&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;s_tmux&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;new-session&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$#&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-eq&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;tmux: errror - s `&lt;/span&gt;
&lt;span class="s2"&gt;    ` alias/func only accepts one optional sg=N arg&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;ss&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;s_tmux&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;ls&lt;span class="w"&gt; &lt;/span&gt;-F&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;#{session_group}&amp;#39;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;ss&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ss&lt;/span&gt;&lt;span class="p"&gt;:(-4)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-z&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$ss&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$ss&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;!&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;sg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;*&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;ss&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;sg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ss&lt;/span&gt;&lt;span class="p"&gt;:(-1)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-lt&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;9&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;tmux: not opening &amp;gt;9 groups&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;ss&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;sg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ss&lt;/span&gt;&lt;span class="p"&gt;:(-1)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;s_tmux&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;new-session&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$ss&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;

sr&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$#&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-ne&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;!&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;?&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;s_tmux&lt;span class="w"&gt; &lt;/span&gt;new-session&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;sg=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;!&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;sg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;?&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;s_tmux&lt;span class="w"&gt; &lt;/span&gt;new-session&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;sg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;?-*&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;*&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;tmux: error - invalid session-match [ &lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt; ]&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;s_tmux&lt;span class="w"&gt; &lt;/span&gt;-N&lt;span class="w"&gt; &lt;/span&gt;attach&lt;span class="w"&gt; &lt;/span&gt;-Et&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$#&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-eq&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;tmux: errror - sr alias/func`&lt;/span&gt;
&lt;span class="s2"&gt;    ` only accepts one optional session id/name arg&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;n&lt;span class="w"&gt; &lt;/span&gt;line&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;ss&lt;/span&gt;&lt;span class="o"&gt;=()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;sl&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="p"&gt;(@f)&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;s_tmux&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;ls&lt;span class="w"&gt; &lt;/span&gt;-F&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;#{session_id} #S#{?session_attached,, [detached]} :: #{session_windows}&amp;#39;&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39; window(s) :: group #{session_group} :: #{session_group_attached} attached&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;line&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;sl&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;line&lt;/span&gt;&lt;span class="p"&gt;% attached&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="p"&gt;##* &lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;!&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;ss&lt;/span&gt;&lt;span class="o"&gt;+=(&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;line&lt;/span&gt;&lt;span class="p"&gt;%% *&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;ss&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-ne&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;s_tmux&lt;span class="w"&gt; &lt;/span&gt;-N&lt;span class="w"&gt; &lt;/span&gt;attach&lt;span class="w"&gt; &lt;/span&gt;-Et&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$ss&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;sl&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-gt&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;sl&lt;/span&gt;&lt;span class="p"&gt;[1]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;!&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;tmux: no screen-like sessions detected&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;tmux: no unique unused session-group`&lt;/span&gt;
&lt;span class="s2"&gt;    ` (&lt;/span&gt;&lt;span class="si"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;sl&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; total), use N or sg=N group, or session \$M id / sg=N-M name&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;line&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;sl&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;  &lt;/span&gt;&lt;span class="nv"&gt;$line&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;They work pretty much same as &lt;tt class="docutils literal"&gt;screen&lt;/tt&gt; and &lt;tt class="docutils literal"&gt;screen &lt;span class="pre"&gt;-r&lt;/span&gt;&lt;/tt&gt; used to do, even
easier for &amp;quot;sr&amp;quot; with simple group numbers, and &amp;quot;sx&amp;quot; for &lt;tt class="docutils literal"&gt;screen &lt;span class="pre"&gt;-x&lt;/span&gt;&lt;/tt&gt; isn't needed
(&amp;quot;sr&amp;quot; will attach to any explicitly picked group just fine).&lt;/p&gt;
&lt;p&gt;And as for a screen-like tmux config - &lt;tt class="docutils literal"&gt;/etc/tmux.screen.conf&lt;/tt&gt;:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
TMUX_SG=t # env var to inform shell prompt

set -g default-terminal 'screen-256color'
set -g terminal-overrides 'xterm*:smcup&amp;#64;:rmcup&amp;#64;'
set -g xterm-keys on

set -g history-limit 30000
set -g set-titles on
set -g set-titles-string '#T'
set -g automatic-rename off
set -g status-bg 'color22'
set -g status-fg 'color11'
set -g status-right '[###I %H:%M]'
set -g status-left '#{client_user}&amp;#64;#h #{session_group} '
set -g status-left-length 40
set -g window-status-current-style 'bg=color17 bold'
set -g window-status-format '#I#F'
set -g window-status-current-format '#{?client_prefix,#[fg=colour0]#[bg=colour180],}#I#F'
set -g mode-keys emacs

unbind-key -T prefix -a
unbind-key -T root -a
unbind-key -T copy-mode-vi -a # don't use those anyways

set -g prefix C-a
bind-key a send-prefix
bind-key C-a last-window

# ... and all useful prefix bind-key lines from &amp;quot;tmux list-keys&amp;quot; output here.
&lt;/pre&gt;
&lt;p&gt;As I don't use layouts/panes and bunch of other features of screen/tmux multiplexers,
it's only like 20 keys at the end for me, but to be fair, tmux keys are pretty much
same as screen after you change prefix to C-a, so probably don't need to be
unbound/replaced at all for someone who uses more of those features.&lt;/p&gt;
&lt;p&gt;So in the end, it's a good overdue upgrade to a more purpose-built/constrained
and at the same time more feature-rich and more modern tool within its scope,
without loosing ease of setup or needing to change any habits - a great thing,
can recommend to anyone still using &amp;quot;screen&amp;quot; in roughly this role.&lt;/p&gt;
</content><category term="Uncategorized"/><category term="unix"/><category term="tools"/><category term="sysadmin"/><category term="ssh"/></entry><entry><title>nftables rate-limiting against low-effort DDoS attacks</title><link href="http://blog.fraggod.net/2025/01/16/nftables-rate-limiting-against-low-effort-ddos-attacks.html" rel="alternate"/><published>2025-01-16T10:17:00+05:00</published><updated>2025-01-16T10:17:00+05:00</updated><author><name>Mike Kazantsev</name></author><id>tag:blog.fraggod.net,2025-01-16:/2025/01/16/nftables-rate-limiting-against-low-effort-ddos-attacks.html</id><summary type="html">&lt;p&gt;Dunno what random weirdo found me this time around, but have noticed 'net
connection on home-server getting clogged by 100mbps of incoming traffic yesterday,
which seemed to be just junk sent to every open protocol which accepts it from some
5-10K IPs around the globe, with bulk being pipelined requests …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Dunno what random weirdo found me this time around, but have noticed 'net
connection on home-server getting clogged by 100mbps of incoming traffic yesterday,
which seemed to be just junk sent to every open protocol which accepts it from some
5-10K IPs around the globe, with bulk being pipelined requests over open nginx connections.&lt;/p&gt;
&lt;p&gt;Seems very low-effort, and easily worked around by not responding to TCP SYN
packets, as volume of those is relatively negligible (not a syn/icmp flood
or any kind of amplification backscatter), and just &lt;a class="reference external" href="https://wiki.nftables.org/"&gt;nftables&lt;/a&gt; can deal with that,
if configured to block the right IPs.&lt;/p&gt;
&lt;p&gt;Actually, first of all, as nginx was a prime target here, and allows single
connection to dump a lot of request traffic into it (what was happening),
two things can be easily done there:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p class="first"&gt;Tighten keepalive and request limits in general, e.g.:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
limit_req_zone $binary_remote_addr zone=perip:10m rate=20r/m;
limit_req zone=perip burst=30 nodelay;
keepalive_requests 3;
keepalive_time 1m;
keepalive_timeout 75 60;
client_max_body_size 10k;
client_header_buffer_size 1k;
large_client_header_buffers 2 1k;
&lt;/pre&gt;
&lt;p&gt;Idea is to at least force bots to reconnect, which will work nicely with
nftables rate-limiting below too.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;If bots are simple and dumb, sending same 3-4 types of requests, grep those:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
tail -F access.log | stdbuf -oL awk '/.../ {print $1}' |
  while read addr; do nft add element inet filter set.inet-bots4 &amp;quot;{$addr}&amp;quot;; done
&lt;/pre&gt;
&lt;p&gt;Yeah, there's &lt;a class="reference external" href="http://www.fail2ban.org/"&gt;fail2ban&lt;/a&gt; and such for that as well, but why
overcomplicate things when a trivial tail to grep/awk will do.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Tbf that takes care of bulk of the traffic in such simple scenario already,
but nftables can add more generalized &amp;quot;block bots connecting to anything way
more than is sane&amp;quot; limits, like these:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
add set inet filter set.inet-bots4.rate \
  { type ipv4_addr; flags dynamic, timeout; timeout 10m; }
add set inet filter set.inet-bots4 \
  { type ipv4_addr; flags dynamic, timeout; counter; timeout 240m; }

add counter inet filter cnt.inet-bots.pass
add counter inet filter cnt.inet-bots.blackhole

add rule inet filter tc.pre \
  iifname $iface.wan ip daddr $ip.wan tcp flags syn jump tc.pre.ddos

add rule inet filter tc.pre.ddos \
  ip saddr &amp;#64;set.inet-bots4 counter name cnt.inet-bots.blackhole drop
add rule inet filter tc.pre.ddos \
  update &amp;#64;set.inet-bots4.rate { ip saddr limit rate over 3/minute burst 20 packets } \
  add &amp;#64;set.inet-bots4 { ip saddr } drop
add rule inet filter tc.pre.ddos counter name cnt.inet-bots.pass
&lt;/pre&gt;
&lt;p&gt;(this is similar to an example under &lt;a class="reference external" href="https://man.archlinux.org/man/nft.8#SET_STATEMENT"&gt;SET STATEMENT from &amp;quot;man nft&amp;quot;&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Where &lt;tt class="docutils literal"&gt;$iface.wan&lt;/tt&gt; and such vars should be &lt;tt class="docutils literal"&gt;define&lt;/tt&gt;'d separately,
as well as &lt;tt class="docutils literal"&gt;tc.pre&lt;/tt&gt; hooks (somewhere like prerouting -350, before anything else).
ip/ip6 addr selectors can also be used with separate IPv4/IPv6 sets.&lt;/p&gt;
&lt;p&gt;But the important things there IMO are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p class="first"&gt;To define persistent sets, like &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;set.inet-bots4&lt;/span&gt;&lt;/tt&gt; blackhole one,
and not flush/remove those on any configuration fine-tuning afterwards,
only build it up until non-blocked botnet traffic is negligible.&lt;/p&gt;
&lt;p&gt;Rate limits like &lt;tt class="docutils literal"&gt;ip saddr limit rate over 3/minute burst 20 packets&lt;/tt&gt;
are stored in the dynamic set itself, so can be adjusted on the fly anytime,
without needing to replace it.&lt;/p&gt;
&lt;p&gt;Sets are easy to export/import in isolation as well:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
# nft list set inet filter set.inet-bots4 &amp;gt; bots4.nft
# nft -f bots4.nft
&lt;/pre&gt;
&lt;p&gt;Last command adds set elements from bots4.nft, as there's no &amp;quot;flush&amp;quot; in there,
effectively merging old set with the new, does not replace it.
&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;-j/--json&lt;/span&gt;&lt;/tt&gt; input/output can be useful there to filter sets via scripts.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Always use separate chain like &lt;tt class="docutils literal"&gt;tc.pre.ddos&lt;/tt&gt; for complicated rate-limiting
and set-matching rules, so that those can be atomically flushed-replaced via
e.g. a simple .sh script to change or tighten/relax the limits as-needed later:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
nft -f- &amp;lt;&amp;lt;EOF
flush chain inet filter tc.pre.ddos
add rule inet filter tc.pre.ddos \
  ip saddr &amp;#64;set.inet-bots4 counter name cnt.inet-bots.blackhole drop
# ... more rate-limiting rule replacements here
EOF
&lt;/pre&gt;
&lt;p&gt;These atomic updates is one of the greatest things about nftables - no need to
nuke whole ruleset, just edit/replace and apply relevant chain(s) via script.&lt;/p&gt;
&lt;p&gt;It's also not hard to add such chains after the fact, but a bit fiddly -
see e.g. &lt;a class="reference external" href="https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/8/html/securing_networks/getting-started-with-nftables_securing-networks#proc_managing-tables-chains-and-rules-using-nft-commands_assembly_creating-and-managing-nftables-tables-chains-and-rules"&gt;&amp;quot;Managing tables, chains, and rules using nft commands&amp;quot; in RHEL docs&lt;/a&gt;
for how to list all rules with their handles (use &lt;tt class="docutils literal"&gt;nft &lt;span class="pre"&gt;-at&lt;/span&gt; list ...&lt;/tt&gt; with
&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;-t&lt;/span&gt;&lt;/tt&gt; in there to avoid dumping large sets), insert/replace rules, etc.&lt;/p&gt;
&lt;p&gt;But the point is - it's a lot easier when pre-filtered traffic is already
passing through dedicated chain to focus on, and edit it separately from the rest.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Counters are very useful to understand whether any of this helps, for example:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
# nft list counters table inet filter
table inet filter {
  counter cnt.inet-bots.pass {
    packets 671 bytes 39772
  }
  counter cnt.inet-bots.blackhole {
    packets 368198 bytes 21603012
  }
}
&lt;/pre&gt;
&lt;p&gt;So it's easy to see that rules are working, and blocking is applied correctly.&lt;/p&gt;
&lt;p&gt;And even better - &lt;tt class="docutils literal"&gt;nft reset counters ... &amp;amp;&amp;amp; sleep 100 &amp;amp;&amp;amp; nft list counters ...&lt;/tt&gt;
command will effectively give the rate of how many bots get passed or blocked per second.&lt;/p&gt;
&lt;p&gt;nginx also has similar metrics btw, without needing to remember any status-page
URLs or monitoring APIs - &lt;tt class="docutils literal"&gt;tail &lt;span class="pre"&gt;-F&lt;/span&gt; access.log | pv &lt;span class="pre"&gt;-ralb&lt;/span&gt; &amp;gt;/dev/null&lt;/tt&gt;
(&lt;a class="reference external" href="https://www.ivarch.com/programs/pv.shtml"&gt;pv&lt;/a&gt; is a common unix &amp;quot;pipe viewer&amp;quot; tool, and can count line rates too).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Sets can have counters as well, like &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;set.inet-bots4&lt;/span&gt;&lt;/tt&gt;,
defined with &lt;tt class="docutils literal"&gt;counter;&lt;/tt&gt; in the example above.&lt;/p&gt;
&lt;p&gt;&lt;tt class="docutils literal"&gt;nft get element inet filter &lt;span class="pre"&gt;set.inet-bots4&lt;/span&gt; '{ 103.115.243.145 }'&lt;/tt&gt;
will get info on blocked packets/bytes for specific bot, when it was added, etc.&lt;/p&gt;
&lt;p&gt;One missing &amp;quot;counter&amp;quot; on sets is the number of elements in those, which piping
it through &lt;tt class="docutils literal"&gt;wc &lt;span class="pre"&gt;-l&lt;/span&gt;&lt;/tt&gt; won't get, as nft dumps multiple elements on the same line,
but &lt;a class="reference external" href="https://jqlang.github.io/jq/"&gt;jq&lt;/a&gt; or a trivial python script can get from &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;-j/--json&lt;/span&gt;&lt;/tt&gt; output:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
nft -j list set inet filter set.inet-bots4 | python /dev/fd/3 3&amp;lt;&amp;lt;'EOF'
import sys, json
for block in json.loads(sys.stdin.read())['nftables']:
  if not (nft_set := block.get('set')): continue
  print(f'{len(nft_set.get(&amp;quot;elem&amp;quot;, list())):,d}'); break
EOF
&lt;/pre&gt;
&lt;p&gt;(jq syntax is harder to remember when using it rarely than python)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;nftables sets can have tuples of multiple things too, e.g. ip + port, or even
a verdict stored in there, but it hardly matters with such temporary bot blocks.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Feed any number of other easy-to-spot bot-patterns into same &amp;quot;blackhole&amp;quot; nftables sets.&lt;/p&gt;
&lt;p&gt;E.g. that &lt;tt class="docutils literal"&gt;tail &lt;span class="pre"&gt;-F&lt;/span&gt; access.log | awk&lt;/tt&gt; is enough to match obviously-phony
requests to same bogus host/URL, and same for malformed junk in error.log,
auth.log, mail.log, etc - stream all those IPs into &lt;tt class="docutils literal"&gt;nft add element ...&lt;/tt&gt;
too, the more the merrier :)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It used to be more difficult to maintain such limits efficiently in userspace to
sync into iptables, but nftables has this basic stuff built-in and very accessible.&lt;/p&gt;
&lt;p&gt;Though probably won't help against commercial DDoS that's expected to get results
instead of just a minor nuisance, against something more valuable than a static
homepage on a $6/mo internet connection - bots might be a bit more sophisticated there,
and numerous enough to clog the pipe by syn-flood or whatever icmp/udp junk,
without distributed network like CloudFlare filtering it at multiple points.&lt;/p&gt;
&lt;p&gt;This time I've finally decided to bother putting it all in the script too
(as well as this blog post while at it), which can be found in the usual repo
for scraps - &lt;a class="reference external" href="https://github.com/mk-fg/fgtk/blob/master/scraps/nft-ddos"&gt;mk-fg/fgtk/scraps/nft-ddos&lt;/a&gt; (or &lt;a class="reference external" href="https://codeberg.org/mk-fg/fgtk/src/branch/master/scraps/nft-ddos"&gt;on codeberg&lt;/a&gt; and &lt;a class="reference external" href="https://fraggod.net/code/git/fgtk/tree/scraps/nft-ddos"&gt;in local cgit&lt;/a&gt;).&lt;/p&gt;
</content><category term="Uncategorized"/><category term="linux"/><category term="rate-limiting"/></entry><entry><title>Automated git release tags</title><link href="http://blog.fraggod.net/2024/12/13/automated-git-release-tags.html" rel="alternate"/><published>2024-12-13T00:28:00+05:00</published><updated>2024-12-13T00:28:00+05:00</updated><author><name>Mike Kazantsev</name></author><id>tag:blog.fraggod.net,2024-12-13:/2024/12/13/automated-git-release-tags.html</id><summary type="html">&lt;p&gt;For projects tracked in some package repositories, apparently it's worth tagging
releases in git repos (as in &lt;tt class="docutils literal"&gt;git tag 24.12.1 HEAD &amp;amp;&amp;amp; git push &lt;span class="pre"&gt;--tags&lt;/span&gt;&lt;/tt&gt;),
for distro packagers/maintainers to check/link/use/compare new release from git,
which seems easy enough to automate if pkg versions are stored …&lt;/p&gt;</summary><content type="html">&lt;p&gt;For projects tracked in some package repositories, apparently it's worth tagging
releases in git repos (as in &lt;tt class="docutils literal"&gt;git tag 24.12.1 HEAD &amp;amp;&amp;amp; git push &lt;span class="pre"&gt;--tags&lt;/span&gt;&lt;/tt&gt;),
for distro packagers/maintainers to check/link/use/compare new release from git,
which seems easy enough to automate if pkg versions are stored in a repo file already.&lt;/p&gt;
&lt;p&gt;One modern way of doing that in larger projects can be CI/CD pipelines, but they
imply a lot more than just release tagging, so for some tiny python module like
&lt;a class="reference external" href="https://github.com/mk-fg/pretty-yaml"&gt;pyaml&lt;/a&gt;, don't see a reason to bother with them atm, and I know how git hooks work.&lt;/p&gt;
&lt;p&gt;For release to be pushed to a repository like &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Python_Package_Index"&gt;PyPI&lt;/a&gt; in the first place,
project repo almost certainly has a version stored in a file somewhere,
e.g. &lt;a class="reference external" href="https://github.com/mk-fg/pretty-yaml/blob/master/pyproject.toml"&gt;pyproject.toml&lt;/a&gt; for PyPI:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;[project]&lt;/span&gt;
&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;pyaml&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;24.12.1&amp;quot;&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Updates to this version string can be automated on their own (I use simple
&lt;a class="reference external" href="https://github.com/mk-fg/fgtk?tab=readme-ov-file#hdr-git-version-bump-filter"&gt;git-version-bump-filter&lt;/a&gt; script for that in some projects), or done manually
when pushing a new release to package repo, and git tags can easily follow that.&lt;/p&gt;
&lt;p&gt;E.g. when pyproject.toml changes in git commit, and that change includes
version= line - that's a commit that should have that updated version tag on it.&lt;/p&gt;
&lt;p&gt;Best place to add/update that tag in git after commit is &lt;a class="reference external" href="https://man.archlinux.org/man/githooks.5#post-commit"&gt;post-commit hook&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="ch"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-eo&lt;span class="w"&gt; &lt;/span&gt;pipefail

die&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;$&amp;#39;\ngit-post-commit :: ----------------------------------------&amp;#39;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;git-post-commit :: ERROR: &lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;$&amp;#39;git-post-commit :: ----------------------------------------\n&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;ver&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;show&lt;span class="w"&gt; &lt;/span&gt;--no-color&lt;span class="w"&gt; &lt;/span&gt;--diff-filter&lt;span class="o"&gt;=&lt;/span&gt;M&lt;span class="w"&gt; &lt;/span&gt;-aU0&lt;span class="w"&gt; &lt;/span&gt;pyproject.toml&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;gawk&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/^\+version\s*=/ {&lt;/span&gt;
&lt;span class="s1"&gt;      split(substr($NF,2,length($NF)-2),v,&amp;quot;.&amp;quot;)&lt;/span&gt;
&lt;span class="s1"&gt;      print v[1]+0 &amp;quot;.&amp;quot; v[2]+0 &amp;quot;.&amp;quot; v[3]+0}&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;

&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$ver&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;~&lt;span class="w"&gt; &lt;/span&gt;^&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;-9&lt;span class="o"&gt;]&lt;/span&gt;+&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;-9&lt;span class="o"&gt;]&lt;/span&gt;+&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;-9&lt;span class="o"&gt;]&lt;/span&gt;+$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;ver&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;gawk&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/^version\s*=/ {&lt;/span&gt;
&lt;span class="s1"&gt;    split(substr($NF,2,length($NF)-2),v,&amp;quot;.&amp;quot;)&lt;/span&gt;
&lt;span class="s1"&gt;    print v[1]+0 &amp;quot;.&amp;quot; v[2]+0 &amp;quot;.&amp;quot; v[3]+0}&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pyproject.toml&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$ver&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;~&lt;span class="w"&gt; &lt;/span&gt;^&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;-9&lt;span class="o"&gt;]&lt;/span&gt;+&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;-9&lt;span class="o"&gt;]&lt;/span&gt;+&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;-9&lt;span class="o"&gt;]&lt;/span&gt;+$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;die&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Failed to get version from git-show and pyproject.toml file&amp;#39;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;ver_tag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;tag&lt;span class="w"&gt; &lt;/span&gt;--sort&lt;span class="o"&gt;=&lt;/span&gt;v:refname&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tail&lt;span class="w"&gt; &lt;/span&gt;-1&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$ver&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$ver&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$ver_tag&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;die&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;No new release to tag,&amp;#39;&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot; and last git-tag [ &lt;/span&gt;&lt;span class="nv"&gt;$ver_tag&lt;/span&gt;&lt;span class="s2"&gt; ] does not match pyproject.toml version [ &lt;/span&gt;&lt;span class="nv"&gt;$ver&lt;/span&gt;&lt;span class="s2"&gt; ]&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;$&amp;#39;\ngit-post-commit :: no new tag, last one matches pyproject.toml\n&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;

git&lt;span class="w"&gt; &lt;/span&gt;tag&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$ver&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;HEAD&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# can be reassigning tag after --amend&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;\ngit-post-commit :: tagged new release [ &lt;/span&gt;&lt;span class="nv"&gt;$ver&lt;/span&gt;&lt;span class="s2"&gt; ]\n&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;git-show there picks version update line from just-created commit,
which is then checked against existing tag and assigned or updated as necessary.&lt;/p&gt;
&lt;p&gt;&amp;quot;Updated&amp;quot; part tends to be important too, as at least for me it's common to
remember something that needs to be fixed/updated only when writing commit msg
or even after git-push, so &lt;tt class="docutils literal"&gt;git commit &lt;span class="pre"&gt;--amend&lt;/span&gt;&lt;/tt&gt; is common, and should update
that same tag to a new commit hash.&lt;/p&gt;
&lt;p&gt;Messages printed in this hook are nicely prepended to git's usual commit info
output in the terminal, so that you remember when/where this stuff is happening,
and any potential errors are fairly obvious.&lt;/p&gt;
&lt;p&gt;Having tags assigned is not enough to actually have those on github/gitlab/codeberg
and such, as git doesn't push those automatically.&lt;/p&gt;
&lt;p&gt;There's &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;--follow-tags&lt;/span&gt;&lt;/tt&gt; option to push &amp;quot;annotated&amp;quot; tags only, but I don't see
any reason why trivial version tags should have a message attached to them,
so of course there's another way too - &lt;a class="reference external" href="https://man.archlinux.org/man/githooks.5#pre-push"&gt;pre-push hook&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="ch"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-e

&lt;span class="c1"&gt;# Push tags on any pushes to &amp;quot;master&amp;quot; branch, with stdout logging&lt;/span&gt;
&lt;span class="c1"&gt;# Re-assigns tags, but does not delete them, use &amp;quot;git push --delete remote tag&amp;quot; for that&lt;/span&gt;

&lt;span class="nv"&gt;push_remote&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;push_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;

&lt;span class="nv"&gt;master_push&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;master_oid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;rev-parse&lt;span class="w"&gt; &lt;/span&gt;master&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;local_ref&lt;span class="w"&gt; &lt;/span&gt;local_oid&lt;span class="w"&gt; &lt;/span&gt;remote_ref&lt;span class="w"&gt; &lt;/span&gt;remote_oid
&lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$local_oid&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;!&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$master_oid&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;master_push&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;t&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$master_push&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;

&lt;span class="nv"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;git-pre-push [ %s %s ] ::&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$push_remote&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$push_url&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;\n%s --- tags-push ---\n&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$prefix&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
git&lt;span class="w"&gt; &lt;/span&gt;push&lt;span class="w"&gt; &lt;/span&gt;--no-verify&lt;span class="w"&gt; &lt;/span&gt;--tags&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$push_url&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# specific URL in case remote has multiple of those&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;%s --- tags-push success ---\n\n&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$prefix&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It has an extra check for whether it's a push for a master branch, where release
tags presumably are, and auto-runs &lt;tt class="docutils literal"&gt;git push &lt;span class="pre"&gt;--tags&lt;/span&gt; &lt;span class="pre"&gt;-f&lt;/span&gt;&lt;/tt&gt; to the same URL.&lt;/p&gt;
&lt;p&gt;Again &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;-f&lt;/span&gt;&lt;/tt&gt; here is to be able to follow any tag reassignments after --amend's,
although it doesn't delete tags that were removed locally, but don't think that
should happen often enough to bother (if ever).&lt;/p&gt;
&lt;p&gt;pre-push position of the hook should abort the push if there're any issues
pushing tags, and pushing to specific URLs allows to use multiple repo URLs in
e.g. default &amp;quot;origin&amp;quot; remote (used with no-args &lt;tt class="docutils literal"&gt;git push&lt;/tt&gt;), like in github +
codeberg + self-hosted URL-combo that I typically use for redundancy and to
avoid depending on silly policies of &amp;quot;free&amp;quot; third-party services (which is also
why maintaining service-specific CI/CD stuff on those seems like a wasted effort).&lt;/p&gt;
&lt;p&gt;With both hooks in place (under &lt;tt class="docutils literal"&gt;.git/hooks/&lt;/tt&gt;), there should be no manual work
involved in managing/maintaining git tags anymore, to forget that they exist again
for all practical purposes.&lt;/p&gt;
&lt;p&gt;Made both hooks for &lt;a class="reference external" href="https://github.com/mk-fg/pretty-yaml"&gt;pyaml&lt;/a&gt; project repo (apparently packaged in some distro),
where maybe more recent versions of those can be found:&lt;/p&gt;
&lt;blockquote&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;a class="reference external" href="https://github.com/mk-fg/pretty-yaml/blob/master/.githook.post-commit"&gt;https://github.com/mk-fg/pretty-yaml/blob/master/.githook.post-commit&lt;/a&gt;&lt;/div&gt;
&lt;div class="line"&gt;&lt;a class="reference external" href="https://github.com/mk-fg/pretty-yaml/blob/master/.githook.pre-push"&gt;https://github.com/mk-fg/pretty-yaml/blob/master/.githook.pre-push&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;p&gt;Don't think git or sh/bash/gawk used in those ever change to bother updating them,
but maybe there'll be some new corner-case or useful git workflow to handle,
which I haven't bumped into yet.&lt;/p&gt;
</content><category term="Uncategorized"/><category term="scm"/><category term="tools"/></entry><entry><title>Adding color to tcpdump makes a ton of difference</title><link href="http://blog.fraggod.net/2024/09/30/adding-color-to-tcpdump-makes-a-ton-of-difference.html" rel="alternate"/><published>2024-09-30T04:09:00+05:00</published><updated>2024-09-30T04:09:00+05:00</updated><author><name>Mike Kazantsev</name></author><id>tag:blog.fraggod.net,2024-09-30:/2024/09/30/adding-color-to-tcpdump-makes-a-ton-of-difference.html</id><summary type="html">&lt;p&gt;Debugging the usual censorshit issues, finally got sick of looking at normal
tcpdump output, and decided to pipe it through a simple translator/colorizer script.&lt;/p&gt;
&lt;p&gt;I think it's one of these cases where a picture is worth a thousand words:&lt;/p&gt;
&lt;a href="http://blog.fraggod.net/images/tcpdump-console-normal.png"&gt; &lt;img   style="width: 771px;"   src="http://blog.fraggod.net/images/tcpdump-console-normal.png"   title="tcpdump console with IPv6 traffic"   alt="tcpdump console with IPv6 traffic"&gt; &lt;/a&gt;&lt;p&gt;This is very hard to read, especially when it's scrolling …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Debugging the usual censorshit issues, finally got sick of looking at normal
tcpdump output, and decided to pipe it through a simple translator/colorizer script.&lt;/p&gt;
&lt;p&gt;I think it's one of these cases where a picture is worth a thousand words:&lt;/p&gt;
&lt;a href="http://blog.fraggod.net/images/tcpdump-console-normal.png"&gt; &lt;img   style="width: 771px;"   src="http://blog.fraggod.net/images/tcpdump-console-normal.png"   title="tcpdump console with IPv6 traffic"   alt="tcpdump console with IPv6 traffic"&gt; &lt;/a&gt;&lt;p&gt;This is very hard to read, especially when it's scrolling,
with long generated IPv6'es in there.&lt;/p&gt;
&lt;p&gt;While this IMO is quite readable:&lt;/p&gt;
&lt;a href="http://blog.fraggod.net/images/tcpdump-console-color.png"&gt; &lt;img   style="width: 460px;"   src="http://blog.fraggod.net/images/tcpdump-console-color.png"   title="tcpdump console with colorized traffic"   alt="tcpdump console with colorized traffic"&gt; &lt;/a&gt;&lt;p&gt;Immediately obvious who's talking to whom and when, where it's especially
trivial to focus on packets from specific hosts by their name shape/color.&lt;/p&gt;
&lt;p&gt;Difference between the two is this trivial config file:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
2a01:4f8:c17:37c1: local.net: !gray
2a01:4f8:c17:37c1:8341:8768:e26:83ff [Container] !bo-ye

2a02:17d0:201:8b0 remote.net !gr
2a02:17d0:201:8b01::1 [Remote-A] !br-gn

2a02:17d0:201:8b00:2a10:6e67:1a0:60ae [Peer] !bold-cyan
2a02:17d0:201:8b00:f60:f2c3:5c:7702 [Desktop] !blue
2a02:17d0:201:8b00:de9a:11c8:e285:235e [Laptop] !wh
&lt;/pre&gt;
&lt;p&gt;...which sets host/network/prefix labels to replace unreadable address parts
with (hosts in brackets as a convention) and colors/highlighting for those
(using either full or two-letter &lt;a class="reference external" href="https://en.wikipedia.org/wiki/DIN_47100"&gt;DIN 47100&lt;/a&gt;-like names for brevity).&lt;/p&gt;
&lt;p&gt;Plus the script to pipe that boring tcpdump output through - &lt;a class="reference external" href="https://github.com/mk-fg/fgtk?tab=readme-ov-file#hdr-tcpdump-translate"&gt;tcpdump-translate&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Another useful feature of such script turns out to be filtering -
tcpdump command-line quickly gets unwieldy with &amp;quot;host ... &amp;amp;&amp;amp; ...&amp;quot; specs,
while in the config above it's trivial to comment/uncomment lines and filter
by whatever network prefixes, instead of cramming it all into shell prompt.&lt;/p&gt;
&lt;p&gt;tcpdump has some of this functionality via DNS reverse-lookups too,
but I really don't want it resolving any addrs that I don't care to track specifically,
which often makes output even more confusing, with long and misleading internal names
assigned by someone else for their own purposes popping up in wrong places, while still
remaining indistinct and lacking colors.&lt;/p&gt;
</content><category term="Uncategorized"/><category term="tools"/><category term="sysadmin"/></entry><entry><title>List connected processes for unix sockets on linux</title><link href="http://blog.fraggod.net/2024/08/06/list-connected-processes-for-unix-sockets-on-linux.html" rel="alternate"/><published>2024-08-06T13:56:00+05:00</published><updated>2024-08-06T13:56:00+05:00</updated><author><name>Mike Kazantsev</name></author><id>tag:blog.fraggod.net,2024-08-06:/2024/08/06/list-connected-processes-for-unix-sockets-on-linux.html</id><summary type="html">&lt;p&gt;For TCP connections, it seems pretty trivial - old &lt;a class="reference external" href="https://man.archlinux.org/man/netstat.8"&gt;netstat&lt;/a&gt; (from &lt;a class="reference external" href="http://net-tools.sf.net/"&gt;net-tools project&lt;/a&gt;)
and modern &lt;a class="reference external" href="https://man.archlinux.org/man/ss.8"&gt;ss&lt;/a&gt; (&lt;a class="reference external" href="https://wiki.linuxfoundation.org/networking/iproute2"&gt;iproute2&lt;/a&gt;) tools do it fine, where you can easily grep both listening
or connected end by IP:port they're using.&lt;/p&gt;
&lt;p&gt;But &lt;tt class="docutils literal"&gt;ss &lt;span class="pre"&gt;-xp&lt;/span&gt;&lt;/tt&gt; for unix sockets (AF_UNIX, aka &amp;quot;named pipes&amp;quot;) doesn't work like
that - only …&lt;/p&gt;</summary><content type="html">&lt;p&gt;For TCP connections, it seems pretty trivial - old &lt;a class="reference external" href="https://man.archlinux.org/man/netstat.8"&gt;netstat&lt;/a&gt; (from &lt;a class="reference external" href="http://net-tools.sf.net/"&gt;net-tools project&lt;/a&gt;)
and modern &lt;a class="reference external" href="https://man.archlinux.org/man/ss.8"&gt;ss&lt;/a&gt; (&lt;a class="reference external" href="https://wiki.linuxfoundation.org/networking/iproute2"&gt;iproute2&lt;/a&gt;) tools do it fine, where you can easily grep both listening
or connected end by IP:port they're using.&lt;/p&gt;
&lt;p&gt;But &lt;tt class="docutils literal"&gt;ss &lt;span class="pre"&gt;-xp&lt;/span&gt;&lt;/tt&gt; for unix sockets (AF_UNIX, aka &amp;quot;named pipes&amp;quot;) doesn't work like
that - only prints socket path for listening end of the connection, which makes
lookups by socket path not helpful, at least with the current iproute-6.10.&lt;/p&gt;
&lt;p&gt;&amp;quot;at least with the current iproute&amp;quot; because manpage actually suggests this:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
ss -x src /tmp/.X11-unix/*
  Find all local processes connected to X server.
&lt;/pre&gt;
&lt;p&gt;Where socket is wrong for modern X - easy to fix - and &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;-p&lt;/span&gt;&lt;/tt&gt; option seem to be
omitted (to show actual processes), but the result is also not at all &amp;quot;local
processes connected to X server&amp;quot; anyway:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
# ss -xp src &amp;#64;/tmp/.X11-unix/X1

Netid State  Recv-Q Send-Q      Local Address:Port   Peer Address:Port  Process
u_str ESTAB  0      0      &amp;#64;/tmp/.X11-unix/X1 26800             * 25948  users:((&amp;quot;Xorg&amp;quot;,pid=1519,fd=51))
u_str ESTAB  0      0      &amp;#64;/tmp/.X11-unix/X1 331064            * 332076 users:((&amp;quot;Xorg&amp;quot;,pid=1519,fd=40))
u_str ESTAB  0      0      &amp;#64;/tmp/.X11-unix/X1 155940            * 149392 users:((&amp;quot;Xorg&amp;quot;,pid=1519,fd=46))
...
u_str ESTAB  0      0      &amp;#64;/tmp/.X11-unix/X1 16326             * 20803  users:((&amp;quot;Xorg&amp;quot;,pid=1519,fd=44))
u_str ESTAB  0      0      &amp;#64;/tmp/.X11-unix/X1 11106             * 27720  users:((&amp;quot;Xorg&amp;quot;,pid=1519,fd=50))
u_str LISTEN 0      4096   &amp;#64;/tmp/.X11-unix/X1 12782             * 0      users:((&amp;quot;Xorg&amp;quot;,pid=1519,fd=7))
&lt;/pre&gt;
&lt;p&gt;It's just a long table listing same &amp;quot;Xorg&amp;quot; process on every line,
which obviously isn't what example claims to fetch, or useful in any way.
So maybe it worked fine earlier, but some changes to the tool or whatever
data it grabs made this example obsolete and not work anymore.&lt;/p&gt;
&lt;p&gt;But there are &amp;quot;ports&amp;quot; listed for unix sockets, which I think correspond to
&amp;quot;inodes&amp;quot; in /proc/net/unix, and are global across host (or at least same netns),
so two sides of connection - that socket-path + Xorg process info - and other
end with connected process info - can be joined together by those port/inode numbers.&lt;/p&gt;
&lt;p&gt;I haven't been able to find a tool to do that for me easily atm, so went ahead to
write my own script, mostly focused on listing per-socket pids on either end, e.g.:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
# unix-socket-links
...
/run/dbus/system_bus_socket :: dbus-broker[1190] :: Xorg[1519] bluetoothd[1193]
  claws-mail[2203] dbus-broker-lau[1183] efreetd[1542] emacs[2160] enlightenment[1520]
  pulseaudio[1523] systemd-logind[1201] systemd-network[1363] systemd-timesyn[966]
  systemd[1366] systemd[1405] systemd[1] waterfox[2173]
...
/run/user/1000/bus :: dbus-broker[1526] :: dbus-broker-lau[1518] emacs[2160] enlightenment[1520]
  notification-th[1530] pulseaudio[1523] python3[1531] python3[5397] systemd[1405] waterfox[2173]

/run/user/1000/pulse/native :: pulseaudio[1523] :: claws-mail[2203] emacs[2160]
  enlightenment[1520] mpv[9115] notification-th[1530] python3[2063] waterfox[2173]

&amp;#64;/tmp/.X11-unix/X1 :: Xorg[1519] :: claws-mail[2203] conky[1666] conky[1671] emacs[2160]
  enlightenment[1520] notification-th[1530] python3[5397] redshift[1669] waterfox[2173]
  xdpms[7800] xterm[1843] xterm[2049] yeahconsole[2047]
...
&lt;/pre&gt;
&lt;p&gt;Output format is &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;&amp;lt;socket-path&amp;gt;&lt;/span&gt; :: &lt;span class="pre"&gt;&amp;lt;listening-pid&amp;gt;&lt;/span&gt; :: &lt;span class="pre"&gt;&amp;lt;clients...&amp;gt;&lt;/span&gt;&lt;/tt&gt;, where it's
trivial to see exactly what is connected to which socket (and what's listening there).&lt;/p&gt;
&lt;p&gt;&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;unix-socket-links&lt;/span&gt; &lt;span class="pre"&gt;&amp;#64;/tmp/.X11-unix/X1&lt;/span&gt;&lt;/tt&gt; can list only conns/pids for that
socket, and adding &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;-c/--conns&lt;/span&gt;&lt;/tt&gt; can be used to disaggregate that list of
processes back into specific connections (which can be shared between pids too),
to get more like a regular netstat/ss output, but with procs on both ends,
not weirdly broken one like &lt;tt class="docutils literal"&gt;ss &lt;span class="pre"&gt;-xp&lt;/span&gt;&lt;/tt&gt; gives you.&lt;/p&gt;
&lt;p&gt;Script is in the usual &lt;a class="reference external" href="https://github.com/mk-fg/fgtk"&gt;mk-fg/fgtk&lt;/a&gt; repo (also &lt;a class="reference external" href="https://codeberg.org/mk-fg/fgtk#hdr-unix-socket-links"&gt;on codeberg&lt;/a&gt; and &lt;a class="reference external" href="https://fraggod.net/code/git/fgtk/about/#hdr-unix-socket-links"&gt;local git&lt;/a&gt;),
with code link and a small doc here:&lt;/p&gt;
&lt;blockquote&gt;
&lt;a class="reference external" href="https://github.com/mk-fg/fgtk?tab=readme-ov-file#hdr-unix-socket-links"&gt;https://github.com/mk-fg/fgtk?tab=readme-ov-file#hdr-unix-socket-links&lt;/a&gt;&lt;/blockquote&gt;
&lt;p&gt;Was half-suspecting that I might need to parse /proc/net/unix or load eBPF
for this, but nope, ss has all the info needed, just presents it in a silly way.&lt;/p&gt;
&lt;p&gt;Also, unlike some other iproute2 tools where that was added (or lsfd below), it
doesn't have &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;--json&lt;/span&gt;&lt;/tt&gt; output flag, but should be stable enough to parse
anyway, I think, and easy enough to sanity-check by the header.&lt;/p&gt;
&lt;hr class="docutils" /&gt;
&lt;p&gt;Oh, and also, one might be tempted to use &lt;a class="reference external" href="https://man.archlinux.org/man/lsof.8"&gt;lsof&lt;/a&gt; or &lt;a class="reference external" href="https://man.archlinux.org/man/lsfd.1"&gt;lsfd&lt;/a&gt; for this, like I did,
but it's more complicated and can be janky to get the right output out of these,
and pretty sure lsof even has side-effects, where it connects to socket with &lt;tt class="docutils literal"&gt;+E&lt;/tt&gt;
(good luck figuring out what's that supposed to do btw), causing all sorts of
unintended mayhem, but here are snippets that I've used for those in some past
(checking where stalled ssh-agent socket connections are from in this example):&lt;/p&gt;
&lt;pre class="literal-block"&gt;
lsof -wt +E &amp;quot;$SSH_AUTH_SOCK&amp;quot; | awk '{print &amp;quot;\\&amp;lt;&amp;quot; $1 &amp;quot;\\&amp;gt;&amp;quot;}' | g -3f- &amp;lt;(ps axlf)
lsfd -no PID -Q &amp;quot;UNIX.PATH == '$SSH_AUTH_SOCK'&amp;quot; | grep -f- &amp;lt;(ps axlf)
&lt;/pre&gt;
&lt;p&gt;Don't think either of those work anymore, maybe for same reason as with &lt;tt class="docutils literal"&gt;ss&lt;/tt&gt;
not listing unix socket path for egress unix connections, and lsof in particular
straight-up hangs without even &lt;tt class="docutils literal"&gt;kill &lt;span class="pre"&gt;-9&lt;/span&gt;&lt;/tt&gt; getting it, if socket on the other
end doesn't process its (silly and pointless) connection, so maybe don't use
that one at least - lsfd seem to be easier to use in general.&lt;/p&gt;
</content><category term="Uncategorized"/><category term="tools"/><category term="linux"/><category term="sysadmin"/></entry></feed>