<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Blog | zealcharm</title><link href="https://www.zealcharm.com/blog/" rel="alternate"/><link href="https://www.zealcharm.com/blog/feeds/all.atom.xml" rel="self"/><id>https://www.zealcharm.com/blog/</id><updated>2025-03-30T00:00:00+00:00</updated><entry><title>A method for maintaining locally patched Arch Linux packages</title><link href="https://www.zealcharm.com/blog/a-method-for-maintaining-locally-patched-arch-linux-packages.html" rel="alternate"/><published>2025-03-30T00:00:00+00:00</published><updated>2025-03-30T00:00:00+00:00</updated><author><name>zealcharm</name></author><id>tag:www.zealcharm.com,2025-03-30:/blog/a-method-for-maintaining-locally-patched-arch-linux-packages.html</id><summary type="html">&lt;p&gt;Due to my &lt;s&gt;stubbornness&lt;/s&gt; fondness for testing the latest and greatest software releases, I often run into problems, and then I need to apply patches on top of Arch Linux's packages to fix them.&lt;/p&gt;
&lt;p&gt;When the number of patched packages grows large enough, this may devolve into chaos, so this …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Due to my &lt;s&gt;stubbornness&lt;/s&gt; fondness for testing the latest and greatest software releases, I often run into problems, and then I need to apply patches on top of Arch Linux's packages to fix them.&lt;/p&gt;
&lt;p&gt;When the number of patched packages grows large enough, this may devolve into chaos, so this is a rough sketch of the method I have used over the years to keep this under control.&lt;/p&gt;
&lt;h2&gt;Identifying patched packages&lt;/h2&gt;
&lt;p&gt;The first challenge is marking the packages that I have patched, so both pacman and I can understand what is installed on my system.&lt;/p&gt;
&lt;p&gt;I do this by simply appending a snippet like this at the end of each of my patched PKGBUILDs:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;custompkgrel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="nv"&gt;pkgrel&lt;/span&gt;&lt;span class="o"&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;custompkgrel&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;00&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pkgrel&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This will turn a package version like &lt;code&gt;helloworld-1.23-4&lt;/code&gt; into &lt;code&gt;helloworld-1.23-1004&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This both makes it identifiable (usually, packages have a small &lt;code&gt;pkgrel&lt;/code&gt;), and makes pacman detect the patched packages as updates of the unpatched ones.&lt;/p&gt;
&lt;p&gt;When I patch the package further, I bump &lt;code&gt;custompkgrel&lt;/code&gt;, so that each patched package has a unique version.&lt;/p&gt;
&lt;h2&gt;Applying .patch files&lt;/h2&gt;
&lt;p&gt;The straightforward way to apply .patch files is to put them inline with the rest of the PKGBUILD, like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="w"&gt; &lt;/span&gt;# Maintainer: John Smith &amp;lt;john.smith@example.com&amp;gt;
&lt;span class="w"&gt; &lt;/span&gt;pkgname=helloworld
&lt;span class="w"&gt; &lt;/span&gt;pkgver=1.23
&lt;span class="w"&gt; &lt;/span&gt;pkgrel=4
&lt;span class="w"&gt; &lt;/span&gt;pkgdesc=&amp;quot;A simple Hello World program&amp;quot;
&lt;span class="w"&gt; &lt;/span&gt;url=&amp;#39;https://www.example.com&amp;#39;
&lt;span class="w"&gt; &lt;/span&gt;arch=(&amp;#39;any&amp;#39;)
&lt;span class="w"&gt; &lt;/span&gt;license=(&amp;#39;MIT&amp;#39;)
&lt;span class="w"&gt; &lt;/span&gt;source=(&amp;quot;hello.tar.gz&amp;quot;
&lt;span class="gi"&gt;+        &amp;quot;helloworld-i18n-support.patch&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;        &amp;quot;helloworld-fix-typo.patch&amp;quot;)
&lt;span class="w"&gt; &lt;/span&gt;sha256sums=(&amp;#39;d267ac47beecb944222bc2e8f7d6dd5e3ec9602cf4a4f3ed70b1e424c8213400&amp;#39;
&lt;span class="gi"&gt;+            &amp;#39;87d2f5b9365fbd0b2342f6632fc202f56d7237097596aa30e3aaf92aa2d19b86&amp;#39;&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;            &amp;#39;5635261bac29f1c0ed8cfbb4044e56bda9e85e9f50898fc0e6d1ca00f205f891&amp;#39;)

&lt;span class="w"&gt; &lt;/span&gt;prepare() {
&lt;span class="gi"&gt;+  patch -Np1 -i helloworld-i18n-support.patch&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;  patch -Np1 -i helloworld-fix-typo.patch
&lt;span class="w"&gt; &lt;/span&gt;}

&lt;span class="w"&gt; &lt;/span&gt;build() {
&lt;span class="w"&gt; &lt;/span&gt;  gcc -o hello hello.c
&lt;span class="w"&gt; &lt;/span&gt;}

&lt;span class="w"&gt; &lt;/span&gt;package() {
&lt;span class="w"&gt; &lt;/span&gt;  install -Dm755 &amp;quot;$srcdir/hello&amp;quot; &amp;quot;$pkgdir/usr/bin/hello&amp;quot;
&lt;span class="w"&gt; &lt;/span&gt;}
&lt;span class="gi"&gt;+&lt;/span&gt;
&lt;span class="gi"&gt;+custompkgrel=1&lt;/span&gt;
&lt;span class="gi"&gt;+pkgrel=&amp;quot;${custompkgrel}00${pkgrel}&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;However, my experience is that it often takes a long time for a patch to be polished, submitted, accepted and released upstream; and during that time, keeping the package synchronized with the Arch Linux repositories or the AUR (via &lt;code&gt;git pull&lt;/code&gt;) turns into a chore of resolving frequent spurious merge conflicts.&lt;/p&gt;
&lt;p&gt;Instead, I have settled on a hack to keep all my patches at the end of the PKGBUILD. Behold:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# [...the original, unchanged PKGBUILD...]&lt;/span&gt;

&lt;span class="nv"&gt;custompkgrel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="nv"&gt;pkgrel&lt;/span&gt;&lt;span class="o"&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;custompkgrel&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;00&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pkgrel&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# This monkey-patches &amp;quot;prepare&amp;quot; to append a line of code at the beginning&lt;/span&gt;
&lt;span class="c1"&gt;# Note that this can be called multiple times, each appending a further line of code&lt;/span&gt;
before_prepare&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;if&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="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;before_prepare_script&lt;/span&gt;&lt;span class="p"&gt;+x&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="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;before_prepare_script&lt;/span&gt;&lt;span class="o"&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="nb"&gt;eval&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;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;prepare() { eval &amp;quot;$before_prepare_script&amp;quot;;&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;declare&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;prepare&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;head&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;-1&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;+3&lt;span class="p"&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="s1"&gt;&amp;#39;}&amp;#39;&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="k"&gt;fi&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;before_prepare_script&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$before_prepare_script&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="s2"&gt;&amp;quot;%q &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;$@&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="s1"&gt;$&amp;#39;\n&amp;#39;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;source&lt;/span&gt;&lt;span class="o"&gt;+=(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;helloworld-i18n-support.patch&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;sha256sums&lt;/span&gt;&lt;span class="o"&gt;+=(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;87d2f5b9365fbd0b2342f6632fc202f56d7237097596aa30e3aaf92aa2d19b86&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
before_prepare&lt;span class="w"&gt; &lt;/span&gt;patch&lt;span class="w"&gt; &lt;/span&gt;-Np1&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;helloworld-i18n-support.patch
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With this method, all my changes are an appendage to the PKGBUILD, so they don't conflict. You might object that this is contrary to version control best practices, where a merge conflict is a strong hint that you must look at whatever has changed and confirm that it is correct.&lt;/p&gt;
&lt;p&gt;However, my experience is that &lt;em&gt;for this very particular case of a personal patch repository&lt;/em&gt;, this greatly improves the UX, while not sacrificing much: The &lt;code&gt;patch&lt;/code&gt; command will fail when the code targeted by the .patch file changes, so there is still a conflict detection method in place.&lt;/p&gt;
&lt;h2&gt;Putting it all together&lt;/h2&gt;
&lt;p&gt;Usually, one of my patched PKGBUILD files looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# [...the original, unchanged PKGBUILD...]&lt;/span&gt;

&lt;span class="nv"&gt;custompkgrel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="nb"&gt;source&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;../custompkg.sh&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;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="c1"&gt;# ZEALCHARM START Add i18n support&lt;/span&gt;
&lt;span class="c1"&gt;# See https://example.com/helloworld-org/helloworld/pull/1234&lt;/span&gt;
&lt;span class="nv"&gt;source&lt;/span&gt;&lt;span class="o"&gt;+=(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;helloworld-i18n-support.patch&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;sha256sums&lt;/span&gt;&lt;span class="o"&gt;+=(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;87d2f5b9365fbd0b2342f6632fc202f56d7237097596aa30e3aaf92aa2d19b86&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
before_prepare&lt;span class="w"&gt; &lt;/span&gt;patch&lt;span class="w"&gt; &lt;/span&gt;-Np1&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;helloworld-i18n-support.patch
&lt;span class="c1"&gt;# ZEALCHARM END Add i18n support&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Here, I have refactored the previous utility into a &lt;code&gt;custompkg.sh&lt;/code&gt; script, so I can share the common parts across all my patched packages.&lt;/p&gt;
&lt;h2&gt;The overall flow&lt;/h2&gt;
&lt;p&gt;When I decide to patch a PKGBUILD, what I do is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Clone the PKGBUILD git repository, e.g. &lt;code&gt;git clone https://gitlab.archlinux.org/archlinux/packaging/packages/helloworld.git&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Apply and commit my changes to the PKGBUILD, as explained above.&lt;/li&gt;
&lt;li&gt;Build the package and add it to a &lt;a href="https://wiki.archlinux.org/title/Pacman/Tips_and_tricks#Custom_local_repository"&gt;custom local repository&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;pacman -Syu&lt;/code&gt;. Pacman will detect the package as an update, since the &lt;code&gt;pkgrel&lt;/code&gt; has changed (e.g. &lt;code&gt;1.23-4 -&amp;gt; 1.23-1004&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To keep the package up to date, the process is similar:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pull the PKGBUILD git repository, i.e. &lt;code&gt;cd helloworld &amp;amp;&amp;amp; git pull&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Rebuild the package and add it to the custom local repository.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;pacman -Syu&lt;/code&gt;. Pacman will detect the package as an update, since the &lt;code&gt;pkgver&lt;/code&gt; or &lt;code&gt;pkgrel&lt;/code&gt; will have changed (e.g. &lt;code&gt;1.23-1004 -&amp;gt; 1.23-1005&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I have some scripts that automate most of this process, so the overall amount of work to keep the patched packages up to date is pretty small.&lt;/p&gt;</content><category term="misc"/></entry><entry><title>A pitfall of Python's subprocess Ctrl+C handling (compared to bash)</title><link href="https://www.zealcharm.com/blog/a-pitfall-of-pythons-subprocess-ctrlc-handling-compared-to-bash.html" rel="alternate"/><published>2024-08-10T00:00:00+00:00</published><updated>2024-08-10T00:00:00+00:00</updated><author><name>zealcharm</name></author><id>tag:www.zealcharm.com,2024-08-10:/blog/a-pitfall-of-pythons-subprocess-ctrlc-handling-compared-to-bash.html</id><summary type="html">&lt;p&gt;Usually, I start writing my scripts in bash, and then translate them to Python if they get too complex. When doing so, I assume that invoking a simple command from a bash script is equivalent to a &lt;a href="https://docs.python.org/3/library/subprocess.html#subprocess.run"&gt;&lt;code&gt;subprocess.run&lt;/code&gt;&lt;/a&gt; call. However, this is not true when it comes to Ctrl …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Usually, I start writing my scripts in bash, and then translate them to Python if they get too complex. When doing so, I assume that invoking a simple command from a bash script is equivalent to a &lt;a href="https://docs.python.org/3/library/subprocess.html#subprocess.run"&gt;&lt;code&gt;subprocess.run&lt;/code&gt;&lt;/a&gt; call. However, this is not true when it comes to Ctrl+C (aka. &lt;a href="https://en.wikipedia.org/wiki/Signal_(IPC)#SIGINT"&gt;&lt;code&gt;SIGINT&lt;/code&gt;&lt;/a&gt;) handling.&lt;/p&gt;
&lt;h2&gt;A reliable bash script&lt;/h2&gt;
&lt;p&gt;This bash script shows a common pattern where a script will first set up some state, does something over it, then cleans the state using a shell &lt;a href="https://manpages.ubuntu.com/manpages/trusty/man1/trap.1posix.html"&gt;&lt;code&gt;trap&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-eu

&lt;span class="c1"&gt;# Set up some temporary stuff and ensure that it is cleaned up on exit. The `sleep`s simulate non-trivial clean-up.&lt;/span&gt;
&lt;span class="nb"&gt;trap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;rm -f a &amp;amp;&amp;amp; sleep 0.2 &amp;amp;&amp;amp; rm -f b &amp;amp;&amp;amp; sleep 0.2 &amp;amp;&amp;amp; rm -f c&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;EXIT
touch&lt;span class="w"&gt; &lt;/span&gt;a&lt;span class="w"&gt; &lt;/span&gt;b&lt;span class="w"&gt; &lt;/span&gt;c

&lt;span class="c1"&gt;# Simulate a long running process&lt;/span&gt;
tail&lt;span class="w"&gt; &lt;/span&gt;-q&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;a&lt;span class="w"&gt; &lt;/span&gt;b&lt;span class="w"&gt; &lt;/span&gt;c
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For example, we may want to perform a backup using &lt;code&gt;rsync&lt;/code&gt;. Before doing it, we unlock and mount the target volume using &lt;code&gt;cryptsetup open&lt;/code&gt; and &lt;code&gt;mount&lt;/code&gt;. Whether things go right or wrong, we clean up the setup steps with &lt;code&gt;umount&lt;/code&gt; and &lt;code&gt;cryptsetup close&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Granted, this pattern is not 100% bulletproof. If our script gets killed [or the power goes off, or the system crashes...] the cleanup step will never run, which may leave some junk behind. But since this is very uncommon, it's often acceptable to deal with it manually.&lt;/p&gt;
&lt;p&gt;But something that may happen more often is that you forgot something, or you ran out of time, and you want to stop the process. In this case, you press Ctrl+C, which sends a &lt;code&gt;SIGINT&lt;/code&gt; to the shell, which triggers the &lt;code&gt;trap&lt;/code&gt; and cleans everything up properly:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;./shell_script_with_traps
&lt;span class="go"&gt;^C&lt;/span&gt;
&lt;span class="gp"&gt;$ &lt;/span&gt;ls&lt;span class="w"&gt; &lt;/span&gt;a&lt;span class="w"&gt; &lt;/span&gt;b&lt;span class="w"&gt; &lt;/span&gt;c
&lt;span class="go"&gt;ls: cannot access &amp;#39;a&amp;#39;: No such file or directory&lt;/span&gt;
&lt;span class="go"&gt;ls: cannot access &amp;#39;b&amp;#39;: No such file or directory&lt;/span&gt;
&lt;span class="go"&gt;ls: cannot access &amp;#39;c&amp;#39;: No such file or directory&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;...maybe it wasn't so reliable after all&lt;/h2&gt;
&lt;p&gt;Thus, our script becomes part of our collection of reliable, battle-hardened scripts. We move to building greater things, which involve invoking that script using Python's &lt;a href="https://docs.python.org/3/library/subprocess.html#subprocess.run"&gt;&lt;code&gt;subprocess.run&lt;/code&gt;&lt;/a&gt;. Can you guess what happens when we press Ctrl+C this time?&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;import subprocess; subprocess.run([&amp;#39;./shell_script_with_traps&amp;#39;])&amp;quot;&lt;/span&gt;
&lt;span class="go"&gt;^C&lt;/span&gt;
&lt;span class="go"&gt;[...the traceback for the KeyboardInterrupt exception...]&lt;/span&gt;
&lt;span class="gp"&gt;$ &lt;/span&gt;ls&lt;span class="w"&gt; &lt;/span&gt;a&lt;span class="w"&gt; &lt;/span&gt;b&lt;span class="w"&gt; &lt;/span&gt;c
&lt;span class="go"&gt;ls: cannot access &amp;#39;a&amp;#39;: No such file or directory&lt;/span&gt;
&lt;span class="go"&gt;ls: cannot access &amp;#39;b&amp;#39;: No such file or directory&lt;/span&gt;
&lt;span class="go"&gt;c&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Only half of our cleanup &lt;code&gt;trap&lt;/code&gt; seems to have run. Let's try using &lt;a href="https://docs.python.org/3/library/asyncio-subprocess.html#asyncio.create_subprocess_exec"&gt;&lt;code&gt;asyncio.create_subprocess_exec&lt;/code&gt;&lt;/a&gt; instead:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;import asyncio&lt;/span&gt;
&lt;span class="go"&gt;async def run():&lt;/span&gt;
&lt;span class="go"&gt;    proc = await asyncio.create_subprocess_exec(&amp;#39;./shell_script_with_traps&amp;#39;)&lt;/span&gt;
&lt;span class="go"&gt;    await proc.communicate()&lt;/span&gt;
&lt;span class="go"&gt;asyncio.run(run())&amp;quot;&lt;/span&gt;
&lt;span class="go"&gt;^C&lt;/span&gt;
&lt;span class="go"&gt;[...the traceback for the KeyboardInterrupt exception...]&lt;/span&gt;
&lt;span class="gp"&gt;$ &lt;/span&gt;ls&lt;span class="w"&gt; &lt;/span&gt;a&lt;span class="w"&gt; &lt;/span&gt;b&lt;span class="w"&gt; &lt;/span&gt;c
&lt;span class="go"&gt;a  b  c&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now the &lt;code&gt;trap&lt;/code&gt; does not appear to run at all. What's going on?&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;SIGINT&lt;/code&gt; handling in Python vs bash&lt;/h2&gt;
&lt;p&gt;When we press Ctrl+C, &lt;em&gt;all&lt;/em&gt; processes in the foreground process group receive the &lt;code&gt;SIGINT&lt;/code&gt; signal. So, both Python and bash get a chance to handle the signal.&lt;/p&gt;
&lt;p&gt;As expected, bash will run the &lt;code&gt;trap&lt;/code&gt; upon receiving &lt;code&gt;SIGINT&lt;/code&gt;. But, on the other hand, Python's &lt;code&gt;subprocess.run&lt;/code&gt; will give the bash process a grace period of &lt;a href="https://github.com/python/cpython/blob/d9444844d7f322334c001c1a301c560d88c78e76/Lib/subprocess.py#L882C11-L882C12"&gt;250 milliseconds&lt;/a&gt; to exit, and then forcefully kill the process. This is why the example's &lt;code&gt;trap&lt;/code&gt; got interrupted halfway through.&lt;/p&gt;
&lt;p&gt;When using &lt;code&gt;asyncio.create_subprocess_exec&lt;/code&gt;, we don't even get this grace period, so the bash process gets killed immediately, often before the &lt;code&gt;trap&lt;/code&gt; can run at all.&lt;/p&gt;
&lt;p&gt;By contrast, in a bash-only scenario (if the script were called from within another bash script), the exit &lt;code&gt;trap&lt;/code&gt; would not get interrupted. See this excerpt from &lt;a href="https://mywiki.wooledge.org/SignalTrap#Special_Note_On_SIGINT_and_SIGQUIT"&gt;Greg's Wiki&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;bash is among a few shells that implement a &lt;em&gt;wait and cooperative exit&lt;/em&gt; approach at handling SIGINT/SIGQUIT delivery. When interpreting a script, upon receiving a SIGINT, it doesn't exit straight away but instead waits for the currently running command to return and only exits (by killing itself with SIGINT) if that command was also killed by that SIGINT. The idea is that if your script calls vi for instance, and you press Ctrl+C within vi to cancel an action, that should not be considered as a request to abort the script.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Finally, note that bash and shell &lt;code&gt;trap&lt;/code&gt;s are irrelevant to the problem at large. For example, a C program that runs some expensive cleanup logic on &lt;code&gt;SIGINT&lt;/code&gt; would also get prematurely killed by Python.&lt;/p&gt;
&lt;h2&gt;What can we do about it?&lt;/h2&gt;
&lt;p&gt;Part of the problem is that our bash script's cleanup logic was never truly reliable after all. We implemented something that was apparently good enough, but as it often happens, it turns out that our assumption was too optimistic and it's not so uncommon to get our shell script killed after all.&lt;/p&gt;
&lt;p&gt;Unfortunately, it is not always simple or possible to implement bulletproof cleanup logic, which generally involves passing the buck to the operating system. For scenarios involving regular files, there are quite a few tricks, such as using pipes (including &lt;a href="https://www.gnu.org/software/bash/manual/html_node/Process-Substitution.html"&gt;bash's process substitution&lt;/a&gt;), and various Linux extras such as &lt;a href="https://manpages.ubuntu.com/manpages/bionic/man2/open.2.html"&gt;&lt;code&gt;open(..., O_TMPFILE)&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://manpages.ubuntu.com/manpages/jammy/man2/memfd_create.2.html"&gt;&lt;code&gt;memfd_create&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://manpages.ubuntu.com/manpages/bionic/man7/namespaces.7.html"&gt;namespaces&lt;/a&gt; (see also: &lt;a href="https://github.com/containers/bubblewrap"&gt;bubblewrap&lt;/a&gt;), and so on. But more complex scenarios involving cryptsetup, filesystem mounts, swap files, and so on, are often impossible or very awkward to handle this way.&lt;/p&gt;
&lt;p&gt;We can also try to work around it from the Python side, though unfortunately there doesn't seem to be any simple way to do it without adding a bunch of complexity (switching to lower level interfaces such as &lt;a href="https://docs.python.org/3/library/subprocess.html#subprocess.Popen"&gt;&lt;code&gt;subprocess.Popen&lt;/code&gt;&lt;/a&gt; and handling errors manually). This may be something to improve in the standard library in the future.&lt;/p&gt;
&lt;p&gt;Another possibility could be to spawn the exit logic as a background process that waits for the foreground script to exit and then cleans up, though this feels like pushing in the direction of building a Rube Goldberg machine that will break in even more subtle ways.&lt;/p&gt;
&lt;p&gt;Ultimately, I don't have any simple, universal and reliable fix for this at the moment. If you have any idea, don't hesitate to contact me!&lt;/p&gt;</content><category term="misc"/></entry><entry><title>Faster logins on dockerized SFTP</title><link href="https://www.zealcharm.com/blog/faster-logins-on-dockerized-sftp.html" rel="alternate"/><published>2021-06-02T00:00:00+00:00</published><updated>2021-06-02T00:00:00+00:00</updated><author><name>zealcharm</name></author><id>tag:www.zealcharm.com,2021-06-02:/blog/faster-logins-on-dockerized-sftp.html</id><summary type="html">&lt;p&gt;I recently dockerized an SFTP file server setup on an old Raspberry Pi 2 server, and I was surprised by how slow logging in to the server was: about 4 seconds per connection&lt;sup id="fnref:1"&gt;&lt;a class="footnote-ref" href="#fn:1"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;My first intuition was that the time was lost to network latency or encryption, but the …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I recently dockerized an SFTP file server setup on an old Raspberry Pi 2 server, and I was surprised by how slow logging in to the server was: about 4 seconds per connection&lt;sup id="fnref:1"&gt;&lt;a class="footnote-ref" href="#fn:1"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;My first intuition was that the time was lost to network latency or encryption, but the previous setup didn't have this problem, so that couldn't be it.&lt;/p&gt;
&lt;p&gt;In fact the problem easily reproduces on a cheap VPS, though to a lesser degree:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;--name&lt;span class="w"&gt; &lt;/span&gt;sftptest&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;127&lt;/span&gt;.0.0.1:1234:22&lt;span class="w"&gt; &lt;/span&gt;--entrypoint&lt;span class="w"&gt; &lt;/span&gt;ash&lt;span class="w"&gt; &lt;/span&gt;atmoz/sftp:alpine&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/entrypoint foo:pass:::upload&amp;quot;&lt;/span&gt;
&lt;span class="gp"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sshpass&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;pass&lt;span class="w"&gt; &lt;/span&gt;ssh&lt;span class="w"&gt; &lt;/span&gt;-p1234&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;PreferredAuthentications&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;password&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;StrictHostKeyChecking&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;no&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;UserKnownHostsFile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;foo@127.0.0.1
&lt;span class="go"&gt;[...]&lt;/span&gt;
&lt;span class="go"&gt;real    0m1.290s&lt;/span&gt;
&lt;span class="go"&gt;user    0m0.014s&lt;/span&gt;
&lt;span class="go"&gt;sys     0m0.008s&lt;/span&gt;
&lt;span class="gp"&gt;$ &lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;rm&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;sftptest
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;At first, I tried playing with various OpenSSH switches and debugging options, but that didn't help much, as the time seemed to be lost "doing nothing". But after some more tinkering, I tried &lt;code&gt;strace&lt;/code&gt;, and quickly found that it was trying to close several million file descriptors:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;--name&lt;span class="w"&gt; &lt;/span&gt;sftptest&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;127&lt;/span&gt;.0.0.1:1234:22&lt;span class="w"&gt; &lt;/span&gt;--entrypoint&lt;span class="w"&gt; &lt;/span&gt;ash&lt;span class="w"&gt; &lt;/span&gt;atmoz/sftp:alpine&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;apk add strace &amp;amp;&amp;amp; strace -f /entrypoint foo:pass:::upload&amp;quot;&lt;/span&gt;
&lt;span class="gp"&gt;$ &lt;/span&gt;timeout&lt;span class="w"&gt; &lt;/span&gt;10s&lt;span class="w"&gt; &lt;/span&gt;sshpass&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;pass&lt;span class="w"&gt; &lt;/span&gt;ssh&lt;span class="w"&gt; &lt;/span&gt;-p1234&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;PreferredAuthentications&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;password&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;StrictHostKeyChecking&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;no&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;UserKnownHostsFile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;foo@127.0.0.1
&lt;span class="gp"&gt;$ &lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;logs&lt;span class="w"&gt; &lt;/span&gt;sftptest
&lt;span class="go"&gt;[...a log of logs...]&lt;/span&gt;
&lt;span class="go"&gt;[pid    39] close(119676)               = -1 EBADF (Bad file descriptor)&lt;/span&gt;
&lt;span class="go"&gt;[pid    39] close(119677)               = -1 EBADF (Bad file descriptor)&lt;/span&gt;
&lt;span class="go"&gt;[pid    39] close(119678)               = -1 EBADF (Bad file descriptor)&lt;/span&gt;
&lt;span class="go"&gt;[pid    39] close(119679)               = -1 EBADF (Bad file descriptor)&lt;/span&gt;
&lt;span class="go"&gt;[pid    39] close(119680)               = -1 EBADF (Bad file descriptor)&lt;/span&gt;
&lt;span class="go"&gt;[pid    39] close(119681)               = -1 EBADF (Bad file descriptor)&lt;/span&gt;
&lt;span class="go"&gt;[pid    39] close(119682)               = -1 EBADF (Bad file descriptor)&lt;/span&gt;
&lt;span class="go"&gt;[...a lot of logs...]&lt;/span&gt;
&lt;span class="go"&gt;^C&lt;/span&gt;
&lt;span class="gp"&gt;$ &lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;rm&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;sftptest
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;After some more analysis, I found that all those &lt;code&gt;close()&lt;/code&gt; calls seemed to come from &lt;a href="https://github.com/openssh/openssh-portable/blob/2dc328023f60212cd29504fc05d849133ae47355/openbsd-compat/bsd-closefrom.c#L65"&gt;a function named &lt;code&gt;closefrom_fallback&lt;/code&gt; in OpenSSH&lt;/a&gt;, which will close all file descriptors up to &lt;code&gt;OPEN_MAX&lt;/code&gt; in an attempt to ensure most file descriptors are closed&lt;sup id="fnref:2"&gt;&lt;a class="footnote-ref" href="#fn:2"&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;So why does this only happen when running in a Docker container? The problem is that Docker containers will have by default a very generous limit of &lt;code&gt;1048576&lt;/code&gt; open files, instead of the typical default of &lt;code&gt;1024&lt;/code&gt; on typical setups:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;getconf&lt;span class="w"&gt; &lt;/span&gt;OPEN_MAX
&lt;span class="go"&gt;1024&lt;/span&gt;
&lt;span class="gp"&gt;$ &lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;--rm&lt;span class="w"&gt; &lt;/span&gt;alpine:3.13&lt;span class="w"&gt; &lt;/span&gt;getconf&lt;span class="w"&gt; &lt;/span&gt;OPEN_MAX
&lt;span class="go"&gt;1048576&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This high &lt;code&gt;OPEN_MAX&lt;/code&gt; setting, when combined with the brute-force 'close all file descriptors' approach, has already caused various issues in &lt;a href="https://github.com/moby/moby/issues/23137"&gt;various&lt;/a&gt; &lt;a href="https://github.com/jeroen/sys/issues/34"&gt;applications&lt;/a&gt;, but I haven't found anyone reporting this for OpenSSH, so I guess there are a lot of annoyingly slow SFTP logins out there.&lt;/p&gt;
&lt;p&gt;Thankfully working around the problem is pretty easy once we know the cause, by specifying a smaller &lt;code&gt;OPEN_MAX&lt;/code&gt; limit by passing the &lt;code&gt;--ulimit nofile=1024:1024&lt;/code&gt; flag to &lt;code&gt;docker run&lt;/code&gt; to get about 14x faster logins:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;--name&lt;span class="w"&gt; &lt;/span&gt;sftptest&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;127&lt;/span&gt;.0.0.1:1234:22&lt;span class="w"&gt; &lt;/span&gt;--ulimit&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;nofile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1024&lt;/span&gt;:1024&lt;span class="w"&gt; &lt;/span&gt;--entrypoint&lt;span class="w"&gt; &lt;/span&gt;ash&lt;span class="w"&gt; &lt;/span&gt;atmoz/sftp:alpine&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/entrypoint foo:pass:::upload&amp;quot;&lt;/span&gt;
&lt;span class="gp"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sshpass&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;pass&lt;span class="w"&gt; &lt;/span&gt;ssh&lt;span class="w"&gt; &lt;/span&gt;-p1234&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;PreferredAuthentications&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;password&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;StrictHostKeyChecking&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;no&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;UserKnownHostsFile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;foo@127.0.0.1
&lt;span class="go"&gt;[...]&lt;/span&gt;
&lt;span class="go"&gt;real    0m0.091s&lt;/span&gt;
&lt;span class="go"&gt;user    0m0.015s&lt;/span&gt;
&lt;span class="go"&gt;sys     0m0.000s&lt;/span&gt;
&lt;span class="gp"&gt;$ &lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;rm&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;sftptest
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="footnote"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;I was using &lt;a href="https://apps.apple.com/us/app/nplayer/id1116905928"&gt;nPlayer&lt;/a&gt; as the client, which seems to open a new connection on every folder change, so those 4 seconds quickly added up to a minute.&amp;#160;&lt;a class="footnote-backref" href="#fnref:1" title="Jump back to footnote 1 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;I haven't looked in detail why this &lt;code&gt;closefrom_fallback&lt;/code&gt; code path is hit, but it appears that OpenSSH will try to enumerate open files by looking at &lt;code&gt;/proc/$$/fd&lt;/code&gt;, but it can't do this after &lt;code&gt;chroot&lt;/code&gt; is called to jail the SFTP to a directory.&amp;#160;&lt;a class="footnote-backref" href="#fnref:2" title="Jump back to footnote 2 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content><category term="misc"/></entry><entry><title>Insanely easy to inadvertently expose services in development machines with Docker</title><link href="https://www.zealcharm.com/blog/insanely-easy-to-inadvertently-expose-services-in-development-machines-with-docker.html" rel="alternate"/><published>2021-01-12T00:00:00+00:00</published><updated>2021-01-12T00:00:00+00:00</updated><author><name>zealcharm</name></author><id>tag:www.zealcharm.com,2021-01-12:/blog/insanely-easy-to-inadvertently-expose-services-in-development-machines-with-docker.html</id><summary type="html">&lt;p&gt;As you probably know, &lt;a href="https://www.docker.com/"&gt;Docker&lt;/a&gt; is a popular way to deploy and develop software. If you use it for this second purpose, it's also insanely easy to inadvertently make your services available to more people than you, and too hard to ensure it doesn't.&lt;/p&gt;
&lt;p&gt;(There's nothing really new is in …&lt;/p&gt;</summary><content type="html">&lt;p&gt;As you probably know, &lt;a href="https://www.docker.com/"&gt;Docker&lt;/a&gt; is a popular way to deploy and develop software. If you use it for this second purpose, it's also insanely easy to inadvertently make your services available to more people than you, and too hard to ensure it doesn't.&lt;/p&gt;
&lt;p&gt;(There's nothing really new is in this post, it's just a quick rant and summary)&lt;/p&gt;
&lt;h2&gt;The first day&lt;/h2&gt;
&lt;p&gt;So, you install Docker on your development machine and you run something like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;80&lt;/span&gt;:80&lt;span class="w"&gt; &lt;/span&gt;myproject
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And now everyone on your network (or even the entire Internet) can access your project by connecting to your machine on port 80, because the default binding address of Docker is &lt;code&gt;0.0.0.0&lt;/code&gt; (listen to all interfaces).&lt;/p&gt;
&lt;p&gt;I can understand this, because you may want to use Docker on an actual server. But this is your development machine, so you only want your service accessible on loopback. So you decide to solve the problem for good, and set the &lt;code&gt;ip&lt;/code&gt; option on &lt;a href="https://docs.docker.com/engine/reference/commandline/dockerd/#daemon-configuration-file"&gt;the &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt; configuration file&lt;/a&gt; (the "default IP when binding container ports") to &lt;code&gt;127.0.0.1&lt;/code&gt; and call it a day, right?&lt;/p&gt;
&lt;h2&gt;The next day&lt;/h2&gt;
&lt;p&gt;With time your project grows, and you switch to &lt;code&gt;docker-compose&lt;/code&gt;, so you run something like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;cat&lt;span class="w"&gt; &lt;/span&gt;docker-compose.yml
&lt;span class="go"&gt;version: &amp;#39;3.3&amp;#39;&lt;/span&gt;
&lt;span class="go"&gt;services:&lt;/span&gt;
&lt;span class="go"&gt;    myproject:&lt;/span&gt;
&lt;span class="go"&gt;        ports:&lt;/span&gt;
&lt;span class="go"&gt;            - &amp;#39;80:80&amp;#39;&lt;/span&gt;
&lt;span class="go"&gt;        image: myproject&lt;/span&gt;
&lt;span class="gp"&gt;$ &lt;/span&gt;docker-compose&lt;span class="w"&gt; &lt;/span&gt;up
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And now your project is again accessible to everyone on your network. This is &lt;a href="https://github.com/docker/compose/issues/2999"&gt;docker-compose issue #2999&lt;/a&gt;. The problem is that the &lt;code&gt;ip&lt;/code&gt; option you configured before actually only applies to the &lt;em&gt;default&lt;/em&gt; network, and &lt;code&gt;docker-compose&lt;/code&gt; generally creates a new network for your project, which will bind again to &lt;code&gt;0.0.0.0&lt;/code&gt;. And as far as I know, there's no way to configure the default binding address for new networks.&lt;/p&gt;
&lt;h2&gt;Use a firewall!&lt;/h2&gt;
&lt;p&gt;So you decide to stop trying to configure Docker and use a firewall instead. You install &lt;a href="https://launchpad.net/ufw"&gt;ufw&lt;/a&gt; which is very easy to install and configure. Problem solved, right?&lt;/p&gt;
&lt;p&gt;Well, it turns out that &lt;a href="https://docs.docker.com/network/iptables/"&gt;Docker by default sets up iptables&lt;/a&gt; in a way that &lt;a href="https://github.com/moby/moby/issues/4737"&gt;bypasses UFW&lt;/a&gt;, so you are going to have to apply some extra configuration to make it work.&lt;/p&gt;
&lt;h2&gt;Non-solutions&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Always put the loopback IP on your docker commands and docker-compose.yml files:&lt;/strong&gt; This is too error prone and you are eventually going to make an human mistake and expose your services to the outside world. And it's painful to have to change every port binding between development and production machines.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Don't you have an home router which also acts as a firewall?&lt;/strong&gt; Yes, but it doesn't help if you have guests, or some other machine on your local network is compromised, or if there's some bug that allows the firewall to be bypassed (your random home router is probably not the best firewall on the market). And my philosophy is that you shouldn't rely on a firewall as the first and only security measure.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Use something else:&lt;/strong&gt; Even with those problems, Docker is often the best option overall. And you don't always make those decisions.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Actual solutions&lt;/h2&gt;
&lt;p&gt;As far as I know, if you don't want to rely on some workaround, your best chance is to use firewalld (&lt;a href="https://docs.docker.com/network/iptables/"&gt;which Docker integrated with&lt;/a&gt;), a hardware/external firewall, or an isolated virtual machine.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;UPDATE (2021-10-15):&lt;/em&gt; Nowadays, in addition to trying to avoid binding to all interfaces as much as I can, my go-to solution for development machines is using Docker in rootless mode instead, launching the daemon as an unprivileged user instead of root. Then, &lt;em&gt;by design&lt;/em&gt;, Docker can no longer bypass the firewall. Be warned though, that rootless mode can be somewhat painful to set up if you aren't on a rolling/fast-moving Linux distro.&lt;/p&gt;
&lt;h2&gt;Final note&lt;/h2&gt;
&lt;p&gt;I am not an expert in Docker networking. There may be some simpler way to fix this. But even if a simple solution actually exists, the fact that there are multiple options that at first sight ought to work but don't shows that this is a problem. How many developers are one (or zero) steps away from exposing their services?&lt;/p&gt;</content><category term="misc"/></entry><entry><title>Headless setup of Alpine Linux on the Raspberry Pi</title><link href="https://www.zealcharm.com/blog/headless-setup-of-alpine-linux-on-the-raspberry-pi.html" rel="alternate"/><published>2020-01-08T00:00:00+00:00</published><updated>2020-01-08T00:00:00+00:00</updated><author><name>zealcharm</name></author><id>tag:www.zealcharm.com,2020-01-08:/blog/headless-setup-of-alpine-linux-on-the-raspberry-pi.html</id><summary type="html">&lt;p&gt;&lt;a href="https://www.alpinelinux.org/"&gt;Alpine Linux&lt;/a&gt; is a lightweight Linux distribution which has gained popularity mainly as a base image for Docker containers. It is also a good candidate for a small system like the Raspberry Pi. However, I couldn't find out how to do a headless setup, i.e. one without a monitor …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;a href="https://www.alpinelinux.org/"&gt;Alpine Linux&lt;/a&gt; is a lightweight Linux distribution which has gained popularity mainly as a base image for Docker containers. It is also a good candidate for a small system like the Raspberry Pi. However, I couldn't find out how to do a headless setup, i.e. one without a monitor or keyboard, but rather just a network connection and SSH, unlike what's easily possible with &lt;a href="https://www.raspberrypi.org/documentation/configuration/wireless/headless.md"&gt;Raspbian&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This post explains how to do it. The process is a bit more convoluted, which is specially inconvenient since you can't get feedback from a monitor, but it can be done. Additionally, an important limitation is that you will need an Ethernet connection for the initial setup, not a wireless connection.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;To start, you will need to generate an SSH public/private key pair using &lt;code&gt;ssh-keygen&lt;/code&gt;, which you will use to authenticate through SSH once the system is ready.&lt;/p&gt;
&lt;p&gt;Afterwards, follow the &lt;a href="https://wiki.alpinelinux.org/wiki/Raspberry_Pi#Preparation"&gt;Raspberry Pi "Preparation" section&lt;/a&gt; in the Alpine Linux Wiki, up to the point where you have extracted the root filesystem on the SD card.&lt;/p&gt;
&lt;p&gt;Now, this is where the magic comes: It turns out that Alpine Linux's initramfs accepts &lt;a href="https://wiki.alpinelinux.org/wiki/Initramfs_init"&gt;some additional parameters in the kernel command line&lt;/a&gt; that allow us to initialize the network &lt;em&gt;and&lt;/em&gt; add an SSH public key to the &lt;code&gt;.ssh/authorized_keys&lt;/code&gt; file. Those are a bit obscure, yet more or less documented and perfect for our headless setup use case. We can add those parameters in the &lt;code&gt;cmdline.txt&lt;/code&gt; file in the root of the SD card in order to get a SSH connection after the system boots for the first time.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;The &lt;code&gt;ip&lt;/code&gt; parameter theoretically accepts the value &lt;code&gt;ip=dhcp&lt;/code&gt; for automatic IP assignment. However, this resulted in a boot failure in my experience, so we will use the static IP format instead. For a typical home network, this should look like the following: &lt;code&gt;ip=192.168.1.222::192.168.1.1:255.255.255.0::eth0::192.168.1.1:&lt;/code&gt; (where &lt;code&gt;192.168.1.222&lt;/code&gt; is the IP that the Raspberry Pi will have after booting). Note: You will be able to switch to DHCP after the initial setup.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The &lt;code&gt;ssh_key&lt;/code&gt; parameter accepts &lt;em&gt;an URL&lt;/em&gt; to an SSH public key. You can decide to upload your SSH public key somewhere, but if you have a computer on your same network as the Raspberry, an easy alternative is to serve the key yourself using e.g. Python's built-in HTTP server:&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;[~/temp_folder]$ &lt;/span&gt;ls
&lt;span class="go"&gt;mykey.pub&lt;/span&gt;
&lt;span class="gp"&gt;[~/temp_folder]$ &lt;/span&gt;cat&lt;span class="w"&gt; &lt;/span&gt;mykey.pub
&lt;span class="go"&gt;ssh-rsa blablablablabla root@mykey&lt;/span&gt;
&lt;span class="gp"&gt;[~/temp_folder]$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;http.server&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;8080&lt;/span&gt;
&lt;span class="go"&gt;Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For this setup, the parameter should look like the following: &lt;code&gt;ssh_key=http://192.168.1.111:8080/mykey.pub&lt;/code&gt; (where &lt;code&gt;192.168.1.111&lt;/code&gt; is the IP of the computer serving the key).&lt;/p&gt;
&lt;p&gt;Add those two parameters together in the &lt;code&gt;cmdline.txt&lt;/code&gt; file in the root of the SD card, so the entire file should look like the following:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;modules&lt;/span&gt;=&lt;span class="k"&gt;loop&lt;/span&gt;,&lt;span class="n"&gt;squashfs&lt;/span&gt;,&lt;span class="n"&gt;sd-mod&lt;/span&gt;,&lt;span class="n"&gt;usb-storage&lt;/span&gt; &lt;span class="n"&gt;quiet&lt;/span&gt; &lt;span class="n"&gt;console&lt;/span&gt;=&lt;span class="n"&gt;tty1&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt;=&lt;span class="mf"&gt;192.168.1.222&lt;/span&gt;::&lt;span class="mf"&gt;192.168.1.1&lt;/span&gt;:&lt;span class="mf"&gt;255.255.255.0&lt;/span&gt;::&lt;span class="n"&gt;eth0::192&lt;/span&gt;&lt;span class="mf"&gt;.168.1.1&lt;/span&gt;: &lt;span class="n"&gt;ssh_key&lt;/span&gt;=&lt;span class="n"&gt;http:&lt;/span&gt;//&lt;span class="mf"&gt;192.168.1.111&lt;/span&gt;:&lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;mykey&lt;/span&gt;.&lt;span class="n"&gt;pub&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Finally, unmount the SD card and try to boot your Raspberry with it. After a bit (the first boot will take longer than usual due to SSH key generation), it should boot correctly and make an SSH server available. Otherwise, make sure you haven't missed any step from the regular Alpine setup, and check if the system responds to ping or if it fetched the SSH key to diagnose the problem.&lt;/p&gt;
&lt;p&gt;If everything went well, you should be able to log as the &lt;code&gt;root&lt;/code&gt; user through SSH:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;[~]$ &lt;/span&gt;ssh&lt;span class="w"&gt; &lt;/span&gt;root@192.168.1.222&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;mykey
&lt;span class="go"&gt;Welcome to Alpine!&lt;/span&gt;

&lt;span class="go"&gt;The Alpine Wiki contains a large amount of how-to guides and general&lt;/span&gt;
&lt;span class="go"&gt;information about administrating Alpine systems.&lt;/span&gt;
&lt;span class="go"&gt;See &amp;lt;http://wiki.alpinelinux.org/&amp;gt;.&lt;/span&gt;

&lt;span class="go"&gt;You can setup the system with the command: setup-alpine&lt;/span&gt;

&lt;span class="go"&gt;You may change this message by editing /etc/motd.&lt;/span&gt;

&lt;span class="gp"&gt;localhost:~# &lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;After this, follow the steps in the Alpine Linux Wiki and run &lt;code&gt;setup-alpine&lt;/code&gt; to install the system. Afterwards, make sure to persist the SSH key with &lt;code&gt;lbu&lt;/code&gt;, in order to keep SSH access across reboots:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;localhost:~# &lt;/span&gt;lbu&lt;span class="w"&gt; &lt;/span&gt;include&lt;span class="w"&gt; &lt;/span&gt;/root/.ssh/authorized_keys&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="gp"&gt;localhost:~# &lt;/span&gt;lbu&lt;span class="w"&gt; &lt;/span&gt;commit&lt;span class="w"&gt; &lt;/span&gt;-d
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Finally, shut down the Raspberry, plug the SD card back into your computer, and remove the &lt;code&gt;ip&lt;/code&gt; and &lt;code&gt;ssh_key&lt;/code&gt; parameters from &lt;code&gt;cmdline.txt&lt;/code&gt;. You should now have a clean, SSH-accessible Alpine Linux install on your Raspberry Pi.&lt;/p&gt;</content><category term="misc"/></entry></feed>