<?xml version="1.0" encoding="UTF-8"?>
<!-- Thanks John Vincent @ https://www.johnvincent.io/jekyll/rss-feed-with-jekyll/ -->
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Adam&apos;s blog</title>
        <description></description>
        <link>https://blog.ahlava.cz/</link>
        <atom:link href="https://blog.ahlava.cz/feed.xml" rel="self" type="application/rss+xml"/>
        <pubDate>Sun, 07 Jun 2026 09:51:46 +0000</pubDate>
        <lastBuildDate>Sun, 07 Jun 2026 09:51:46 +0000</lastBuildDate>
        <generator>Jekyll v4.4.1</generator>
        
        
        <item>
            <title>My new home data backup strategy</title>
            <description>&lt;p&gt;A &lt;a href=&quot;/2022/06/15/backup-server-hdd.html&quot;&gt;few years back&lt;/a&gt;, I described how my off-site home data backup strategy worked. Since then, it was due for a big overhaul, which I have successfully completed. First, let’s go over how it was before and how I improved upon it.&lt;/p&gt;

</description>
            <pubDate>Mon, 23 Mar 2026 00:00:00 +0000</pubDate>
            <link>https://blog.ahlava.cz/2026/03/23/my-backup-solution.html</link>
            <guid isPermaLink="true">https://blog.ahlava.cz/2026/03/23/my-backup-solution.html</guid>
            
            <category>sysadmin</category>
            
            
            <content>
            &lt;p&gt;A &lt;a href=&quot;/2022/06/15/backup-server-hdd.html&quot;&gt;few years back&lt;/a&gt;, I described how my off-site home data backup strategy worked. Since then, it was due for a big overhaul, which I have successfully completed. First, let’s go over how it was before and how I improved upon it.&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;previous-setup&quot;&gt;Previous setup&lt;/h2&gt;

&lt;p&gt;In the past, my setup was fairly simple. I had one main server in the basement and one RPi in another house. Connected to the main server were two sets of HDDs – one for primary data, and a second for full backups. The RPi also had its own set of HDDs connected, but only for full backups. One crontab job rsynced data from the main HDDs to the backup HDDs, while a second job rsynced data from the main HDDs to the off-site RPi.&lt;/p&gt;

&lt;p&gt;This setup worked well enough for multiple years, but it had two main drawbacks:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;As the backup was done once a week, changes that happened more quickly would not be backed up anywhere.&lt;/li&gt;
  &lt;li&gt;The speed on the off-site RPi was so slow that the backup completed successfully only if there was not much new data added, effectively finishing only once in a few months.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It was evident that something needed to change; a week of work is a week of work, after all. You don’t want to lose it.&lt;/p&gt;

&lt;h2 id=&quot;current-setup&quot;&gt;Current setup&lt;/h2&gt;

&lt;p&gt;My current setup is much more layered. The first stage is daily snapshots using my &lt;a href=&quot;https://github.com/esoadamo/backup-prepare&quot;&gt;backup-prepare&lt;/a&gt; tool. This tool is able to create a full snapshot of the current state of a directory either via hardlinks or via BTRFS subvolumes, the latter being instant and the method I use. Every midnight, a read-only snapshot is created and kept for 8 days. Unless there is a hard drive failure, I lose at most a day’s work in case a restore is needed.&lt;/p&gt;

&lt;p&gt;The second layer is a weekly data backup using &lt;a href=&quot;https://borgbackup.readthedocs.io/&quot;&gt;borg&lt;/a&gt;. Borg is great at deduplicating data and never backs up anything that was already uploaded. After the first full backup is completed, every subsequent backup is much faster as it is only differential. This solution scales perfectly; it works for my VPSs with 10 GiB of HDD space up to my data disks with tens of TBs. I have created a &lt;a href=&quot;/assets/blog/my-backup-solution/do-backup.py&quot;&gt;custom wrapper&lt;/a&gt; (+ &lt;a href=&quot;/assets/blog/my-backup-solution/do-backup-toml.toml&quot;&gt;example config&lt;/a&gt;) that works great with the daily snapshots (each snapshot directory has a suffix with the current date, so borg always scans the files as new instead of using the cache). It uses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bwrap&lt;/code&gt; to make it seem like the latest snapshot always has the same path. The added benefit of using borg over snapshots is that I never get &lt;em&gt;“file changed during backup”&lt;/em&gt; warnings.&lt;/p&gt;

&lt;p&gt;Because borg can work over SSH connections, the borg repository is not saved on the backed-up device itself, but instead on one of my VPSes. This VPS has my &lt;a href=&quot;https://github.com/esoadamo/docker-dropbear-backup/&quot;&gt;dropbear-backup&lt;/a&gt; Docker image running. The main advantage is that I can easily pair SSH keys with different subfolders, making the creation of a new account with restricted access (only for borg, sftp, or rsync) a breeze.&lt;/p&gt;

&lt;p&gt;Still, we are talking about backing up tens of terabytes of data, which would cost a lot of money if it was all saved on the VPS disk. Instead, I use an &lt;a href=&quot;https://rclone.org/docker/&quot;&gt;rclone Docker plugin&lt;/a&gt; with an rclone-compatible storage provider mounted inside the dropbear backup container. This setup makes my storage virtually infinite, while the VPS itself has only a few GB of storage. A bonus is that I can put a virtual layer of encryption on top of the cloud storage using rclone’s crypt remote.&lt;/p&gt;

&lt;p&gt;The cloud storage can be considered warm; if needed, I can recover the files quickly. However, if a ransomware attack hit the VPS, it could very well encrypt the whole warm storage, as it is not backed up itself. To mitigate this, every week the entire warm cloud storage is synced to my local Raspberry Pi with multiple external HDDs attached. Together, they have enough storage to keep the whole cloud storage with 2 months of weekly snapshots. As the RPi is limited by residential network speeds, I consider it cold storage, because recovering a lot of data from there would take several weeks at least.&lt;/p&gt;

&lt;p&gt;However, we are still not done. Having only 2 months of history may not be enough for all my use cases, so I back up all the data from the RPi to a freezing cold &lt;a href=&quot;https://www.backblaze.com/&quot;&gt;Backblaze&lt;/a&gt; storage, which offers versioning and object locking, preventing a ransomware attack from destroying everything. Now we are done.&lt;/p&gt;

&lt;p&gt;If this was a lot to take in, here is a diagram detailing the levels:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/my-backup-solution/diagram.svg&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;Diagram of my backup solution&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;closing-words&quot;&gt;Closing words&lt;/h2&gt;

&lt;p&gt;My personal setup may be overkill for many, but it still might be a nice pointer on where to start when trying to establish your own backup procedures. I opted to have more storage capacity and accept the possible downtime in case of a hard drive failure instead of some form of RAID. However, if you would like to sleep better at night, I would suggest RAID 1 with BTRFS, for example. This way you can keep all the other features, such as CoW and (almost) instant snapshots.&lt;/p&gt;

&lt;h2 id=&quot;honorable-mentions&quot;&gt;Honorable mentions&lt;/h2&gt;

&lt;p&gt;I also considered other services for a while, but decided they were not the right match for me. That does not mean they won’t be the best match for you, though:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://rsync.net/&quot;&gt;rsync.net&lt;/a&gt; - access via standard Linux tools, but a little pricey&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.hetzner.com/storage/storage-box/&quot;&gt;Hetzner Storage&lt;/a&gt; - EU-based fast storage with snapshots and multiple protocol access&lt;/li&gt;
&lt;/ul&gt;

            </content>
        </item>
        
        
        
        <item>
            <title>(CZ) Technický průvodce získáním pedagogického osvědčení</title>
            <description>&lt;p&gt;Pokud jste jako já, nejspíše znáte pojem pedagogické minimum. Pokud nejste jako já a tento pojem neznáte, může to znamenat dvě věci. Zaprvé buď nevíte, že pedagogické minimum je osvědčení, bez kterého nemůžete učit. A nebo, zadruhé, jste se začali zajímat o učení až po září 2023 a zjistili jste, že nic jako pedagogické minimum neexistuje!&lt;/p&gt;

</description>
            <pubDate>Sun, 08 Feb 2026 00:00:00 +0000</pubDate>
            <link>https://blog.ahlava.cz/2026/02/08/pedagogicke-minimum.html</link>
            <guid isPermaLink="true">https://blog.ahlava.cz/2026/02/08/pedagogicke-minimum.html</guid>
            
            <category>czech</category>
            
            <category>life</category>
            
            <category>teaching</category>
            
            
            <content>
            &lt;p&gt;Pokud jste jako já, nejspíše znáte pojem pedagogické minimum. Pokud nejste jako já a tento pojem neznáte, může to znamenat dvě věci. Zaprvé buď nevíte, že pedagogické minimum je osvědčení, bez kterého nemůžete učit. A nebo, zadruhé, jste se začali zajímat o učení až po září 2023 a zjistili jste, že nic jako pedagogické minimum neexistuje!&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;pedagogické-minimum-neexistuje&quot;&gt;Pedagogické minimum neexistuje!&lt;/h2&gt;

&lt;p&gt;Ono totiž to je tak, že po chvíli googlení se teda dozvídáte, že &lt;em&gt;“pedagogické minimum však neexistuje”&lt;/em&gt; a že &lt;em&gt;“Díky novele zmiňovaného zákona mohou od 1. 9. 2023 začít učit i zájemci bez pedagogické kvalifikace a vzdělání si doplnit během prvních třech let práce ve školství. Do začátku je dobré mít motivaci, možnost si udělat čas na učení a chuť postupně si doplnit pedagogické vzdělání.”&lt;/em&gt; (zdroj: &lt;a href=&quot;https://www.zacniucit.cz/muzu-ucit&quot;&gt;zacniucit.cz&lt;/a&gt;, &lt;a href=&quot;https://web.archive.org/web/20250418170155/https://www.zacniucit.cz/muzu-ucit&quot;&gt;archiv&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/pedagogicke-minimum/muzu-ucit.png&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;Snímek obrazovky z zacniucit.cz&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Takže bezva, první roky můžete učit i bez jakékohokoliv pedagogického vzdělání! No, takže se do toho učení pustíte, učíte rok, učíte druhý rok a naráz zjistíte, že vás to vlastně baví a chtěli byste pokračovat. Ale co teď? Na třetí rok si už ale něco doplnit musíte. Co je ale to něco?&lt;/p&gt;

&lt;h2 id=&quot;hledání-správného-kurzu&quot;&gt;Hledání správného kurzu&lt;/h2&gt;

&lt;p&gt;Šikovná stránka &lt;a href=&quot;https://www.zacniucit.cz/muzu-ucit&quot;&gt;zacniucit.cz&lt;/a&gt; nám stále pomáhá: &lt;em&gt;“A co se skrývá pod tou pedagogickou kvalifikací? Nemusí to být nutně 5 let studia. Možná jste tento typ studia znali pod už zaniklým názvem „pedagogické minimum“. Nahradilo jej doplňkové pedagogické studium (DPS).”&lt;/em&gt; Kde už ale bohužel není tak nápomocná, je vyhledání toho přesného kurzu, který by vám DPS zabezpečil.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/pedagogicke-minimum/muzu-ucit-kurzy.png&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;Interaktivní průvodce kurzů DPS&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;No nic, čas ušpinit si ruce a přijít s nějakými kurzy starým dobrým internetovým vyhledáváním. Bydlíte v Brně jako já? Fajn! Máte dvě možnosti: Studium pedagogiky na &lt;a href=&quot;https://icv.mendelu.cz/studium-pedagogiky/?psn=55&quot;&gt;MENDELU&lt;/a&gt; (17 900,- Kč; &lt;a href=&quot;https://web.archive.org/web/20250613093918/https://icv.mendelu.cz/studium-pedagogiky/?psn=55&quot;&gt;archív&lt;/a&gt;) nebo &lt;a href=&quot;https://www.ped.muni.cz/czv/pro-zajemce-o-czv/programy-czv/dv24odbss&quot;&gt;PdF MUNI&lt;/a&gt; (10 900 Kč/1 semestr, 3 semestry, &lt;a href=&quot;https://web.archive.org/web/20250615130823/https://www.ped.muni.cz/czv/pro-zajemce-o-czv/programy-czv/dv24odbss&quot;&gt;archív&lt;/a&gt;) a máte hotovo během roku a půl. Nebo se vám možná nechce studovat rok a půl a nebo se vám možná nechce platit přes 15 000 Kč za celý kurz. Naštěstí existuje ještě jedna možnost – &lt;a href=&quot;https://www.petrklic-dvpp.cz/&quot;&gt;DVPP Petrklíč&lt;/a&gt;. Petrklíč nabízí kurz, který je hotový během půl roku, stojí 14 000,- Kč v základu a nebo můžete přímo požádat úřad práce o zaplacení celé částky. To zní ideálně, že? Jediná nevýhoda – od jara 2025 už neposkytují kurzy v Brně, ale jen a pouze v Praze. No co už, pořád to vypadá jako nejlepší možnost, pojďme se tedy pustit do žádosti o proplacení od úřadu práce.&lt;/p&gt;

&lt;h2 id=&quot;úřad-práce&quot;&gt;Úřad práce&lt;/h2&gt;

&lt;p&gt;Petrklíč vypadá jako ideální možnost (až na to, že je pouze v Praze). Nejlevnější, nejkratší a pokud se přihlásíte přes projekt Úřadu práce, bude dokonce úplně zdarma! Jak tedy vypadá přihláška pro úřad práce? V mém případě měla cesta tři kroky. Je ale možné, a také v to doufám a věřím, že zkušenosti jiných se budou lišit. Každopádně moje tři kroky byly:&lt;/p&gt;

&lt;h3 id=&quot;krok-první--zájemce-o-práci&quot;&gt;Krok první – zájemce o práci&lt;/h3&gt;

&lt;p&gt;Abyste mohli žádat o proplacení (re)kvalifikačního kurzu od úřadu práce, musíte se na úřadu práce &lt;em&gt;zaregistrovat&lt;/em&gt;. Existují dvě úrovně registrace – uchazeč o práci a zájemce o práci. Jelikož mám i druhé, hlavní zaměstnání, zajímalo mě, kterou úroveň mám zvolit a jaké pro mne z toho plynou povinnosti. Po několika hodinách googlení a další hodince přímo na úřadu práce vyplynulo, že se chci stát zájemcem o práci. Proč? Protože uchazeč je někdo, kdo žádnou práci zrovna nemá a z toho vyplývají další povinnosti. Na druhou stranu být zájemce o práci registrovaný na úřadu práce je zhruba to stejné, jako založit si účet na Seznamu.&lt;/p&gt;

&lt;h3 id=&quot;krok-druhý--vyplnit-formulář&quot;&gt;Krok druhý – vyplnit formulář&lt;/h3&gt;

&lt;p&gt;Protože úřad práce je pokrokový, dovolí vám všechno vyřídit online. Stačí se přihlásit bankovní identitou, rozkliknout správný kurz (na ten je odkaz přímo ze stránek &lt;a href=&quot;https://www.petrklic-dvpp.cz/prihlaska/praha/&quot;&gt;Petrklíče&lt;/a&gt;) a vyplnit přihlášku (včetně vašeho vzdělání, zaměstnání, motivační dopis, atp.). Pak stačí jen tento formulář odeslat a čekat, než vám úřad práce pošle mail, abyste poslali všechny tyto údaje znovu.&lt;/p&gt;

&lt;h3 id=&quot;krok-třetí--zamítnutí&quot;&gt;Krok třetí – zamítnutí&lt;/h3&gt;

&lt;p&gt;Pokud vám, stejně jako mě, přijde čekání na rozhodnutí úřadu o proplacení kurzu dlouhé, můžete ve volné chvíli vždy nakouknout, jestli už nějaké to rozhodnutí nepřišlo. Rozhodně nečekejte, že vám snad úřad pošle nějaké upozornění, prostě si na to máte pamatovat sami. A tak když tak z dlouhé chvíle koukáte, možná se jednou dozvíte, že vám úřad vaši účast proplatit zamítl. Bohužel prý nejsem ohrožen ztrátou zaměstnání a zaplatit mi kurz a akreditaci na učitele by bylo nehospodárné. No budiž, sice jsem jako důvod ohrožení zaměstnání uváděl zákon, který mi ukládá tento kurz absolvovat, nebo již nebudu moci učit, podložený smlouvami o provedení práce, nicméně to je očividně slabý argument. Rozhodnutí úřadu je konečné a já vám ze celého srdce přeji, abyste měli více úspěchů s vyřízením žádosti než já.&lt;/p&gt;

&lt;h2 id=&quot;praha&quot;&gt;Praha&lt;/h2&gt;

&lt;p&gt;No ale teď už zase zpátky do pozitiva. Pokud jste tady, nejspíš vám žádost o proplacení vyšla, nebo máte prostě silnou motivaci učit a kurz si zaplatíte tak jako tak. Tím, že jediná nabídka je nyní pouze v Praze, je čas vycestovat do našeho hlavního města. Velmi příjemné je, že kurz se v prezenční výuce vyučuje pouze kousek od Václaváku, takže v pohodě můžete jet vlakem ČD v pátek v 9h z Brna a pak vlakem v 14h zase hezky zpátky. Jen si určitě dejte pozor na místenky, ty vlaky bývají hodně narvané.&lt;/p&gt;

&lt;h2 id=&quot;kurz-samotný&quot;&gt;Kurz samotný&lt;/h2&gt;

&lt;p&gt;Kurz samotný je na moc pěkné lokalitě &lt;a href=&quot;https://mapy.com/s/nodehufeco&quot;&gt;Školská 32&lt;/a&gt;, hned kousek za Václavákem. Pokud je ale vedro a nebo se chcete vyhnout Václaváku kvůli počtu turistů, doporučuji vzít šalinu/tramvaj 9 na Národní třídu a hned tam do obchoďáku &lt;a href=&quot;https://mapy.com/s/nepehuvuro&quot;&gt;Quadrio&lt;/a&gt;. Nejenže tady mají super fast foody od Hugo, přes Indii až po McDonalds’, je tady i stanice metra a proto je velká šance, že tuto budovu navštívíte častěji. Na druhou stranu, pokud by vám tento McDonalds’ nevyhovoval, ničeho se nebojte. Pěšky se odtud na adresu kurzu dostanete asi za 8 minut a cestou potkáte ještě další 2 pobočky této frančízy. Pokud byste ale chtěli jen někam na kávičku, budova kurzu je pěkně vybavená – mají tu i kuchyňku, kde si sami můžete připravit volně dostupnou rozpustnou kávu.&lt;/p&gt;

&lt;h3 id=&quot;týden-první&quot;&gt;Týden první&lt;/h3&gt;

&lt;p&gt;Jak se dá čekat, týden první je poznávací. Uvítá vás nejspíše paní Iva Stratilová, velmi zkušená pedagožka, která vás provede úvodem do pedagogiky, forem a metod. Tento týden se rozhodně vyplatí přijít, protože všechnu probíranou látku také rovnou prakticky zkoušíte – a prakticky si vyzkoušet je prostě super!&lt;/p&gt;

&lt;h3 id=&quot;týden-druhý&quot;&gt;Týden druhý&lt;/h3&gt;

&lt;p&gt;Ve druhém týdnu nás přivítal pan Edgar. Už od prvního pohledu velice veselý a energetický člověk, nicméně v přínosu nových dovedností a kompetencí nastal výrazný propad. Sic s námi za jeden víkend prošel téměř všechny závětečné otázky ke kurzu, všechen výklad byl prakticky a pouze výklad s občasným vtipem. Přečíst si dostupná skripta k předmětu by mělo nejspíše podobný výsledek. Co ale jeho výklad zachraňovalo byly příběhy z jeho života, často s pedagogikou spojené jen velmi vágně. Jestli ale slyšet příběhy ze života stojí za celovíkendovou cestu do Prahy nechám už na posouzení čtenáře.&lt;/p&gt;

&lt;h3 id=&quot;online-výuka&quot;&gt;Online výuka&lt;/h3&gt;

&lt;p&gt;Během letních prázdnin probíhala výuka téměř výhradně online. Všechny týdny, kterých jsem se zúčastnil vypadaly velmi podobně – v pátek od 14:00 do 18:00, o víkendu 8:30 – 12:30. Na začátku i na konci musí mít všichni povinně zapnutou kameru, aby byla pořízena fotka do evidence. V průběhu pak bylo kameru povoleno vypnout. Na všech přednášejících bylo znát, že je výuka baví a že mají povědomí o tom, co říkají. Obsahově jsme se pohybovali v základech psychologie a výchovy na úrovni střední školy (emoce, počitky apod.), vzdělávací systém v České Republice (školka, základní škola, …), odborná didaktika. Na posledním online víkendu jsem nebyl kvůli dřívějším závazkům, čímž jsem si také vyčerpal všechnu povolenou absenci. Mírnou nevýhodou bylo, že nejspíše nikdo nekoordinoval obsah prezentací jednotlivých přednášejících – každá prezentace, která se byť jen vzdáleně týkala didaktiky, obsahovala znovu stejný úvod do ní a jednotlivých typů, který vždy zabral minimálně půl hodiny.&lt;/p&gt;

&lt;h3 id=&quot;týden-třetí&quot;&gt;Týden třetí&lt;/h3&gt;

&lt;p&gt;Po letních prázdninách zpátky v Praze. Tentokrát s dcerou paní Ivy Stratilové, Štěpánkou Stratilovou. Tématem víkendu byly kompetence, způsoby hodnocení a konflikty. Paní Štěpánka Stratilová působí jako mediátor, takže odbornostně byl celý víkend na vysoké úrovni. Zajímavé bylo, že výsledkem celého víkendu byl pocit, že učitel odborných předmětů je jedinou nadějí na žákovo spasení a že by měl se svým žákem řešit přímo všechny jeho osobní problémy. O tom, že existuje na škole výchovný poradce nebo metodik prevence, který má téměř jistě více kompetencí k vyřešení náročnějších situací, nepadlo ani slovo.&lt;/p&gt;

&lt;h3 id=&quot;týden-čtvrtý&quot;&gt;Týden čtvrtý&lt;/h3&gt;

&lt;p&gt;Chyběl jsem kvůli pracovní cestě, spotřeboval jsem tím všechnu povolenou absenci pro prezenční víkendy.&lt;/p&gt;

&lt;h3 id=&quot;týden-pátý&quot;&gt;Týden pátý&lt;/h3&gt;

&lt;p&gt;Pátý týden byl ve znamení etické výuky. Byl velmi dobře připravený, nicméně, jak sama lektorka podotkla, materiály pro etickou výchovu končí devátou třídou. Přestože se jednalo o zajímavé téma, nebylo pro nikoho z nás zúčastněných příliš aplikovatelné. Navíc jsem se tohoto bloku účastnil nemocný, jelikož jsem si všechnu absenci vybral na pracovní cestu, takže jsem si jen přál, abych se mohl co nejdříve vrátit z Prahy domů.&lt;/p&gt;

&lt;h2 id=&quot;závěrečná-práce-a-zkouška&quot;&gt;Závěrečná práce a zkouška&lt;/h2&gt;

&lt;p&gt;Ukončení celého kurzu se skládalo ze závěrečné práce, její obhajoby a závěrečné zkoušky. Závěrečná práce se píše na jedno z předem zadaných témat, rozsahem se má pohybovat kolem 10 000 znaků (tedy krapet kratší než tento blogový příspěvek). Na závěrečnou práci máte dvě možnosti konzultace v rámci výuky, odevzdání je dva týdny před závěrečnou zkouškou. Obecně hodnotím zkušenost se závěrečnou prací jako pozitivní, byla mi schválena hned po odeslání na první konzultaci.&lt;/p&gt;

&lt;p&gt;Závěrečná zkouška probíhala také hladce. Bylo cca 30 otázek, každá rozdělená ještě na podotázky s procentuální hodnotou. Aby byla zkouška úspěšně složena, musel zkoušený odpovědět na minimálně 80 % každé ze tří vylosovaných otázek. Atmosféra po dobu zkoušení byla dobrá, zkoušející se doptávali na doplňující otázky, takže spíše než strohé zkoušení a hledání mezer se jednalo o dialog. Pokud tedy člověku nějaký pojem vypadl, měl možnost si jej v rámci dialogu odvodit či dohledat v paměti.&lt;/p&gt;

&lt;h2 id=&quot;horší-zkušenosti&quot;&gt;Horší zkušenosti&lt;/h2&gt;

&lt;p&gt;Během průběhu celého kurzu se opakovalo pár ne-úplně v pořádku momentů. Předně komunikace ze strany vedoucích kurzu směrem k nám byla poměrně strohá – např. když se změnily termíny prezenčního kurzu, nebyl o tom zaslán žádný email. Jediný způsob, jak to zjistit, bylo přihlásit se do webového portálu a všimnout si upozornění. Protože mi nové termíny nevyhovovaly, podal jsem otázku, jestli se i nové termíny započítávají už i do tak šibeniční povolené absence a jaká témata budou nově probíraná ve který den. Trvalo několik týdnů, než jsem dostal odpověď s novým harmonogramem a nabídku, že pokud mi nové termíny nevyhovují, mohu absolvovat v původních termínech obsah stejný, nicméně s jinou studijní skupinou.&lt;/p&gt;

&lt;p&gt;Dalším opakujícím se jevem byla averze k zasílání prezentací. Vysvětlení bylo takové, že vše potřebné bychom měli mít ve vypracovaných skriptech. To ale pokaždé nešlo dohromady s faktem, že některé zajímavé informace se vyskytly pouze v prezentaci a ve skriptech nebyly zmíněny vůbec. Velmi cením práci, kterou vypracování skript muselo zabrat, nicméně bych rád měl možnost mít jednoduchý přístup i k materiálům, které nejsou nezbytně nutné pro závěrečnou zkoušku.&lt;/p&gt;

&lt;h2 id=&quot;certifikát&quot;&gt;Certifikát&lt;/h2&gt;

&lt;p&gt;Jako důkaz o absolvování kurzu mi po krátké době po zkoušce dorazil elektronický certifikát. Jedná se o jedinou formu osvědčení, kterou za kurz dostanete. Proto je také potřeba se ujistit, že vydaný certifikát je důvěryhodný – ideálně obsahuje kvalifikovaný elektronický podpis, který je platný v celé EU. Takový certifikát se dá nyní získat už &lt;a href=&quot;https://www.lupa.cz/clanky/certifikat-pro-elektronicky-podpis-i-bez-cesty-na-pobocku-jak-funguje-sluzba-certifikat-online-od-postsignum/&quot;&gt;i online&lt;/a&gt;. Původní certifikát, který mi dorazil, takový ale nebyl. Byl totiž podepsaný tzv. self-signed certifikátem, který si může vygenerovat kdokoliv a kdykoliv, takže nenesl žádnou právní hodnotu, na což mě můj PDF prohlížeč neopomněl hlasitě upozornit. Příjemně mě potěšilo, když po mé žádosti přišel certifikát s plně ověřitelným podpisem. Věřím, že tento typ podpisu bude využíván i nadále, nicméně přesto doporučuji po obdržení certifikátu provést kontrolu.&lt;/p&gt;

&lt;h2 id=&quot;finanční-okénko&quot;&gt;Finanční okénko&lt;/h2&gt;

&lt;p&gt;Zajímá vás, na kolik mne nakonec kurz vyšel? Po sečtení všech částek je cena následující:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;&lt;strong&gt;Kategorie&lt;/strong&gt;&lt;/th&gt;
      &lt;th&gt;&lt;strong&gt;Cena&lt;/strong&gt;&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Kurz&lt;/td&gt;
      &lt;td&gt;14 000, 00 Kč&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Ubytování&lt;/td&gt;
      &lt;td&gt;9 935,00 Kč&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Cestování&lt;/td&gt;
      &lt;td&gt;2 970,00 Kč&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Strava&lt;/td&gt;
      &lt;td&gt;1 896,00 Kč&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Ostatní&lt;/td&gt;
      &lt;td&gt;60,00 Kč&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Celkem&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;28 861,00 Kč&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Nutno dodat, že jsem se sice snažil zbytečně nerozhazovat, nicméně taky jsem nešel s cenou tak nízko jak to šlo – mohl bych například celý víkend vždy jíst jen pečivo a vyšlo by to nakonec o pár stovek levněji.  Pokud by vás zajímalo opravdu podrobné rozúčtování, můžete si jej stáhnout &lt;a href=&quot;/assets/blog/pedagogicke-minimum/costs.ods&quot;&gt;tady&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;závěr&quot;&gt;Závěr&lt;/h2&gt;

&lt;p&gt;Přestože tento text může místy působit trochu sarkasticky, mým cílem není očerňovat jednotlivé aktéry. Naopak, rád bych upozornil na situaci, kdy bez velmi silné motivace nejspíše uchazeč o pedagogickou praxi raději celou učitelskou dráhu vzdá. Je mi doopravdy líto, že v momentě, kdy chybí učitelé všech možných předmětů, české prostředí ještě ztěžuje podmínky pro získání pedagogického osvědčení na všech možných frontách. Sám jsem nejednou zvažoval, jestli mi celý tento proces za ty nervy doopravdy stojí. Mohu tedy jen doufat, že do budoucna bude situace jednodušší. Ve výsledku jsem alespoň rád, že zkouška nebyla extra náročná a mám osvědčení o jejím absolvování.&lt;/p&gt;

&lt;h2 id=&quot;bonus-pražský-motoráček&quot;&gt;Bonus: Pražský motoráček&lt;/h2&gt;

&lt;p&gt;Pokud stejně jako já budete bydlet v &lt;a href=&quot;https://www.booking.com/hotel/cz/apartments-v-bloku.cs.html&quot;&gt;Apartments V Bloku&lt;/a&gt; (&lt;a href=&quot;https://web.archive.org/web/20250629124424/https://www.booking.com/hotel/cz/apartments-v-bloku.cs.html&quot;&gt;archív&lt;/a&gt;) v Jinonicích, možná zjistíte, že o víkendu jezdí kolem vás historický vlak &lt;a href=&quot;https://pid.cz/zabava-a-zajimavosti/prazsky-motoracek/&quot;&gt;Pražský motoráček&lt;/a&gt; (&lt;a href=&quot;https://web.archive.org/web/20250423151656/https://pid.cz/zabava-a-zajimavosti/prazsky-motoracek/&quot;&gt;archív&lt;/a&gt;). A taky, že když v sobotu končíte s kurzem kolem jedné, stihnete v pohodě poslední jízdu motoráčku v 15:07 z Prahy-Jinonice. Přestože se jedná o historický vlak, platí na něj normálně klasická PID jízdenka. Konkrétně, co se vám může podařit s 90 minutovou jízdenkou je následující:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Nastoupit 15:07 v zastávce Praha-Jinonice&lt;/li&gt;
  &lt;li&gt;Vystoupit 15:12 v zastávce Praha-Stodůlky&lt;/li&gt;
  &lt;li&gt;Projít se pěšky lesem 4km kolem soch do Praha-Cibulka&lt;/li&gt;
  &lt;li&gt;Nastoupit 16:18 v zastávce Praha-Cibulka&lt;/li&gt;
  &lt;li&gt;Vystoupit 16:27 v zastávce Praha-Žvahov&lt;/li&gt;
  &lt;li&gt;Projít se pěšky 5km na Děvín a kolem výběhu koně převalského zpátky domů&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Je to moc hezký výlet plný krásných panoramat. Jen si nezapomeňte dost vody a opalováku!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/pedagogicke-minimum/motoracek.jpg&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;Historický Pražský motoráček&lt;/em&gt;&lt;/p&gt;

            </content>
        </item>
        
        
        
        <item>
            <title>2025 Recap: Books &amp; Podcasts</title>
            <description>&lt;p&gt;As the 2025 has come to an end, let’s recap on some of the books I have read this year and what podcasts did I listen to.&lt;/p&gt;

</description>
            <pubDate>Tue, 13 Jan 2026 00:00:00 +0000</pubDate>
            <link>https://blog.ahlava.cz/2026/01/13/books-2025.html</link>
            <guid isPermaLink="true">https://blog.ahlava.cz/2026/01/13/books-2025.html</guid>
            
            <category>life</category>
            
            <category>short</category>
            
            
            <content>
            &lt;p&gt;As the 2025 has come to an end, let’s recap on some of the books I have read this year and what podcasts did I listen to.&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;books&quot;&gt;Books&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.goodreads.com/book/show/34942741-the-consuming-fire&quot;&gt;The Consuming Fire (The Interdependency, #2)&lt;/a&gt; by &lt;a href=&quot;https://whatever.scalzi.com/&quot;&gt;John Scalzi&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;The second part of the series was more focused on smaller scale of the world, but I really liked the development and further word building. Hooked for the third part.&lt;/li&gt;
      &lt;li&gt;Book 1 - very cool, hooked imediatelly&lt;/li&gt;
      &lt;li&gt;Book 2 - a lot more graphic, hope to get connection back to the original plot line&lt;/li&gt;
      &lt;li&gt;Book 3 - nice finish, I like how it ended&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.goodreads.com/book/show/36510196-old-man-s-war&quot;&gt;Old Man’s War Series&lt;/a&gt; by &lt;a href=&quot;https://whatever.scalzi.com/&quot;&gt;John Scalzi&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;Book 1 - great word building, I hope to see more of the Perry character&lt;/li&gt;
      &lt;li&gt;Book 2 - a lot more graphic, hope to get connection back to the original plot line&lt;/li&gt;
      &lt;li&gt;Book 3 - this worked out great, I now understand book 1 and 2&lt;/li&gt;
      &lt;li&gt;Book 4 - well now I understand book 3 and I love it much more&lt;/li&gt;
      &lt;li&gt;Book 5 - happy to see that B-Team is doing its own thing&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;All of &lt;a href=&quot;https://www.goodreads.com/book/show/32758901-all-systems-red?ref=nav_sb_ss_1_9&quot;&gt;Murderbot&lt;/a&gt; by &lt;a href=&quot;https://www.goodreads.com/marthawells&quot;&gt;Martha Wells&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;Loved it, would want more. However the fact that the release order of books is not chronological storywise got me confused for a few moments.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.goodreads.com/book/show/7081.The_Android_s_Dream&quot;&gt;The Android’s Dream&lt;/a&gt; by &lt;a href=&quot;https://whatever.scalzi.com/&quot;&gt;John Scalzi&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;Wanted more android sci-fi, did not get it, but was not dissapointed&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.goodreads.com/book/show/88072.Agent_to_the_Stars&quot;&gt;Agent to the Stars&lt;/a&gt; by &lt;a href=&quot;https://whatever.scalzi.com/&quot;&gt;John Scalzi&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;Second book about aliens communicating by smell in a row? Very predictable, but loved it nontheless&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.goodreads.com/book/show/55291869-murder-by-other-means&quot;&gt;Murder by Other Means (The Dispatcher, #2)&lt;/a&gt; by &lt;a href=&quot;https://whatever.scalzi.com/&quot;&gt;John Scalzi&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;Good read, but maybe better if I would read the first book first?&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.goodreads.com/book/show/26082188-the-dispatcher&quot;&gt;The Dispatcher&lt;/a&gt; by &lt;a href=&quot;https://whatever.scalzi.com/&quot;&gt;John Scalzi&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;Well now I know I enjoyed the second book more without knowing the first one first. Still good&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.goodreads.com/book/show/222350268-syndik-t&quot;&gt;Syndikát&lt;/a&gt; by &lt;a href=&quot;https://www.goodreads.com/author/show/4607159.Leo_Ky_a&quot;&gt;Leoš Krysa&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;A Czech Sci-Fi with good narration with a toons of Czech references? Count me in! Loved the whole story except for the last quarter, which was a little weaker.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.goodreads.com/book/show/35018901-head-on&quot;&gt;Head-on (Lock In, #2)&lt;/a&gt; by &lt;a href=&quot;https://whatever.scalzi.com/&quot;&gt;John Scalzi&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;Again a little confused, loved it in the end, but maybe better if I read the first book first?&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.goodreads.com/book/show/21418013-lock-in&quot;&gt;Lock-in&lt;/a&gt; by &lt;a href=&quot;https://whatever.scalzi.com/&quot;&gt;John Scalzi&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;And again, I liked the second book better. Glad that I have started with it, but still enjoyable.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.goodreads.com/book/show/70535.2001&quot;&gt;2001: A Space Odyssey&lt;/a&gt; by &lt;a href=&quot;https://www.goodreads.com/author/show/7779.Arthur_C_Clarke&quot;&gt;Arthur C. Clarke&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;Never read this classic before, nor seen the movie. A little more confusing story and shorter than I expected, but still very enjoyable&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.goodreads.com/book/show/51101829-a-very-scalzi-christmas&quot;&gt;A Very Scalzi Christmas&lt;/a&gt; by &lt;a href=&quot;https://whatever.scalzi.com/&quot;&gt;John Scalzi&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;Read this one just before Christmas, laugh out loud, definitely would reccomend&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;podcasts&quot;&gt;Podcasts&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://darknetdiaries.com/&quot;&gt;Darknet Diaries&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;Discovered recently, large backlog with great content and goosebumps-producing stories&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://nebula.tv/thelayover&quot;&gt;The Layover&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;Weekly recap and behind the scenes of what happened during &lt;a href=&quot;https://www.youtube.com/@jetlagthegame&quot;&gt;Jet Lag: The Game&lt;/a&gt; with some entertaining in-between seasons stuff&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://nebula.tv/probablynotaliens&quot;&gt;It’s Probably (not) Aliens&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;Soo many episode when starting now, very great explanations of why Aliens were not the cause, but super-interesting history was it instead&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

            </content>
        </item>
        
        
        
        <item>
            <title>Meeting a scammer in Minecraft</title>
            <description>&lt;p&gt;A few days back my friends and I were searching for a new Minecraft server to play the Bedwars gamemode on. Little did we know what rollercoaster it would take us and that we had a new old friend already waiting for us.&lt;/p&gt;

</description>
            <pubDate>Sat, 18 Jan 2025 00:00:00 +0000</pubDate>
            <link>https://blog.ahlava.cz/2025/01/18/minecraft-scammer.html</link>
            <guid isPermaLink="true">https://blog.ahlava.cz/2025/01/18/minecraft-scammer.html</guid>
            
            <category>cybersec</category>
            
            
            <content>
            &lt;p&gt;A few days back my friends and I were searching for a new Minecraft server to play the Bedwars gamemode on. Little did we know what rollercoaster it would take us and that we had a new old friend already waiting for us.&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;joining-a-new-server&quot;&gt;Joining a new server&lt;/h2&gt;

&lt;p&gt;Most of the time when playing Minecraft minigames competitively, since the closing of &lt;a href=&quot;https://en.wikipedia.org/wiki/Mineplex&quot;&gt;Mineplex&lt;/a&gt;, we are playing on a &lt;a href=&quot;https://hypixel.net/&quot;&gt;Hypixel server&lt;/a&gt; – the most popular server there is, which comes with a super-scary anti-cheat or anti-scam watchdog. However, this server is running on the pre-1.9 version, which features an old Minecraft combat system, while we prefer the newer one. This is why we decided to venture and find a new server and we landed on &lt;a href=&quot;https://www.cubecraft.net/play/java/&quot;&gt;Cubecraft&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Immediately after joining my friend was greeted by some &lt;em&gt;“old friend”&lt;/em&gt;! Said &lt;em&gt;“old friend”&lt;/em&gt; tried to get in touch again after a few years they played together. They even knew they were from Czechia!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/minecraft-scam/01a-welcome.png&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;Being welcomed immediately after joining a server&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/minecraft-scam/01b-welcome-chat.png&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;Welcoming chat - Scammer (red), my friend (blue)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At this point, we were hooked and wanted to see where this friend went.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Quick Q&amp;amp;A&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;em&gt;Why was my friend approached?&lt;/em&gt; A friend of mine has a special cosmetic item in Minecraft and accounts with this special cosmetic item – a cape – are being sold for a lot of money, so stealing them is a good business. He is contacted quite often with offers and regular phishing, however, meeting someone pretending to be his friend was first for us all.&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;How the scammer knew my friend is from Czechia?&lt;/em&gt; They have Czech flag on their &lt;a href=&quot;https://namemc.com/&quot;&gt;NameMC&lt;/a&gt; profile.&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;How was the scammer able to single out my friend seconds after joining a new server?&lt;/em&gt; No idea, this was honestly super impressive.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;playing-with-a-new-old-friend&quot;&gt;Playing with a new old friend&lt;/h2&gt;

&lt;p&gt;We have played a few rounds, in which the scammer was super nice to us – they gave us diamonds even when they were on different teams and never attacked us &lt;em&gt;(which is a bannable offence in this game, FYI)&lt;/em&gt; and even invited one more of their friends into the group. We all were having fun! You know what is more fun? Calling with friends while we are playing.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/minecraft-scam/02-call.png&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;After a few rounds, they asked us to join their Discord server for a call - Scammer (red), scammer’s friend (pink), my friend (blue)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And this was the other shoe – an invitation to a Discord server &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;discord[.]gg/furnace&lt;/code&gt;. All we need to join this server and verify our Minecraft accounts to be able to join the voice call. Interestingly, when we invited them to our own Discord server they did not join and instead pressured us to join their server. Weird, right?&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/minecraft-scam/03-join-discord.png&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;The verification is nothing sinister, pinky promise - Scammer (red), scammer’s friend (pink), my friend (blue)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;By the way, this is where the friend of the scammer came into play – they were supposed to show us how easy is to join the Discord server.&lt;/p&gt;

&lt;h2 id=&quot;verifying-minecraft-account&quot;&gt;Verifying Minecraft account&lt;/h2&gt;

&lt;p&gt;As stated above, when you join the public Discord server, you have to “verify” your account before you are able to do anything else. There is a Q&amp;amp;A about why the link is necessary and a big green button with the text Link account.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/minecraft-scam/04-link.png&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;Verify your Minecraft account by giving a random Discord bot your Microsoft email and Minecraft username&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;After clicking the button, you find out that the verification itself is very easy. All you need to do is enter your Minecraft username and Microsoft account email (Minecraft username can be anything, the Microsoft account has to exist).&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/minecraft-scam/05-verification.png&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;Last step of verification is a 6-digit code from your mail, easy, right?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then you just fill in the code that gets to your email and you are verified!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/minecraft-scam/06-reset.png&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;My “verification code”, or is it?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Wait a second, do you see the code that arrived? This code is not for some verification, it is for a Microsoft account sign-in!&lt;/p&gt;

&lt;h2 id=&quot;this-is-what-the-scam-is-all-about&quot;&gt;This is what the scam is all about&lt;/h2&gt;

&lt;p&gt;So the scam is supposed to get you to join their Discord server through their &lt;em&gt;“verification”&lt;/em&gt; process that will steal your Microsoft account. And because your Minecraft account is tied to your Microsoft account, the scammer will gain full access to both accounts for the price of one password reset.&lt;/p&gt;

&lt;p&gt;As the list of Discord server members is public, we can see how many people were possibly scammed. Unfortunately, there is no clear indication of how many of these are scammers, but given that the server population is currently approx 500 accounts, we can guess that at least some of them are victims.&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;This was the first time I have encountered this type of scam – a scammer posing as an old friend of yours asks you to join their Discord server with a slight catch. To join the Discord server, you have to &lt;em&gt;“verify”&lt;/em&gt; your account which will lead to your Microsoft account being stolen. The main reason for this type of scam is not only the classic one but also to get high-value accounts that can be sold for a considerable amount of money.&lt;/p&gt;

&lt;p&gt;As with many social engineering scams, the scammers are using psychological tactics where they are first pretending to be your friends to incentivise you to do what they want you to do. What is especially sickening is that given the main audience of Minecraft is kids, they may also be the main target of the scammers. They are able to do this with impressive speed as my friend was approached within seconds of joining a new Minecraft server.&lt;/p&gt;

&lt;p&gt;We have of course reported the scammer both to Discord and Cubecraft. At least on Cubercraft, we can confirm that both their accounts were banned. This won’t stop them for long but hopefully will make their day a little worse.&lt;/p&gt;

            </content>
        </item>
        
        
        
        <item>
            <title>2024 Recap: Books &amp; Podcasts</title>
            <description>&lt;p&gt;As the 2024 has come to an end, let’s recap on some of the books I have read this year and what podcasts did I listen to.&lt;/p&gt;

</description>
            <pubDate>Mon, 13 Jan 2025 00:00:00 +0000</pubDate>
            <link>https://blog.ahlava.cz/2025/01/13/books-2024.html</link>
            <guid isPermaLink="true">https://blog.ahlava.cz/2025/01/13/books-2024.html</guid>
            
            <category>life</category>
            
            <category>short</category>
            
            
            <content>
            &lt;p&gt;As the 2024 has come to an end, let’s recap on some of the books I have read this year and what podcasts did I listen to.&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;books&quot;&gt;Books&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.goodreads.com/book/show/75282865-earthside&quot;&gt;Earthside (Quantum Earth, #2)&lt;/a&gt; by &lt;a href=&quot;http://dennisetaylor.org/&quot;&gt;Dennis E. Taylor&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;The best setup and payoff I have seen in a book so far&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.goodreads.com/book/show/159116833-not-till-we-are-lost&quot;&gt;Not Till We Are Lost (Bobiverse #5)&lt;/a&gt; by &lt;a href=&quot;http://dennisetaylor.org/&quot;&gt;Dennis E. Taylor&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;Great as always, although I would rank this as my least favorite Bobiverse book. Compared to others, there were too many story lines with unsatisfying endings&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.goodreads.com/book/show/57007683-the-apollo-murders&quot;&gt;The Apollo Murders (Apollo Murders, #1)&lt;/a&gt; by &lt;a href=&quot;https://chrishadfield.ca/&quot;&gt;Chris Hadfield&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;Appreciated the realism given to it by its author&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.goodreads.com/book/show/13055592-redshirts&quot;&gt;Redshirts&lt;/a&gt; by &lt;a href=&quot;https://whatever.scalzi.com/&quot;&gt;John Scalzi&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;A great fan service with a lot of meta content, at the end I could not get my head around all the layers&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.goodreads.com/book/show/30078567-the-collapsing-empire&quot;&gt;The Collapsing Empire (The Interdependency, #1)&lt;/a&gt; by &lt;a href=&quot;https://whatever.scalzi.com/&quot;&gt;John Scalzi&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;After reading Redshirts, I wanted to check out some more &lt;em&gt;“serious”&lt;/em&gt; book and immediately was hooked on the next one&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.goodreads.com/book/show/18007564-the-martian&quot;&gt;The Martian&lt;/a&gt; and &lt;a href=&quot;https://www.goodreads.com/book/show/34928122-artemis&quot;&gt;Artemis&lt;/a&gt; by &lt;a href=&quot;https://www.galactanet.com/writing.html&quot;&gt;Andy Weir&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;All of them are great and made my laugh out loud, my ranking is &lt;a href=&quot;https://www.goodreads.com/book/show/54493401-project-hail-mary&quot;&gt;Project Hail Mary&lt;/a&gt; &amp;gt; The Martian &amp;gt; Artemis&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;All of the &lt;a href=&quot;https://www.goodreads.com/author/show/4192148.James_S_A_Corey&quot;&gt;Expanse by James S.A. Corey&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;After finishing the TV series, I wanted to know how the story ends. Discovered soon enough that the books are much more funny and light-hearted than their screen counterparts. Had to go back and read all of them.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;podcasts&quot;&gt;Podcasts&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://darknetdiaries.com/&quot;&gt;Darknet Diaries&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;Discovered recently, large backlog with great content and goosebumps-producing stories&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://nebula.tv/thelayover&quot;&gt;The Layover&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;Weekly recap and behind the scenes of what happened during &lt;a href=&quot;https://www.youtube.com/@jetlagthegame&quot;&gt;Jet Lag: The Game&lt;/a&gt; with some entertaining in-between seasons stuff&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.youtube.com/channel/UCmFsowfzkJo3_Zpx5s6sV0g&quot;&gt;Jam Mechanics&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;Bug and TNC are given prompts for which they have 3 hours to create a song from scratch, love the energy between them two&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;songs&quot;&gt;Songs&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://youtu.be/zMKUpMd04QI&quot;&gt;Tongues &amp;amp; Teeth&lt;/a&gt; by &lt;a href=&quot;https://www.thecranewives.com/&quot;&gt;The Crane Wives &lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=AxNjTM7VhJ4&quot;&gt;2 Bed, 2 Bath (and a Ghost)&lt;/a&gt; by &lt;a href=&quot;https://bughunterbug.com/&quot;&gt;Bug Hunter&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

            </content>
        </item>
        
        
        
        <item>
            <title>Stopped self-hosting everything</title>
            <description>&lt;p&gt;Self-hosting everything is fun – at first. After a while and a few days of downtime, cloud starts to seem more shiny than before.
This is why I decided to move from self-hosting everything to a more balanced approach.&lt;/p&gt;

</description>
            <pubDate>Sun, 29 Dec 2024 00:00:00 +0000</pubDate>
            <link>https://blog.ahlava.cz/2024/12/29/moving-to-cloud.html</link>
            <guid isPermaLink="true">https://blog.ahlava.cz/2024/12/29/moving-to-cloud.html</guid>
            
            <category>sysadmin</category>
            
            <category>dev</category>
            
            
            <content>
            &lt;p&gt;Self-hosting everything is fun – at first. After a while and a few days of downtime, cloud starts to seem more shiny than before.
This is why I decided to move from self-hosting everything to a more balanced approach.&lt;/p&gt;

&lt;!--more--&gt;

&lt;p&gt;The big three categories of services that I used to self-host are email, all websites and VPN that connected my home lab with VPSs. As you can probably imagine, there is a different kind of nerves when one of the things enters its downtime. You need to always know that everything is working properly, because if all your mail servers are down for longer period of time, you could miss something important &lt;em&gt;(such as a university assignment, what a horror!)&lt;/em&gt;. My personal breaking point was when I was hit with a two-week outage on one of my VPS due to the provider having storage issues right at the time I was supposed to give a lecture from a website hosted on it. This does not mean that I have stopped self-hosting everything, just that I now better judge what is worth my time and nerves versus what for what I am willing to give a few bucks to someone to deal with the struggle for me.&lt;/p&gt;

&lt;p&gt;For better orientation, I have divided my services into three main categories: Email, VPN and websites. For each of them, I am first going to briefly describe what was my approach in the past and what I am using now:&lt;/p&gt;

&lt;h2 id=&quot;email&quot;&gt;Email&lt;/h2&gt;

&lt;p&gt;The first rule of email is that you always have at least two servers. I had a main Linux VPS with Postfix that taken care of receiving and sending email with IMAP access through Dovecot. Then, I had a secondary Linux VPS also with Postfix, but this secondary VPS just saved everything that it received locally and waited for a cron job from the primary server to take it for ingestion. In the unlikely situation that both my VPSes were down at the same time, I had a tertiary MX record pointing at &lt;a href=&quot;https://www.junkemailfilter.com/spam/free_mx_backup_service.html&quot;&gt;JunkEmailFilter’s MX Backup&lt;/a&gt; service. This service promised to keep all received mails and forward them to the primary email once you sort your outage out. Great stuff! Saved me a couple of times during the years. Even though I have nailed the receiving part of email most of the time, there were still hiccups, e.g. when somebody sent me a large file as an attachment. What is worse is sending your email from your own IP address – nowadays, all major mail providers are using block lists, and they are sending new IP addresses to SPAM folder by default (to combat SPAM, which makes sense). For the several years I was running my self-hosted mail provider, sending mail to @gmail.com address was always a roll of a dice.&lt;/p&gt;

&lt;p&gt;Where have I moved instead? To &lt;a href=&quot;https://proton.me/mail&quot;&gt;Proton mail&lt;/a&gt;. I have purchased their 2-year plan during the Black Friday sale and haven’t regretted it since. I really like Proton because of their privacy focus. Plus, it is easy to add custom domain(s) together with catchall addresses and to set up some basic Sieve-like filters (the only thing missing is filters by mail content, which in the context of Proton makes sense). The only inconvenience is that I need to self-host a Proton mail bridge in order to be able to send and read my mail from Thunderbird.&lt;/p&gt;

&lt;p&gt;However, I operate more domains than my Proton plan would allow for, so I cannot migrate everything over there. For my other domains, I am using two services:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://blog.cloudflare.com/introducing-email-routing/&quot;&gt;Cloudflare email routing&lt;/a&gt; if I only want to receive the mail and not send it&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://purelymail.com/&quot;&gt;Purelymail&lt;/a&gt; for everything else.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Purelymail offers a pricing based on how many mails you have stored or sent, so when I keep forwarding everything to my Proton mail account and only occasionally send a mail, the overall price per year for multiple domains is far less than $10 per year for me. Unbeatable. Except for that, I am often using &lt;a href=&quot;https://duckduckgo.com/email/&quot;&gt;DuckDuckGo email protection&lt;/a&gt; for throwaway accounts or &lt;a href=&quot;https://simplelogin.io/&quot;&gt;SimpleLogin&lt;/a&gt; for ones I want to use more often, but still keep my real address private.&lt;/p&gt;

&lt;h2 id=&quot;vpn&quot;&gt;VPN&lt;/h2&gt;

&lt;p&gt;As I have mentioned in a &lt;a href=&quot;https://blog.adamhlavacek.com/2024/10/05/lan-cloudflare-access&quot;&gt;past article&lt;/a&gt;, there are multiple options how to access applications running on LAN from whole Internet. In the past, I have opted to use a setup with one central VPS that had a public IP address. This VPS had also running WireGuard, to which all my LAN-only servers were connected. Then, by using HAProxy on the VPS, I have decided which domain should be forwarded where. A second layer is that I want my LAN to be accessible from my parent’s LAN and vice versa. Because I am running MikroTik routers everywhere, I have again used the WireGuard on the central VPS to connect all the networks together. Then, when the routers could see each other, I have set up a second WireGuard layer just between them and created relevant forwarding rules for IP ranges. As you can probably already see, the main problem is the VPS being a single point of failure and often also a bottleneck because of it limited 100 Mbit/s bandwidth.&lt;/p&gt;

&lt;p&gt;Instead, I have mainly migrated to three different solutions:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;web pages &lt;a href=&quot;#web-apps&quot;&gt;see below&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;instead of connecting routers through central VPS with WireGuard, I use &lt;a href=&quot;https://www.zerotier.com/&quot;&gt;ZeroTier&lt;/a&gt; which has a package available for ARM-based MikroTik routers. After the routers can see each other thanks to the ZeroTier, I still use WireGuard to bridge the LAN networks.&lt;/li&gt;
  &lt;li&gt;to connect to LAN from WAN, I have two very cheap VPS dedicated only to do port forwarding to WireGuard on my local MikroTik router&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I opted not to use the ZeroTier directly for connecting networks because I did not want ZeroTier to be able to see all traffic that goes between my LANs. Instead, the additional WireGuard layer provides  &lt;a href=&quot;https://www.wireguard.com/protocol/&quot;&gt;post-quantum security&lt;/a&gt;, while also giving me full access control on every endpoint. However, if I opted to use the ZeroTier directly, I would not have to purchase the two additional VPSes (one primary and one secondary) to connect to my LAN from WAN, I would only need the ZeroTier application.&lt;/p&gt;

&lt;h2 id=&quot;web-apps&quot;&gt;Web apps&lt;/h2&gt;

&lt;p&gt;Static pages, such as this blog, are all hosted through &lt;a href=&quot;https://pages.cloudflare.com/&quot;&gt;Cloudflare Pages&lt;/a&gt;. Because the pages can be build through CI/CD directly from GitHub repositories, all I have to provide is the command to build the pages, set the alias and then forget about it. This reduces load on my servers while increasing uptime (no single point of failure on my side!).&lt;/p&gt;

&lt;p&gt;Dynamic pages, such as APIs and alike, are running on my VPSes or local servers, which have &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cloudflared&lt;/code&gt; installed. Then, I route relevant subdomains to relevant ports of docker containers on the target machines. This gives me the advantage of not having to configure reverse proxies and also automatically generates TLS certificates for me. Furthermore, I can secure my applications behind SSO through Google or Microsoft or similar if I want to prevent whole Internet reaching my application without proper authentication.&lt;/p&gt;

&lt;p&gt;Another option that I use is to use the Cloudflare Access login as a source of truth which I use within &lt;a href=&quot;https://github.com/esoadamo/cloudflare-oidc-proxy&quot;&gt;my project&lt;/a&gt; as an OIDC identity provider, so you can log-in with your Google/Microsoft/w/e account into your locally hosted Nextcloud or Gitea.&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;In short, even though I still fully support anybody who wants to try to self-host anything and everything, I have decided to move away from it. It was a great learning exercise and gave me much understanding of the inner workings of many of the applications, but in the end it because almost a full-time job to keep everything running and caused problems when the bottlenecks hit. Now, with my mixed solution, I can sleep much better because I know that even when some parts of my network are offline, some services are still running and a good chance is that I won’t need to fix them first thing in the morning.&lt;/p&gt;

&lt;h2 id=&quot;dns-bonus&quot;&gt;DNS Bonus&lt;/h2&gt;

&lt;p&gt;When you are on your LAN you may want to use the LAN-only IP addresses of your servers for faster access, or if you just want to simply block &lt;a href=&quot;https://github.com/PeterDaveHello/threat-hostlist&quot;&gt;known malicious domains&lt;/a&gt;, you may want to overwrite local DNS records. In the past, I have used &lt;a href=&quot;https://github.com/DNSCrypt/dnscrypt-proxy/&quot;&gt;dnscrypt-proxy&lt;/a&gt; as a self-hosted DNS server. Now, I have migrated to setting custom DNS rules directly inside my MikroTik routers. As this task needs to be applied periodically for multiple routers, I have created a simple script that is able to take a &lt;a href=&quot;https://en.wikipedia.org/wiki/Hosts_(file)&quot;&gt;hosts file&lt;/a&gt; and set the same records inside the MikroTik routers:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ipaddress&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pathlib&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Path&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sys&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;argv&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;re&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sub&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;is_valid_ipv4_address&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ip_string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;bool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;ipaddress&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;IPv4Address&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ip_string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;except&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ipaddress&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;AddressValueError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;conver_hosts_line&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;line&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;line&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;line&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&quot;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;line&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;sub&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;\s+&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; &lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;line&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;line&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;line&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; &lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;domain&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;line&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; &lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;except&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;ValueError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;matching_rule&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;name=&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;domain&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&apos;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;domain&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;domain&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;domain&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;\\.&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;.*&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;matching_rule&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;regex=&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;domain&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&apos;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;is_valid_ipv4_address&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;/ip/dns/static/add type=A &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;matching_rule&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; address=&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; comment=&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;autogen&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; ttl=30&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;/ip/dns/static/add type=CNAME &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;matching_rule&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; cname=&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; comment=&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;autogen&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; ttl=30&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;file_in&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;argv&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;file_in&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;exists&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;/ip dns static remove [find comment=&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;autogen&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;file_in&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;line&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;line&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;conver_hosts_line&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;line&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;line&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;continue&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;line&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nf&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then you can simply take your &lt;a href=&quot;https://en.wikipedia.org/wiki/Hosts_(file)&quot;&gt;hosts file&lt;/a&gt; and use the script in bash: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;./hosts-to-routeros.py hosts.txt | ssh -T mikrotik-router&lt;/code&gt;. It will remove all previously autogenerated rules and replace them with the new content. An example of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hosts.txt&lt;/code&gt; file may be:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;immich.example.com        10.0.0.2
my-server                 10.0.0.3
nextcloud.example.com     my-server
*.example.org             my-server
homeassistant.example.com external.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;As you can see, it supports both wildcards and aliases. Furthermore, you can then use &lt;a href=&quot;https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/&quot;&gt;DoH&lt;/a&gt; inside your MikroTik router for more private resolving or use some WireGuard-based VPN with in-build malware and adblock such as &lt;a href=&quot;https://protonvpn.com/support/netshield/&quot;&gt;ProtonVPN&lt;/a&gt; as your DNS server.&lt;/p&gt;

            </content>
        </item>
        
        
        
        <item>
            <title>Cloudflare Access to get to LAN</title>
            <description>&lt;p&gt;Self-hosting applications at one’s home provide benefits over using their cloud-native alternatives. These benefits may include privacy (your data is stored in your hardware vs in the cloud with the second party) and overall cost (effectively paying only for electricity and hardware up-front vs paying for a subscription tier every month/annually). On the other hand, as its most prominent disadvantage, self-hosted applications at one’s home are usually only available from inside the local area network. In contrast, cloud-native applications can be accessed from anywhere with an Internet connection. This text will focus on solutions tailored for SOHO (small-office and home-office) settings.&lt;/p&gt;

</description>
            <pubDate>Sat, 05 Oct 2024 00:00:00 +0000</pubDate>
            <link>https://blog.ahlava.cz/2024/10/05/lan-cloudflare-access.html</link>
            <guid isPermaLink="true">https://blog.ahlava.cz/2024/10/05/lan-cloudflare-access.html</guid>
            
            <category>sysadmin</category>
            
            
            <content>
            &lt;p&gt;Self-hosting applications at one’s home provide benefits over using their cloud-native alternatives. These benefits may include privacy (your data is stored in your hardware vs in the cloud with the second party) and overall cost (effectively paying only for electricity and hardware up-front vs paying for a subscription tier every month/annually). On the other hand, as its most prominent disadvantage, self-hosted applications at one’s home are usually only available from inside the local area network. In contrast, cloud-native applications can be accessed from anywhere with an Internet connection. This text will focus on solutions tailored for SOHO (small-office and home-office) settings.&lt;/p&gt;

&lt;!--more--&gt;

&lt;p&gt;&lt;em&gt;Note: this post was originally submitted as an university subject essay, so that it uses a little more formal language than usual.&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;classical-solutions&quot;&gt;Classical solutions&lt;/h2&gt;

&lt;p&gt;To see the advantages of choosing Cloudflare Access for this task, we first need to discuss what other solutions are already present and often used. Multiple options exist for accessing LAN-only applications from the Internet, each with its custom set of up- and downsides. Please see that almost none of the classical solutions listed below discusses the problematics of IPv4 and IPv6 running simultaneously, as most of the approaches allow for only one at a time. Furthermore, no approach deals with modern security practices such as Zero Trust&lt;a href=&quot;https://www.cloudflare.com/learning/security/glossary/what-is-zero-trust/&quot;&gt;[s]&lt;/a&gt; in any way by itself.&lt;/p&gt;

&lt;h3 id=&quot;public-ip-address&quot;&gt;Public IP address&lt;/h3&gt;

&lt;p&gt;With some of the largest Czech ISPs (internet service providers), it is possible to obtain a public IP address&lt;a href=&quot;https://www.o2.cz/osobni/internet/pevna-ip-adresa-pro-internet-na-doma&quot;&gt;[s]&lt;/a&gt;&lt;a href=&quot;https://www.vodafone.cz/pece/internet-data/internet-v-pocitaci/pevna-ip-adresa/&quot;&gt;[s]&lt;/a&gt; that directs all traffic directly to a router in possession of the SOHO administrator. The IP address may be provided as a benefit from an ISP for a fixed price or, much more commonly, for a monthly price. This solution offers the most significant freedom for the administrator, as they can tailor the network stack to their liking by simply setting up port forwardings on their ingress router. On the other hand, this solution may be disadvantageous in terms of security (the administrator is fully responsible for setting correct access controls)  and flexibility (if the administrator does use some form of centralized configuration, configuring the rules properly may steadily become infeasible within more complex settings).&lt;/p&gt;

&lt;h3 id=&quot;vpn-service-with-exposed-ports&quot;&gt;VPN service with exposed ports&lt;/h3&gt;

&lt;p&gt;Some VPN providers like ProtonVPN&lt;a href=&quot;https://protonvpn.com/support/port-forwarding/&quot;&gt;[s]&lt;/a&gt; and ExpressVPN&lt;a href=&quot;https://www.expressvpn.com/support/knowledge-hub/router-app-port-forwarding/&quot;&gt;[s]&lt;/a&gt; provide an option to forward a single (TCP/UDP) port from the VPN’s public IP address to a  local machine or specific LAN address. The main advantage of this approach is the ease of setup and little technical knowledge barrier compared to other solutions when the administrator can install the VPN provider’s official GUI application and enable port forwarding. The drawbacks are evident; with only a single port, the option to host multiple applications is severely limited unless the administrator decides to use additional local solutions such as a &lt;a href=&quot;/assets/blog/lan-cloudflare-access/https://www.haproxy.org/&quot;&gt;reverse proxy&lt;/a&gt; for multiple HTTP-based applications or a &lt;a href=&quot;/assets/blog/lan-cloudflare-access/https://github.com/yrutschle/sslh&quot;&gt;protocol multiplexer&lt;/a&gt; for a specialized list of supported applications. One possibility to overcome this obstacle may be to host a VPN server on the exposed port, allowing client devices to access the LAN upon connection. Nevertheless, the port number and IP address provided by the VPN service may change, which may require additional reconfiguration on client devices.&lt;/p&gt;

&lt;h3 id=&quot;creating-a-custom-tunnel-with-vps&quot;&gt;Creating a custom tunnel with VPS&lt;/h3&gt;

&lt;p&gt;Another approach may be purchasing a cheap VPS (virtual private server), typically with a public IP address. Then, it is possible to set up a custom VPN server that the services on LAN will be connected to or directly use the remote port forwarding SSH option to forward the traffic from VPS to the self-hosted application. Because the VPS acts as a proxy, this approach may be suitable for both IPv4 and IPv6, even when the ISP is IPv4-only or IPv6-only. On the other hand, using a VPS as a proxy adds a new layer of complexity – the response time is increased by the time that it takes to forward the request from VPS to LAN and back, and the connection may fail, e.g. when the VPS reboots because of applied updates. It also places additional requirements onto the administrator, who now needs to properly configure the VPS so as not to allow unauthorized access, which can be an obstacle for administrators without experience with managing remote servers.&lt;/p&gt;

&lt;h3 id=&quot;using-a-dedicated-p2p-software&quot;&gt;Using a dedicated P2P software&lt;/h3&gt;

&lt;p&gt;An option that relies solely on the clients talking directly to each other, usually with the help of nodes that act as middlemen exchanging information about connected clients to each other. Examples of such software may be &lt;a href=&quot;/assets/blog/lan-cloudflare-access/https://www.vpn.net/&quot;&gt;LogMeIn Hamachi&lt;/a&gt;, &lt;a href=&quot;/assets/blog/lan-cloudflare-access/https://github.com/ntop/n2n/releases&quot;&gt;n2n&lt;/a&gt; and &lt;a href=&quot;/assets/blog/lan-cloudflare-access/https://github.com/hackerschoice/gsocket/&quot;&gt;gsocket&lt;/a&gt;. Most prominently, the Hamachi targets regular users, requiring little to no technical knowledge. This makes the entry barrier almost nonexistent. On the other hand, it requires special software to be installed on both endpoints and because of its P2P nature, clients may be blocked on more restricted networks like airports or hotel lobbies.&lt;/p&gt;

&lt;h2 id=&quot;cloudflare-access&quot;&gt;Cloudflare Access&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;/assets/blog/lan-cloudflare-access/https://one.dash.cloudflare.com/&quot;&gt;Cloudflare Access&lt;/a&gt; is primarily a web-application product from a company with the same name. Cloudflare is perhaps best known for its word-wide CDN (content delivery network), which offers both hosting static pages and acting as a reverse proxy where the inbound connections are accepted to the Cloudflare network and then internally forwarded to a real server, which will then process the request. The main benefits of using Cloudflare as a reverse proxy are the hiding locations of the real servers, which is helpful in case of DDOS attacks, which Cloudflare can filter out.&lt;/p&gt;

&lt;p&gt;Generally, Cloudflare Access masks the actual IP address of the server by overwriting responses to DNS queries with addresses from their IP range.&lt;/p&gt;

&lt;h3 id=&quot;cloudflare-access-application-types&quot;&gt;Cloudflare Access Application types&lt;/h3&gt;

&lt;p&gt;Cloudflare Access supports three main application types.&lt;/p&gt;
&lt;h4 id=&quot;saas-software-as-a-service&quot;&gt;SaaS (Software as a Service)&lt;/h4&gt;

&lt;p&gt;Cloudflare Acess supports integration with SaaS&lt;a href=&quot;https://developers.cloudflare.com/cloudflare-one/applications/configure-apps/saas-apps/&quot;&gt;[s]&lt;/a&gt; directly, but this solution is more focused on enterprise applications than SoHo, so I will consider it out-of-scope for this project. However, it has a significant advantage for some self-hosted applications in the form of SAML (Security Assert Markup Language&lt;a href=&quot;https://developers.cloudflare.com/cloudflare-one/identity/authorization-cookie/&quot;&gt;[s]&lt;/a&gt;) provider.&lt;/p&gt;

&lt;h3 id=&quot;private-network&quot;&gt;Private network&lt;/h3&gt;

&lt;p&gt;With this application type, the administrator can specify a private IP that should be exposed through Cloudflare private network. Because access to this network always requires additional software installed on endpoint devices, I consider this out-of-scope for this project.&lt;/p&gt;

&lt;h4 id=&quot;self-hosted&quot;&gt;Self-hosted&lt;/h4&gt;

&lt;p&gt;In the self-hosted application type, the administrator is supposed to enter which subdomains (and optionally paths) should be redirected to the LAN. Setting up this application is the basis for further creation of processes, like access policy and the actual tunnels to the LAN.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/lan-cloudflare-access/cfa-selfhosted.png&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;Example self-hosted application configuration&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;cloudflare-zero-trust&quot;&gt;Cloudflare Zero Trust&lt;/h2&gt;

&lt;p&gt;Zero trust&lt;a href=&quot;https://www.cloudflare.com/learning/security/glossary/what-is-zero-trust/&quot;&gt;[s]&lt;/a&gt; is a design philosophy in which a user is required to authenticate before accessing resources (e.g., web application), which is mainly in contrast to a more classical approach where a user is granted access to an application based on e. g. being in the correct network. With zero trust implemented, even if the attacker can get access to the network (for example, by compromising a remote code vulnerability within an IoT (Internet of Things) device like IP cam &lt;a href=&quot;https://www.cisa.gov/news-events/alerts/2021/09/28/rce-vulnerability-hikvision-cameras-cve-2021-36260&quot;&gt;[s]&lt;/a&gt;), their impact is limited by still missing credentials that are required to access protected resources.&lt;/p&gt;

&lt;p&gt;Whereas generally, when accessing a web application, the Zero Trust is implemented directly as a login system to said web application, the Cloudflare Zero Trust acts as a middleware that will prevent access to the target application until the user complies with the set access policy. Because we cannot expect the average hobbyist who is a self-hosting administrator to follow the latest security-related news, this approach may be beneficial when a vulnerability is discovered within the target web application because the attacker would still need to satisfy the access policy, which in turn gives more time to the administrator to patch the vulnerability.&lt;/p&gt;

&lt;h3 id=&quot;access-policy&quot;&gt;Access policy&lt;/h3&gt;

&lt;p&gt;As referenced above, access policy&lt;a href=&quot;https://developers.cloudflare.com/cloudflare-one/policies/access/&quot;&gt;[s]&lt;/a&gt; is a set of rules that any party must fulfil to be granted access to the target resource. At least one policy must be configured for each Cloudflare Access application, and each policy needs to be assigned to at least one group of users to which it applies. The policy may be set to approve access or block access. In its most basic form, the access policy allows access to any known user. However, the access policy may contain much more focused controls – e.g., based on the current user’s country, OIDC claim&lt;a href=&quot;https://auth0.com/resources/ebooks/the-openid-connect-handbook&quot;&gt;[s]&lt;/a&gt; and more. Unfortunately, it is impossible to specify controls based on time (e.g., only allowing access to the application during business hours) directly through policy specifications inside the web UI. If the administrator requires such functionality, it must be implemented directly within each application or by modifying an &lt;a href=&quot;/assets/blog/lan-cloudflare-access/https://github.com/esoadamo/cloudflare-oidc-proxy&quot;&gt;OIDC proxy&lt;/a&gt;, as hinted in the Appendix.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/lan-cloudflare-access/cfa-policy.png&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;An example of an allow access policy with group and country selection&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;identity-management&quot;&gt;Identity management&lt;/h3&gt;

&lt;p&gt;We have already touched on users and their groups inside an Access policy chapter. Cloudflare itself does not provide any identity management (with a tiny exception of &lt;a href=&quot;/assets/blog/lan-cloudflare-access/https://developers.cloudflare.com/cloudflare-one/identity/one-time-pin/&quot;&gt;one-time mail password&lt;/a&gt;), while instead relying on external services using the single sign-on principle (SSO, &lt;a href=&quot;https://www.cloudflare.com/learning/access-management/what-is-sso/&quot;&gt;[s]&lt;/a&gt;). The administrator can configure multiple identity sources from predefined providers or specify custom OIDC&lt;a href=&quot;https://auth0.com/resources/ebooks/the-openid-connect-handbook&quot;&gt;[s]&lt;/a&gt; settings. This approach has several advantages – for individuals/SoHo, most users already have their Google or Microsoft accounts. For enterprises, it is possible to reuse, e.g., their already set up Microsoft AD tenant, making the onboarding process effortless for regular users.
Furthermore, Cloudflare provides step-by-step guides on how to set up each provider. Whenever user authenticates to the Cloudflare Zero Trust page with any configured provider for the first time, they are assigned a seat. The free tier has a total of 50 active seats&lt;a href=&quot;https://developers.cloudflare.com/cloudflare-one/identity/users/seat-management/&quot;&gt;[s]&lt;/a&gt;; after a seat is taken, it is possible to free it again.&lt;/p&gt;

&lt;p&gt;For this project, I have tested setting up &lt;a href=&quot;/assets/blog/lan-cloudflare-access/https://developers.cloudflare.com/cloudflare-one/identity/idp-integration/azuread/&quot;&gt;Microsoft Azure AD®&lt;/a&gt; and &lt;a href=&quot;/assets/blog/lan-cloudflare-access/https://developers.cloudflare.com/cloudflare-one/identity/idp-integration/google/&quot;&gt;Google&lt;/a&gt; as identity providers. The initial setup took about 30 minutes, primarily because of my minimal experience with both environments. Both providers offer modern &lt;a href=&quot;/assets/blog/lan-cloudflare-access/https://www.cloudflare.com/learning/access-management/what-is-multi-factor-authentication/&quot;&gt;MFA&lt;/a&gt; solutions for securing target user accounts; their only significant difference is that when setting up the providers with personal accounts, with Microsoft Azure AD I was able to select which user accounts are allowed to authenticate to my AD tenant instance. With Google as an identity provider, any valid user can authenticate. To counter this, I have had to set up a particular group only with selected user seat accounts inside the Cloudflare Zero Trust dashboard. Still, it may be possible for a malicious actor to log in with as many Google accounts as possible. While they are still unable to access the resources because of manual group filtering, they can fill up all seats available with the free tier.&lt;/p&gt;

&lt;h2 id=&quot;cloudflare-tunnels&quot;&gt;Cloudflare Tunnels&lt;/h2&gt;

&lt;p&gt;After setting up the self-hosted application records, the next step is to launch a local tunnel. The tunnel is a server process or docker container that accepts requests from the Cloudflare network and 
forwards them to specified local addresses&lt;a href=&quot;https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/&quot;&gt;[s]&lt;/a&gt;. To initialize a new tunnel on LAN, all the user has to do is to copy a pre-generated command that already contains all required configurations. Afterwards, creating a new forwarding from WAN to LAN is only a matter of selecting a subdomain (same as in configuring a self-hosted application), protocol (by default HTTP, but can be anything TCP-based), and local address with port to which will the tunnel be forwarding remote requests. Optionally, selecting an access policy that must be satisfied before the request can pass through is also possible. The only limitation is that the same subdomain cannot be used for multiple protocols (e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sub.example.org&lt;/code&gt; cannot be set up to forward both HTTP and SSH; another subdomain like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sub-ssh.example.org&lt;/code&gt; must be used instead).&lt;/p&gt;

&lt;p&gt;Because a single tunnel can forward multiple subdomains, adding new forwarding from WAN to LAN takes tens of seconds after the initial setup is finished, which is a significant simplification for the network administrator.&lt;/p&gt;
&lt;h2 id=&quot;preventing-double-logging&quot;&gt;Preventing double-logging&lt;/h2&gt;

&lt;p&gt;After the user authenticates themselves against the Cloudflare Zero trust login page, they may still be required to log into the target web application, which leads to several weak points. Firstly, the user’s comfort is reduced as they must go through another login process right after finishing the previous one. Secondly, either the administrator of the web application needs to set up the same SSO options as for the Cloudflare Zero trust login page, which places additional complexity on the application management, or the users need an additional account just for this web application, which in turn may increase risk of password compromise, as users may not follow security best practices for all their accounts.&lt;/p&gt;

&lt;p&gt;If the administrator could configure the web application to always accept the same login credentials as the Cloudflare Zero Trust page, it would increase the overall user comfort and application security while decreasing administration costs.&lt;/p&gt;
&lt;h3 id=&quot;saml&quot;&gt;SAML&lt;/h3&gt;

&lt;p&gt;Security Assert Markup Language&lt;a href=&quot;https://developers.cloudflare.com/cloudflare-one/identity/authorization-cookie/&quot;&gt;[s]&lt;/a&gt; can be leveraged to provide an SSO&lt;a href=&quot;https://www.cloudflare.com/learning/access-management/what-is-sso/&quot;&gt;[s]&lt;/a&gt;, but this standard is more prevalent for enterprise-oriented applications. For NextCloud, &lt;a href=&quot;/assets/blog/lan-cloudflare-access/https://apps.nextcloud.com/apps/user_saml&quot;&gt;an official application&lt;/a&gt; can be installed, which adds the possibility of using SAML as a backend for user management. As Cloudflare provides SAML endpoints only for SaaS applications&lt;a href=&quot;https://developers.cloudflare.com/cloudflare-one/applications/configure-apps/saas-apps/&quot;&gt;[s]&lt;/a&gt;, we need to create a new virtual SaaS application within the dashboard and then use the provided details to configure SAML within NextCloud correctly.&lt;/p&gt;

&lt;p&gt;Unfortunately, SAML for smaller open-source applications like Gitea is &lt;a href=&quot;/assets/blog/lan-cloudflare-access/https://github.com/go-gitea/gitea/issues/5512&quot;&gt;not supported&lt;/a&gt;, so it is only an option for some of this paper’s target self-hosted applications.&lt;/p&gt;
&lt;h3 id=&quot;cloudflare-access-cookie&quot;&gt;Cloudflare Access cookie&lt;/h3&gt;

&lt;p&gt;Every request sent to the target application behind Cloudflare Zero Trust protection includes a  &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CF_Authorization&lt;/code&gt; cookie [[@cloudflareAuthorizationCookie]]. This cookie contains a signed JSON web token (JWT &lt;a href=&quot;https://www.cloudflare.com/learning/access-management/token-based-authentication/&quot;&gt;[s]&lt;/a&gt;) with the user’s email. If the developers of target applications were to implement authentication using this cookie, it would be possible to log in directly to the application. Still, this would require developers to implement a non-standard login mechanism. Another approach may be to verify the provided JWT on &lt;a href=&quot;/assets/blog/lan-cloudflare-access/https://github.com/kudelskisecurity/haproxy-cloudflare-jwt-validator&quot;&gt;a reverse proxy&lt;/a&gt; and set a header with a user name, which is supported, for example, on Gitea&lt;a href=&quot;https://docs.gitea.com/features/authentication\#reverse-proxy&quot;&gt;[s]&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The main drawback of using the cookie for authentication is that it requires the user to always log in through WAN through Cloudflare Zero Trust, even when the user may prefer to access the application through LAN for considerably faster response speeds (e.g. by using DNS masking).&lt;/p&gt;
&lt;h2 id=&quot;accessing-ssh&quot;&gt;Accessing SSH&lt;/h2&gt;

&lt;p&gt;So far, we have discussed only accessing HTTP-based applications, but Cloudflare tunnels support any TCP-based protocol, with SSH being explicitly stated. If the user only wants to access the LAN-only SSH server from WAN, creating a new record in the Cloudflare tunnel, which will forward all requests, is possible.&lt;/p&gt;

&lt;p&gt;Though, if the user would also want to protect the SSH server with Zero Trust (e.g. requiring logging in with MFA-protected Microsoft Account), they first need to install a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cloudflared&lt;/code&gt; client on their device&lt;a href=&quot;https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/use-cases/ssh/\#native-terminal&quot;&gt;[s]&lt;/a&gt;. Then, by modifying their local SSH config to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cloudflared&lt;/code&gt; as a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ProxyCommand&lt;/code&gt;, it will first launch the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cloudflared&lt;/code&gt; command whenever they try to assess the SSH server. The command opens a web browser with a Cloudflare Zero Trust page, where users can log in with their accounts. After a successful authentication, an SSH connection proceeds as usual. Furthermore, thanks to the modification of the user’s local SSH config file, this whole procedure needs to be done only once, making the authentication process after initial setup effortless. However, the apparent disadvantage of this approach is that the user needs to install the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cloudflare&lt;/code&gt; client on every machine from which they plan to access the SSH server.&lt;/p&gt;

&lt;p&gt;To solve this disadvantage, Cloudflare Access offers an option to render the SSH terminal inside the web browser. This setting can be enabled within the Additional Settings section of the self-hosted application. This browser terminal is again protected with Cloudflare Zero Trust, and the rendered terminal supports classical keyboard shortcuts and function keys, making it a reasonably usable alternative. Unfortunately, this approach requires the user to manually input a password or copy a private key unless additional settings like &lt;a href=&quot;/assets/blog/lan-cloudflare-access/https://developers.cloudflare.com/cloudflare-one/identity/users/short-lived-certificates&quot;&gt;short-lived certificates&lt;/a&gt; are used.&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;In conclusion, Cloudflare Access offers a helpful alternative to access LAN-only resources from WAN. The complexity of the solution and the time spent configuring it for the first time is well-balanced, with easy scalability for adding new resources and following security best practices like Zero Trust. The way in which the Cloudflare Zero Trust is implemented may also increase the overall security of the LAN network, e.g., even when an inexperienced administrator exposes a vulnerable application, a malicious actor is still unable to access it. Furthermore, relying almost solely on external identity management services increases user comfort as they can reuse their already existing accounts. Finally, the Cloudflare Zero Trust is free for up to 50 users/seats, which should be plenty for common SOHO requirements.&lt;/p&gt;

            </content>
        </item>
        
        
        
        <item>
            <title>Creating safe WiFi abroad, Vol. 3</title>
            <description>&lt;p&gt;I have an old small laptop lying under my bed, let’s put some &lt;a href=&quot;https://openwrt.org/&quot;&gt;OpenWrt&lt;/a&gt; on it! (TL;DR: failure)&lt;/p&gt;

</description>
            <pubDate>Sat, 01 Jul 2023 00:00:00 +0000</pubDate>
            <link>https://blog.ahlava.cz/2023/07/01/safe-wifi-abroad-vol-3.html</link>
            <guid isPermaLink="true">https://blog.ahlava.cz/2023/07/01/safe-wifi-abroad-vol-3.html</guid>
            
            <category>short</category>
            
            <category>sysadmin</category>
            
            
            <content>
            &lt;p&gt;I have an old small laptop lying under my bed, let’s put some &lt;a href=&quot;https://openwrt.org/&quot;&gt;OpenWrt&lt;/a&gt; on it! (TL;DR: failure)&lt;/p&gt;

&lt;!--more--&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/secure-wifi-abroad-3/setup.jpeg&quot; alt=&quot;Picture of the running setup&quot; /&gt;
&lt;em&gt;Picture of the final setup sent to me&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My family went on a vacation to Croatia while I was finishing exams. The only sensible thing to do was to gear them up with a router that will protect them against all bad actors.&lt;/p&gt;

&lt;h3 id=&quot;setup--installation&quot;&gt;Setup &amp;amp; installation&lt;/h3&gt;

&lt;p&gt;The setup:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;unknown old Packard Bell laptop with Intel Celeron CPU&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.czc.cz/netis-wf2116/205328/produkt&quot;&gt;USB WiFi adapter&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;bootable SD card with &lt;a href=&quot;https://openwrt.org/docs/guide-user/installation/openwrt_x86&quot;&gt;OpenWrt for x86 hardware&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pros:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;the small size of the whole OS (&amp;lt; 200 MB)&lt;/li&gt;
  &lt;li&gt;very fast booting speed (&amp;lt; 20s)&lt;/li&gt;
  &lt;li&gt;easy setup of OpenVPN with kill-switch&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cons:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;no interfaces detected from the beginning (missing drivers)&lt;/li&gt;
  &lt;li&gt;couldn’t get WireGuard routing to work for clients on LAN (maybe with more time)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The overall process of installation was very positive and the only issue I had with the missing drivers was – for Ethernet – solved by downloading the package manually (for my &lt;a href=&quot;https://downloads.openwrt.org/releases/22.03.5/targets/x86/generic/&quot;&gt;generic x86&lt;/a&gt; I had to download the driver from &lt;a href=&quot;https://downloads.openwrt.org/releases/22.03.5/targets/x86/generic/packages/&quot;&gt;here&lt;/a&gt; – beware that the URL ends with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/targets/x86/generic/packages/&lt;/code&gt;, otherwise you are on a wrong platform and the installation will fail). As to what driver to install, I have booted up Fedora Live, which showed me the correct driver name by executing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lspci -vvv&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The wireless was a little more complicated, as I had to install &lt;a href=&quot;https://forum.openwrt.org/t/no-wireless-interface-on-x86-hardware/63286/2&quot;&gt;much more packages&lt;/a&gt;. At least now I could use the web UI to install them, because my Ethernet connection was already working.&lt;/p&gt;

&lt;h2 id=&quot;wireless-trouble&quot;&gt;Wireless trouble&lt;/h2&gt;

&lt;p&gt;My first thought was to transform this laptop into a slave AP. Unfortunately, the on-board Intel card does not support AP mode. Oh well, I want to connect an external USB WiFi adapter anyway.&lt;/p&gt;

&lt;p&gt;My second idea was to create an AP on the external adapter and use the internal card to connect to the host network/WAN. Ultimately, this was the same approach that I used &lt;a href=&quot;/2022/08/28/safe-wifi-abroad-vol-2.html&quot;&gt;last year&lt;/a&gt;. This has worked, but produced unpredictable bandwidth through-output (1-30 Mbit/s) with a threatening syslog message:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rtlwifi: -----hwsec_cam_bitmap: 0x0 entry_idx=X&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This, unfortunately, is a &lt;a href=&quot;https://forum.openwrt.org/t/wireless-not-reliable-after-upgrade-to-21-02-1/112996&quot;&gt;known issue&lt;/a&gt; with some USB adapters without any sensible solution.&lt;/p&gt;

&lt;h2 id=&quot;real-world-usage-fail&quot;&gt;Real-world usage (fail)&lt;/h2&gt;

&lt;p&gt;Even with all the issues above, I have decided to set up a kill-switch OpenVPN connection and ship it with my family to Croatia. And it failed, miserably. During my testing setup, I have never used more than two connected devices at the same time. But, when everyone connected their devices, the CPU did not make it.&lt;/p&gt;

            </content>
        </item>
        
        
        
        <item>
            <title>Join to start a Minecraft server</title>
            <description>&lt;p&gt;Multiple Minecraft servers are running on my cheap VPS with limited RAM – how to make accessing them as user-friendly as possible?&lt;/p&gt;

</description>
            <pubDate>Sat, 10 Jun 2023 00:00:00 +0000</pubDate>
            <link>https://blog.ahlava.cz/2023/06/10/minecraft-starter.html</link>
            <guid isPermaLink="true">https://blog.ahlava.cz/2023/06/10/minecraft-starter.html</guid>
            
            <category>dev</category>
            
            <category>sysadmin</category>
            
            <category>short</category>
            
            
            <content>
            &lt;p&gt;Multiple Minecraft servers are running on my cheap VPS with limited RAM – how to make accessing them as user-friendly as possible?&lt;/p&gt;

&lt;!--more--&gt;

&lt;p&gt;All of my servers are running almost effortlessly thanks to the amazing &lt;a href=&quot;https://github.com/itzg/docker-minecraft-server&quot;&gt;Docker Minecraft server&lt;/a&gt;. However, in a situation when one of the servers wants to eat at least 6 GiB of memory while the maximal memory available is 8 GiB, all servers can’t be running at the same time. Some of them must be stopped.&lt;/p&gt;

&lt;h2 id=&quot;autostop--autopause&quot;&gt;AutoStop &amp;amp; AutoPause&lt;/h2&gt;

&lt;p&gt;The amazing docker image provides the option to &lt;a href=&quot;https://docker-minecraft-server.readthedocs.io/en/latest/misc/autopause-autostop/autopause/&quot;&gt;pause&lt;/a&gt; (to free up the CPU) or to &lt;a href=&quot;https://docker-minecraft-server.readthedocs.io/en/latest/misc/autopause-autostop/autostop/&quot;&gt;stop&lt;/a&gt; the server. Supposedly, when a pause happens, a knock server is fired up on the same port and listens for an incoming TCP connection. When the TCP connection is established, the Minecraft server is started again. Unfortunately, this did not work for me (the server was always running when I checked the RAM usage) and I did not feel like debugging. I wanted something better anyway.&lt;/p&gt;

&lt;h2 id=&quot;recreating-the-behaviour&quot;&gt;Recreating the behaviour&lt;/h2&gt;

&lt;p&gt;I liked the idea of a knock server starting the server again. For that, I have written a simple bash script that checks if a docker container is running and if not, creates a NetCat server that waits for a connection. When a TCP connection is established, the Minecraft server is started. To shutdown the server again, I have set AutoStop to shut down the server after one hour without any players:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;#!/bin/bash&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Example: the container is named mc_server1 and is using port 25565&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# -&amp;gt; run with ./autostart server1 25565&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-u&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;SERVER&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;PORT&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$2&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Will check for &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$SERVER&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; on port &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$PORT&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;nv&quot;&gt;CONTAINER&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;mc_&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$SERVER&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;while &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
    if &lt;/span&gt;docker ps | fgrep &lt;span class=&quot;nt&quot;&gt;--quiet&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$CONTAINER&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
        &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sleep &lt;/span&gt;10
        &lt;span class=&quot;k&quot;&gt;continue
    fi

    &lt;/span&gt;netcat &lt;span class=&quot;nt&quot;&gt;-lw&lt;/span&gt; 1 0.0.0.0 &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$PORT&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Starting &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$SERVER&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    docker start &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$CONTAINER&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;making-it-user-friendly&quot;&gt;Making it user-friendly&lt;/h2&gt;

&lt;p&gt;This is nice, but the server starts as soon as any player (or &lt;a href=&quot;https://www.reddit.com/r/admincraft/comments/13bxenj/server_scanner_bots_what_to_do/&quot;&gt;bots&lt;/a&gt;) checks if the server is online (does not even have to try to establish a player session). As I am using whitelists on the server, I would want the autostart script to respond only to the whitelisted users. For that to work, I asked my brother (as he has much more Minecraft-related experience than me) to create a script that would:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;listen on the same port as the Minecraft server&lt;/li&gt;
  &lt;li&gt;show a message to the users that the server is currently turned off&lt;/li&gt;
  &lt;li&gt;allow the user to join the server, which will trigger the autostart process&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can download the mock Minecraft server &lt;a href=&quot;/assets/blog/minecraft-autostart/mock-minecraft-server.7z&quot;&gt;here&lt;/a&gt;. The only required change to my autostart script was to replace the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;netcat&lt;/code&gt; with the custom Python script:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;#!/bin/bash&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Example: the container is named mc_server1 and is using port 25565&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# -&amp;gt; run with ./autostart server1 25565&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-u&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;SERVER&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;PORT&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$2&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Will check for &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$SERVER&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; on port &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$PORT&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;nv&quot;&gt;CONTAINER&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;mc_&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$SERVER&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;while &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
    if &lt;/span&gt;docker ps | fgrep &lt;span class=&quot;nt&quot;&gt;--quiet&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$CONTAINER&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
        &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sleep &lt;/span&gt;10
        &lt;span class=&quot;k&quot;&gt;continue
    fi

    &lt;/span&gt;python mock-server.py &lt;span class=&quot;s2&quot;&gt;&quot;/ot/minecraft-autostart/&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$SERVER&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.json&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Starting &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$SERVER&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    docker start &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$CONTAINER&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;what-users-see&quot;&gt;What users see&lt;/h2&gt;

&lt;p&gt;The final workflow is pretty rewarding to see:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/minecraft-autostart/offline.png&quot; alt=&quot;Minecraft server list showing mock server&quot; /&gt;
&lt;em&gt;The server is offline - join to start the server&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/minecraft-autostart/offline-join.jpeg&quot; alt=&quot;A message that the user was kicked from the server&quot; /&gt;
&lt;em&gt;Notification that the server is starting now&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/minecraft-autostart/starting.png&quot; alt=&quot;Minecraft server lists showing the server offline&quot; /&gt;
&lt;em&gt;Wait few moments for the real server to start&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/minecraft-autostart/started.png&quot; alt=&quot;Minecraft server lists showing the server offline&quot; /&gt;
&lt;em&gt;The real server is running&lt;/em&gt;&lt;/p&gt;

            </content>
        </item>
        
        
        
        <item>
            <title>Cat-proofing the Raspberry Pi</title>
            <description>&lt;p&gt;At my girlfriend’s place, I have a RPi 4 server with an old external 1.5TB HDD attached. To protect the hardware from her furry pets. I had to place the server inside a big cardboard box filled with plastic air bubbles and hide the box itself under the nightstand. This setup, as time had shown, was not &lt;em&gt;without flaws&lt;/em&gt;.&lt;/p&gt;

</description>
            <pubDate>Sun, 30 Apr 2023 00:00:00 +0000</pubDate>
            <link>https://blog.ahlava.cz/2023/04/30/catproofing-the-pi.html</link>
            <guid isPermaLink="true">https://blog.ahlava.cz/2023/04/30/catproofing-the-pi.html</guid>
            
            <category>DIY</category>
            
            <category>sysadmin</category>
            
            
            <content>
            &lt;p&gt;At my girlfriend’s place, I have a RPi 4 server with an old external 1.5TB HDD attached. To protect the hardware from her furry pets. I had to place the server inside a big cardboard box filled with plastic air bubbles and hide the box itself under the nightstand. This setup, as time had shown, was not &lt;em&gt;without flaws&lt;/em&gt;.&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;its-hot--loud&quot;&gt;It’s hot &amp;amp; loud&lt;/h2&gt;

&lt;p&gt;First, imagine a computer in a closed tight space with active USB ports. Such instalment inevitably leads to overheating. Overheating can be solved by adding a &lt;a href=&quot;https://rpishop.cz/pasivni/669-hlinikove-chladice-pro-raspberry-pi.html&quot;&gt;passive cooler&lt;/a&gt; and an &lt;a href=&quot;https://rpishop.cz/aktivni/3244-raspberry-pi-4-case-fan-728886755172.html&quot;&gt;official RPi fan&lt;/a&gt;, nice!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/catproofing-rpi/original_case.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;Photo of Raspberry Pi with the official fan installed&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;not-a-fan-of-high-tempeature&quot;&gt;Not a “fan” of high tempeature&lt;/h3&gt;

&lt;p&gt;Oops, the fan is really loud with quite a high pitch, which makes sleeping in the same room almost unbearable. To lower the time with the fan on, let’s write a simple script (…because the RPi is running Ubuntu and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;raspi-settings&lt;/code&gt; is not available here…) that will turn the fan only when the CPU temperature reaches a certain threshold:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;#!/usr/bin/python3
&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;os&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;system&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gpiozero&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LED&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sleep&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;FAN_PIN&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;14&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;FILE_TEMP&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;/sys/class/thermal/thermal_zone0/temp&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;TEMP_MAX&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;75&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;TEMP_COOLDOWN&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;65&lt;/span&gt;

&lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;The fan will start when the tempeature is over &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;TEMP_MAX&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; degrees&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;flush&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;fan&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LED&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;FAN_PIN&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cool&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;FILE_TEMP&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;temp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;//&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1000&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;temp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;TEMP_MAX&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Fan ON, &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;temp&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;flush&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;cool&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;temp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;TEMP_COOLDOWN&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;FAN OFF, &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;temp&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;flush&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;cool&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;fan&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;fan&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;off&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;finally&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;sleep&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This makes the noise problem almost disappear, but still, when the load would get high enough (e. g. when multiple phones would start backing up their photos at the same time), the temperature &lt;em&gt;could&lt;/em&gt; grow over the threshold and start the fan &lt;em&gt;in the middle of the night&lt;/em&gt;. I must note, that the setup was active in this form for a few years and I am &lt;em&gt;truly sorry&lt;/em&gt;.&lt;/p&gt;

&lt;h3 id=&quot;governor&quot;&gt;Governor!&lt;/h3&gt;

&lt;p&gt;I figured out that I need to take matters more closely into my hands. From the &lt;a href=&quot;https://forums.raspberrypi.com/viewtopic.php?p=1293274&amp;amp;sid=1b4be32ae0a99299b50b48c204d391c2#p1293274&quot;&gt;forums&lt;/a&gt;, I was able to deduct that the RPi itself starts to slow down CPU speed, but in, for me unpractically high, temperature. Fortunately, I could switch the CPU governor to manual and force it to run at the lowest possible clock speed:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;#!/usr/bin/python3
&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;os&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;system&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gpiozero&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LED&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sleep&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;FAN_PIN&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;14&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;FILE_TEMP&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;/sys/class/thermal/thermal_zone0/temp&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;TEMP_MAX&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;75&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;TEMP_CPU_SLOWDOWN&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;67&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;TEMP_COOLDOWN&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;65&lt;/span&gt;

&lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;The fan will start when the tempeature is over &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;TEMP_MAX&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; degrees, the CPU will slow down at &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;TEMP_CPU_SLOWDOWN&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; degrees&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;flush&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;fan&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LED&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;FAN_PIN&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nf&quot;&gt;system&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;cpufreq-set -g ondemand&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cool&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;slow&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;FILE_TEMP&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;temp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;//&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1000&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;temp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;TEMP_MAX&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Fan ON, &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;temp&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;flush&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;cool&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;temp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;TEMP_CPU_SLOWDOWN&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;slow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;CPU SLOW, &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;temp&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;flush&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;slow&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;system&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;cpufreq-set -g userspace&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;nf&quot;&gt;system&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;cpufreq-set -rf 600M&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;temp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;TEMP_COOLDOWN&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;FAN OFF, &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;temp&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;flush&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;cool&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;slow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;CPU normal, &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;temp&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;flush&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;nf&quot;&gt;system&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;cpufreq-set -g ondemand&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;slow&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;fan&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;fan&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;off&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;finally&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;nf&quot;&gt;sleep&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Furthermore, I have added &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maxcpus=2&lt;/code&gt; to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/boot/firmware/cmdline.txt&lt;/code&gt; file, so only 2 out of 4 CPU cores were enabled. These two modifications have reduced the frequency of fan starts but still would run if the CPU load stayed high for long enough. This was because the Pi still was placed inside a small box. To resolve this, I would have to place the Pi into a more open space.&lt;/p&gt;

&lt;h3 id=&quot;then-it-clicked&quot;&gt;Then it clicked&lt;/h3&gt;

&lt;p&gt;The RPi was now perfectly quiet, but still, one source of the noise remained – during every write, the HDD would give out a (semi-)loud clicking sound, which was utterly annoying (but still lasted several years, I &lt;em&gt;really&lt;/em&gt; am sorry). First, I tried some software-based solutions:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;set &lt;a href=&quot;https://btrfs.readthedocs.io/en/latest/Administration.html&quot;&gt;BTRF’s commit interval&lt;/a&gt; to 5 minutes, with the idea behind it being that it would write all data only once in a while and would not click that much. As you can imagine, this resulted in a data loss after power loss on more than one occasion&lt;/li&gt;
  &lt;li&gt;use &lt;a href=&quot;https://rclone.org/commands/rclone_mount/&quot;&gt;FUSE-based fs&lt;/a&gt; that would cache the data on the internal SD card and after the specified interval would move the data to the HDD. This option resulted in a considerable performance loss and wore off the internal SD card rather quickly&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Besides, neither of the options solved the initial clicking problem, only delayed it. The only possible solution was to replace the HDD with an SSD. After a bit of research and setting up an RSS notification for &lt;a href=&quot;https://www.reddit.com/r/buildapcsales/?f=flair_name%3A%22SSD%20-%20Sata%22&quot;&gt;/r/buildapcsales&lt;/a&gt;, I have found out that Patriot makes nice and cheap &lt;a href=&quot;https://www.alza.cz/patriot-p210-2tb-d5880377.htm&quot;&gt;2TB discs&lt;/a&gt;. That, combined with &lt;a href=&quot;https://www.aliexpress.com/item/4001022709931.html?spm=a2g0o.order_list.order_list_main.28.71431802ax6Q0j&quot;&gt;a case&lt;/a&gt;, made for a simple and working external SSD.&lt;/p&gt;

&lt;h2 id=&quot;a-new-adversary&quot;&gt;A new adversary&lt;/h2&gt;

&lt;p&gt;My SO’s new addition to the pet family got &lt;a href=&quot;https://youtu.be/SqMCYdqaFCQ?t=24&quot;&gt;bigger and better&lt;/a&gt; &lt;em&gt;(at moving the cardboard box containing the RPi)&lt;/em&gt;, which poses a new threat to the data integrity, as cats are not known for their carefulness with anything they consider a toy.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/catproofing-rpi/lucifer.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;Photo of my new adversary, Lucifer&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Moreover, even when he did not try to move some boxes while nobody was looking, Lucifer still left quite a large load of furr everywhere he went, which did not seem exactly healthy for any electronic devices. Any form of placing the Pi in a freely accessible spot would now equal hazarding its life. I had to find a space open enough so that the air would circulate properly and that the cat won’t be able to reach it.&lt;/p&gt;

&lt;h2 id=&quot;new-setup&quot;&gt;New setup&lt;/h2&gt;

&lt;p&gt;Luckily, my SO has one of the tables with space for cables that was wide enough for the Pi to fit in. Furthermore, because the network in her house is wireless only with some devices running &lt;a href=&quot;https://www.nukib.cz/cs/infoservis/hrozby/1941-aplikace-tiktok-predstavuje-bezpecnostni-hrozbu/&quot;&gt;untrustworthy software&lt;/a&gt;, I saw this as a great opportunity to create a custom wired network where the Pi would act as a router, safe from all untrusted devices, so I hooked the Pi with a cheap 4-port 100 Mbit/s switch.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/catproofing-rpi/first_setup.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;Photo of Pi and other electronics hidden inside a table&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Even though acceptable in theory, the air circulation was not good enough to keep the Pi from overheating and, in the end, it made the situation more harmful. Even worse, it turned out that the Pi’s plastic case itself was unable to decrease its temperature quickly enough.&lt;/p&gt;

&lt;p&gt;After a while, I decided to place the Pi between the supports for the shelf above the table. And, because bright red and white would appear too distracting, I have decided to purchase a &lt;a href=&quot;https://www.alza.cz/case-pro-raspberry-pi-4-alu-cerna-d5671223.htm&quot;&gt;new case&lt;/a&gt; made of metal. This could act as a supplementary layer of passive cooling. Even though it hadn’t a dedicated spot to hold the fan, I was able to attach the fan with a few wires through the gaps in the upper part of the case. Then, I positioned the SSD on the opposite end of the shelf and arranged all cables inside the table together with the switch (which does not suffer from overheating).&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/catproofing-rpi/final_setup.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;Photo of Pi and SSD under the shelf&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;final-words&quot;&gt;Final words&lt;/h2&gt;

&lt;p&gt;After everything was finished and I sacrificed the &lt;a href=&quot;https://www.reddit.com/r/raspberry_pi/comments/gp3jhm/raspberry_pi_4s_usb_30_ports_are_causing/&quot;&gt;USB 3 speed in favour of WiFi&lt;/a&gt;, I now have a server setup which provides a safe ethernet network to all devices. Based on logs (and my SO’s experience) there was not a single fan run in a few months (except after a power loss during a thunderstorm). Furthermore, there were no cat-related incidents, because none of the pets is allowed on the table and even if they disregard this rule, they have never paid any attention to the black box attached to a wall.&lt;/p&gt;

            </content>
        </item>
        
        
        
        <item>
            <title>Buying tickets from Ticketmaster</title>
            <description>&lt;p&gt;A few days ago, I wanted to buy a ticket to a concert. A relatively simple task, as you need only to act quickly enough and find your place as soon as the sale opens. Little did I know that since I bought tickets last time, the Ticketmaster introduced some “upgrades”.&lt;/p&gt;

</description>
            <pubDate>Sun, 16 Oct 2022 00:00:00 +0000</pubDate>
            <link>https://blog.ahlava.cz/2022/10/16/buying-tickets-from-ticketmaster.html</link>
            <guid isPermaLink="true">https://blog.ahlava.cz/2022/10/16/buying-tickets-from-ticketmaster.html</guid>
            
            <category>dev</category>
            
            <category>life</category>
            
            
            <content>
            &lt;p&gt;A few days ago, I wanted to buy a ticket to a concert. A relatively simple task, as you need only to act quickly enough and find your place as soon as the sale opens. Little did I know that since I bought tickets last time, the Ticketmaster introduced some “upgrades”.&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;ticketmastercz--at--eu&quot;&gt;Ticketmaster(.cz ≠ .at ≟ .eu)&lt;/h2&gt;

&lt;p&gt;I believe, that I do not need to introduce you to the Ticketmaster, as it has quite a &lt;a href=&quot;https://yewtu.be/watch?v=-_Y7uqqEFnY&quot;&gt;reputation&lt;/a&gt;. The sale was supposed to begin at 10:00, so at about 9:40 I began to look for the event. The link at the artist’s web page took me to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ticketmaster.at&lt;/code&gt; (which makes sense as the event takes place in Austria), where I tried to log in with my Ticketmaster account. When filling the username and password through my browser extension, the login has failed, so I opted for copying the credentials from my backup password manager. This resulted in another fail. To check the validity of saved information, I have tried to log in into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ticketmaster.cz&lt;/code&gt;, which was successful. It would seem that the Ticketmaster does not share the information between its instances in different countries, even if both countries are part of the EU.&lt;/p&gt;

&lt;p&gt;Now, at 9:53, there was no time to research what exactly is happening. Instead, I have created a new account at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ticketmaster.at&lt;/code&gt; using autofill of the same credentials as for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ticketmaster.cz&lt;/code&gt;. After this, I was met with a reassuring message on a white background.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/buying-tickets-from-ticketmaster/init.png&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;“You will be automatically placed in the queue when the sale begins”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The interesting this about this was, that now I was redirected to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ticketmaster.eu&lt;/code&gt;, so the Ticketmaster does share account data between its national instances in the end?&lt;/p&gt;

&lt;h2 id=&quot;smart-queue&quot;&gt;Smart Queue&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;“The Latest Way to Shop for Ticket to Popular Events”&lt;/em&gt;, said &lt;a href=&quot;https://help.ticketmaster.com/s/article/queue?language=en_US&quot;&gt;Ticketmaster&lt;/a&gt;. According to them, it is a form of ticket bots prevention. Very well, I am intrigued how this will go. The 10:00 strikes about … now!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/buying-tickets-from-ticketmaster/queue.png&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;Queue with more than 25 thousands people before me about 20 minutes after the start&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;As soon as the clock hit the 10:00 mark, the page refreshed and showed me my place in the smart queue - I was nearly 30 thousandth person to be waiting for the tickets. This will take a while. Why not use the time to learn something more about the new queuing system? Let’s see what Ticketmaster recommends me &lt;em&gt;“For a smoother shopping experience…”&lt;/em&gt;:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;em&gt;“Sign in to your Ticketmaster account at least 10 minutes in advance of joining the Waiting Room. This will speed up your purchase later.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;“Confirm you have a valid form of payment in your account with current email and billing information. This will make checkout a breeze.”&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;“If you need to step away, turn up the volume on your device so when it’s your turn, you hear the Queue notification bell.”&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because I created my Austria-based Ticketmaster account only about 7 minutes before the sale has started, this may explain why I received such a high queue position. Well, nothing I can do about it now – but I can prepare for the second point by filling in all my info (which I have already filled in my Czech-based account).&lt;/p&gt;

&lt;h3 id=&quot;smart-queue-with-notifications&quot;&gt;Smart Queue with notifications&lt;/h3&gt;

&lt;p&gt;It had seemed that the queue will take a few hours, and I certainly did not intend to stare at the screen all that time. But leaving would mean that I may miss a crucial time when to return to the keyboard. If only there was some way to notify me when only a certain number of people is left before me…&lt;/p&gt;

&lt;p&gt;The easiest way would surely be to inject a script through a developer tools, which would parse the number of people left and call a HTTP endpoint. But I did not intend to risk being detected as the bot by merely opening the browser’s developer tools, let alone by injecting a new script into the page.&lt;/p&gt;

&lt;p&gt;I had to opt for something less invasive – something that would read the text from the screen for me. As it turned out, there exists a package for an OCR software called &lt;a href=&quot;https://manpages.org/tesseract&quot;&gt;tesseract&lt;/a&gt;, which takes a single image as an input and a single text file as output. Furthermore, because the number would (hopefully) not grow and consequently nor would the size of the text, I could be able to manually record the dimensions and coordinates of the text for cropping unnecessary information out of the picture.&lt;/p&gt;

&lt;p&gt;After cropping, what I was left with was a black text on a white background, which almost seemed like it was made with people using OCR on screenshotted web browser in mind! After a bit of fiddling with Bash, I came up with a script that will:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;send me periodical updates twice a minute&lt;/li&gt;
  &lt;li&gt;send me a priority message once less than 500 people is in front of me&lt;/li&gt;
&lt;/ol&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;#!/bin/bash&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-Eexuo&lt;/span&gt; pipefail

&lt;span class=&quot;nb&quot;&gt;trap&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;while true; do bash -c &apos;notif --important FAIL&apos; &amp;amp;&amp;amp; break; sleep 2; done&quot;&lt;/span&gt; ERR

&lt;span class=&quot;nv&quot;&gt;wd&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mktemp&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$wd&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;last.txt

&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;while &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
        &lt;/span&gt;gnome-screenshot &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; screen.png
        convert screen.png &lt;span class=&quot;nt&quot;&gt;-crop&lt;/span&gt; 420x38+2090+568 screen.crop.png
        tesseract &lt;span class=&quot;nt&quot;&gt;-l&lt;/span&gt; eng screen.crop.png screen.txt
        &lt;span class=&quot;nv&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;egrep &lt;span class=&quot;s1&quot;&gt;&apos;ahead of you: ([0-9]+)&apos;&lt;/span&gt; screen.txt.txt | &lt;span class=&quot;nb&quot;&gt;sed&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-Ee&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;s/^.*:\s([0-9]+).*?$/\1/&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;lastCount&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cat &lt;/span&gt;last.txt&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$count&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$lastCount&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
            if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$count&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-lt&lt;/span&gt; 500 &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
                &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;important&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;--important&quot;&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;else
                &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;important&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;fi
            &lt;/span&gt;notif &lt;span class=&quot;nv&quot;&gt;$important&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Queue: &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$count&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
            &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$count&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; last.txt
        &lt;span class=&quot;k&quot;&gt;fi
        &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sleep &lt;/span&gt;35 &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;break
    &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;done&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;true

rm&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$wd&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And how did it turned out? I think pretty great, as evidenced by my message history.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/buying-tickets-from-ticketmaster/result.png&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;Messages sent by the notification script&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;here-comes-the-bell&quot;&gt;Here comes the bell&lt;/h2&gt;

&lt;p&gt;After about 2 and half hours of waiting, the time to act was now. It was my turn to buy the tickets – all I had to do was to select how many of what type of tickets I wanted, press the “Search for tickets” button and … &lt;em&gt;“There was an error while processing your request”&lt;/em&gt;. Wait, what? I have waited more than 150 minutes just to fail? What about a single ticket of any other category? Also fail. Why? What was happening?&lt;/p&gt;

&lt;p&gt;At this point I had nothing to loose, so I opened the browser developer tools and looked at the network section. Whenever I pressed the button, the response for the subsequent HTTP request would be simply &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;blocked&lt;/code&gt; with a status code of 403. The system things that &lt;a href=&quot;https://help.ticketmaster.ca/s/article/Why-am-I-getting-a-blocked-forbidden-or-403-error-message?language=en_US&quot;&gt;I am a bot&lt;/a&gt;. Why does it think that? I really don’t know, as nothing I have done should differ from a normal user behavior. Maybe because I have registered a new account, as they suggested in their 2nd point, only 5 minutes before the sale has started? By the way – the bell mentioned in the 3rd point never rang.&lt;/p&gt;

&lt;p&gt;Desperately, I have refreshed the page and was greeted with a new place in the queue – more than 40 thousands.&lt;/p&gt;

&lt;h2 id=&quot;do-i-have-the-tickets&quot;&gt;Do I have the tickets?&lt;/h2&gt;

&lt;p&gt;Yes! With a great foresight, my girlfriend has joined the queue at about 10:05 from her iPhone without any prior registration and has successfully completed the whole process. Needless to say, if there was any alternative to the Ticketmaster, I would gladly use it. But with the current state, if I want to get to the event, I have no other option.&lt;/p&gt;

            </content>
        </item>
        
        
        
        <item>
            <title>Saving photos on an unencrypted smartphone SD card</title>
            <description>&lt;p&gt;When I have bought my &lt;a href=&quot;https://wiki.lineageos.org/devices/pioneer/&quot;&gt;Sony Xperia XA2&lt;/a&gt;, my main goal was to buy a phone that can run a &lt;em&gt;(mostly)&lt;/em&gt; open-source OS – &lt;a href=&quot;https://lineageos.org/&quot;&gt;LineageOS&lt;/a&gt;. My last smartphone, the Xiaomi Redmi 4X, has supported a feature in which an SD card could be encrypted and extend the internal phone storage. It has newer occurred to me that this newer phone may not support this. Now I am stuck with my 32 GB of internal storage, where 29 GB is already occupied by the system and apps. Where can I keep my photos?&lt;/p&gt;

&lt;h2 id=&quot;internal-storage&quot;&gt;Internal storage&lt;/h2&gt;

&lt;p&gt;By default, all photos taken with the preinstalled LineageOS camera are saved to a fixed location inside the internal storage, and there is no option to change that behavior. Other applications, like &lt;a href=&quot;https://f-droid.org/en/packages/net.sourceforge.opencamera/&quot;&gt;OpenCamera&lt;/a&gt;, do not work as well with the device’s camera. As stated before, the internal storage is already quite occupied and would not stand a chance against a longer 4K video. I have to store it somewhere else.&lt;/p&gt;

&lt;h2 id=&quot;external-storage&quot;&gt;External storage&lt;/h2&gt;

&lt;p&gt;My phone supports only an unencrypted ex-FAT formatted SD card, which limits my options by a large margin. On my previous phone, I had an option to format the SD card as a part of the encrypted internal storage. Unfortunately, as I have learned perhaps too late, my phone does not support this method. There are many applications on the Google Play Store, that claim to &lt;a href=&quot;https://play.google.com/store/search?q=encrypt%20photos&amp;amp;c=apps&quot;&gt;encrypt and protect&lt;/a&gt; your photos, but I see four main deal-breakers with this approach:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;they are not open-source&lt;/li&gt;
  &lt;li&gt;they use an unspecified encryption algorithm&lt;/li&gt;
  &lt;li&gt;they mostly do not allow you to select where to store the encrypted photos&lt;/li&gt;
  &lt;li&gt;storing photos cannot be automatized&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;droidfs&quot;&gt;DroidFS&lt;/h3&gt;

&lt;p&gt;On the other hand, on the F-Droid, I have found a neat application called &lt;a href=&quot;https://f-droid.org/en/packages/sushi.hardcore.droidfs/&quot;&gt;DroidFS&lt;/a&gt;. This application internally creates a &lt;a href=&quot;https://nuetzlich.net/gocryptfs/&quot;&gt;gocryptfs&lt;/a&gt; volume(s) with a custom location and passphrases (with an optional fingerprint unlock). This application is open source, uses a known encryption algorithm, and lets you select where to store the photos. The only problem is with the automation of the encryption process – if you would want to add photos through an external app, you would have to first mount the gocryptfs volume, which depends on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/dev/fuse&lt;/code&gt;, which is not available without rooting the phone, which is something I do not want to do, as this could break my &lt;a href=&quot;https://developer.android.com/training/safetynet&quot;&gt;SafetyNet&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The only implementation without the requirement for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fuse&lt;/code&gt; device that I have found is &lt;a href=&quot;https://github.com/slackner/gocryptfs-inspect&quot;&gt;gocryptfs-inspect&lt;/a&gt;, but this works only for a decryption of files, not encryption.&lt;/p&gt;

&lt;p&gt;I am certain that it would be possible to create a custom project in Python that would also work for encrypting files, but I would rather find an easier solution.&lt;/p&gt;

&lt;h3 id=&quot;rcx---rclone-for-android&quot;&gt;RCX - Rclone for Android&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://rclone.org/&quot;&gt;Rclone&lt;/a&gt; is an application that I have already mentioned on this blog at least &lt;a href=&quot;/2022/07/01/backup-routeros-config.html&quot;&gt;once&lt;/a&gt;. Its center point are so called &lt;em&gt;“Storage systems”&lt;/em&gt;, which can be a real type of storage – e.g. local drive, SFTP, Google Drive – or a virtual one – e.g. a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;crypt&lt;/code&gt; storage that takes another storage and performs a transparent encryption upon it.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://f-droid.org/en/packages/io.github.x0b.rcx/&quot;&gt;RCX - Rclone for Android&lt;/a&gt; is an open-source Android application that can manage and connect to the rclone &lt;em&gt;“Storage systems”&lt;/em&gt;. Furthermore, it also integrates well with the Android’s &lt;a href=&quot;https://android-doc.github.io/guide/topics/providers/document-provider.html&quot;&gt;SAF&lt;/a&gt;, so that I can access the content from the &lt;a href=&quot;https://www.ghisler.com/android.htm&quot;&gt;Total Commander application&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Even better, when saved into a shared directory on a SD card, I can gain a full control with &lt;a href=&quot;https://termux.dev/en/&quot;&gt;Termux&lt;/a&gt; bash shell! The shared directory on the SD card, to which all applications can both read and write, is located at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/storage/&amp;lt;sd-id&amp;gt;/Android/media/&lt;/code&gt;. With the RCX we can create a new directory – &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/storage/&amp;lt;sd-id&amp;gt;/Android/media/photos.crypt&lt;/code&gt; – and a corresponding &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;crypt&lt;/code&gt; remote. Then we can export the configuration from the RCX and import it into Termux.&lt;/p&gt;

&lt;h2 id=&quot;automating-the-process&quot;&gt;Automating the process&lt;/h2&gt;

&lt;p&gt;Now I have a rclone remote called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Photos&lt;/code&gt; both in RCX and Termux. This remote is a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;crypt&lt;/code&gt; wrapper over a physical &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/storage/&amp;lt;sd-id&amp;gt;/Android/media/photos.crypt&lt;/code&gt; directory. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Photos&lt;/code&gt; remote has disabled filename obfuscation.&lt;/p&gt;

&lt;p&gt;The thing that I do want to do now is for the photos and videos from my internal storage to be encrypted and copied to the SD card, where they will be sorted by their year and month for easier browsing. This task can be achieved with a following script:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;#!/bin/bash&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-euo&lt;/span&gt; pipefail

&lt;span class=&quot;c&quot;&gt;# Set the initial directory to the scripts location&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;dirname&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;realpath&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$0&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# All picures and videos start with IMG_ or VID_, followed by year, month and day&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;^((IMG_)|(VID_))([0-9]{4})([0-9]{2})([0-9]{2})_.*?$&apos;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Where to look for original files&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;directories&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=(&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$HOME&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/storage/pictures/&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$HOME&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/storage/movies/&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Encrypt all files&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;directory &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;directories&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[@]&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Encrypting &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$directory&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$directory&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;f &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
        if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
            continue
        fi
        &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-qE&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$template&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;continue

        &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;... &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
        rclone moveto &lt;span class=&quot;nt&quot;&gt;--progress&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Photos:/&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;done
done&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Move the encrypted files into coorrect sub-directories by year and month&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Moving&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; /storage/sdcard/Android/media/photos.crypt

&lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;f &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
    if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
        continue
    fi
    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-qE&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$template&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;continue

    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;... &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;year&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;sed&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-Ee&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;s/&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$template&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;4/&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;month&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;sed&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-Ee&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;s/&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$template&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;5/&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;day&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;sed&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-Ee&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;s/&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$template&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;6/&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

    &lt;span class=&quot;nv&quot;&gt;directory&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$year&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$month&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$directory&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;mv&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$directory&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/&quot;&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; -&amp;gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$directory&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Optionally, if I would want to synchronize the content of Photos remote with my cloud storage, the process would be fairly easy – I just have to add a new rclone remote and then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sync&lt;/code&gt; them by adding the following lines at the end of the script:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Optionally synchronize with cloud&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;set&lt;/span&gt; +u
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;--sync&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Syncing&quot;&lt;/span&gt;
    rclone &lt;span class=&quot;nb&quot;&gt;sync&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--progress&lt;/span&gt; Photos:/ cloud:/Photos/
&lt;span class=&quot;k&quot;&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;final-words&quot;&gt;Final words&lt;/h2&gt;

&lt;p&gt;I have tried several options on how to securely store photos on an unencrypted SD card. In the end, I have settled for a script that takes care of the encryption for me and also can synchronize my files with my cloud storage. Now I can set up a cron job inside the Termux or use something like Tasker or Automate to have it run in periodical intervals. My internal storage space is saved!&lt;/p&gt;
</description>
            <pubDate>Mon, 12 Sep 2022 00:00:00 +0000</pubDate>
            <link>https://blog.ahlava.cz/2022/09/12/secure-photos-on-mobile.html</link>
            <guid isPermaLink="true">https://blog.ahlava.cz/2022/09/12/secure-photos-on-mobile.html</guid>
            
            <category>sysadmin</category>
            
            
            <content>
            &lt;p&gt;When I have bought my &lt;a href=&quot;https://wiki.lineageos.org/devices/pioneer/&quot;&gt;Sony Xperia XA2&lt;/a&gt;, my main goal was to buy a phone that can run a &lt;em&gt;(mostly)&lt;/em&gt; open-source OS – &lt;a href=&quot;https://lineageos.org/&quot;&gt;LineageOS&lt;/a&gt;. My last smartphone, the Xiaomi Redmi 4X, has supported a feature in which an SD card could be encrypted and extend the internal phone storage. It has newer occurred to me that this newer phone may not support this. Now I am stuck with my 32 GB of internal storage, where 29 GB is already occupied by the system and apps. Where can I keep my photos?&lt;/p&gt;

&lt;h2 id=&quot;internal-storage&quot;&gt;Internal storage&lt;/h2&gt;

&lt;p&gt;By default, all photos taken with the preinstalled LineageOS camera are saved to a fixed location inside the internal storage, and there is no option to change that behavior. Other applications, like &lt;a href=&quot;https://f-droid.org/en/packages/net.sourceforge.opencamera/&quot;&gt;OpenCamera&lt;/a&gt;, do not work as well with the device’s camera. As stated before, the internal storage is already quite occupied and would not stand a chance against a longer 4K video. I have to store it somewhere else.&lt;/p&gt;

&lt;h2 id=&quot;external-storage&quot;&gt;External storage&lt;/h2&gt;

&lt;p&gt;My phone supports only an unencrypted ex-FAT formatted SD card, which limits my options by a large margin. On my previous phone, I had an option to format the SD card as a part of the encrypted internal storage. Unfortunately, as I have learned perhaps too late, my phone does not support this method. There are many applications on the Google Play Store, that claim to &lt;a href=&quot;https://play.google.com/store/search?q=encrypt%20photos&amp;amp;c=apps&quot;&gt;encrypt and protect&lt;/a&gt; your photos, but I see four main deal-breakers with this approach:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;they are not open-source&lt;/li&gt;
  &lt;li&gt;they use an unspecified encryption algorithm&lt;/li&gt;
  &lt;li&gt;they mostly do not allow you to select where to store the encrypted photos&lt;/li&gt;
  &lt;li&gt;storing photos cannot be automatized&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;droidfs&quot;&gt;DroidFS&lt;/h3&gt;

&lt;p&gt;On the other hand, on the F-Droid, I have found a neat application called &lt;a href=&quot;https://f-droid.org/en/packages/sushi.hardcore.droidfs/&quot;&gt;DroidFS&lt;/a&gt;. This application internally creates a &lt;a href=&quot;https://nuetzlich.net/gocryptfs/&quot;&gt;gocryptfs&lt;/a&gt; volume(s) with a custom location and passphrases (with an optional fingerprint unlock). This application is open source, uses a known encryption algorithm, and lets you select where to store the photos. The only problem is with the automation of the encryption process – if you would want to add photos through an external app, you would have to first mount the gocryptfs volume, which depends on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/dev/fuse&lt;/code&gt;, which is not available without rooting the phone, which is something I do not want to do, as this could break my &lt;a href=&quot;https://developer.android.com/training/safetynet&quot;&gt;SafetyNet&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The only implementation without the requirement for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fuse&lt;/code&gt; device that I have found is &lt;a href=&quot;https://github.com/slackner/gocryptfs-inspect&quot;&gt;gocryptfs-inspect&lt;/a&gt;, but this works only for a decryption of files, not encryption.&lt;/p&gt;

&lt;p&gt;I am certain that it would be possible to create a custom project in Python that would also work for encrypting files, but I would rather find an easier solution.&lt;/p&gt;

&lt;h3 id=&quot;rcx---rclone-for-android&quot;&gt;RCX - Rclone for Android&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://rclone.org/&quot;&gt;Rclone&lt;/a&gt; is an application that I have already mentioned on this blog at least &lt;a href=&quot;/2022/07/01/backup-routeros-config.html&quot;&gt;once&lt;/a&gt;. Its center point are so called &lt;em&gt;“Storage systems”&lt;/em&gt;, which can be a real type of storage – e.g. local drive, SFTP, Google Drive – or a virtual one – e.g. a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;crypt&lt;/code&gt; storage that takes another storage and performs a transparent encryption upon it.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://f-droid.org/en/packages/io.github.x0b.rcx/&quot;&gt;RCX - Rclone for Android&lt;/a&gt; is an open-source Android application that can manage and connect to the rclone &lt;em&gt;“Storage systems”&lt;/em&gt;. Furthermore, it also integrates well with the Android’s &lt;a href=&quot;https://android-doc.github.io/guide/topics/providers/document-provider.html&quot;&gt;SAF&lt;/a&gt;, so that I can access the content from the &lt;a href=&quot;https://www.ghisler.com/android.htm&quot;&gt;Total Commander application&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Even better, when saved into a shared directory on a SD card, I can gain a full control with &lt;a href=&quot;https://termux.dev/en/&quot;&gt;Termux&lt;/a&gt; bash shell! The shared directory on the SD card, to which all applications can both read and write, is located at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/storage/&amp;lt;sd-id&amp;gt;/Android/media/&lt;/code&gt;. With the RCX we can create a new directory – &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/storage/&amp;lt;sd-id&amp;gt;/Android/media/photos.crypt&lt;/code&gt; – and a corresponding &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;crypt&lt;/code&gt; remote. Then we can export the configuration from the RCX and import it into Termux.&lt;/p&gt;

&lt;h2 id=&quot;automating-the-process&quot;&gt;Automating the process&lt;/h2&gt;

&lt;p&gt;Now I have a rclone remote called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Photos&lt;/code&gt; both in RCX and Termux. This remote is a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;crypt&lt;/code&gt; wrapper over a physical &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/storage/&amp;lt;sd-id&amp;gt;/Android/media/photos.crypt&lt;/code&gt; directory. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Photos&lt;/code&gt; remote has disabled filename obfuscation.&lt;/p&gt;

&lt;p&gt;The thing that I do want to do now is for the photos and videos from my internal storage to be encrypted and copied to the SD card, where they will be sorted by their year and month for easier browsing. This task can be achieved with a following script:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;#!/bin/bash&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-euo&lt;/span&gt; pipefail

&lt;span class=&quot;c&quot;&gt;# Set the initial directory to the scripts location&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;dirname&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;realpath&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$0&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# All picures and videos start with IMG_ or VID_, followed by year, month and day&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;^((IMG_)|(VID_))([0-9]{4})([0-9]{2})([0-9]{2})_.*?$&apos;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Where to look for original files&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;directories&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=(&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$HOME&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/storage/pictures/&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$HOME&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/storage/movies/&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Encrypt all files&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;directory &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;directories&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[@]&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Encrypting &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$directory&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$directory&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;f &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
        if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
            continue
        fi
        &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-qE&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$template&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;continue

        &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;... &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
        rclone moveto &lt;span class=&quot;nt&quot;&gt;--progress&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Photos:/&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;done
done&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Move the encrypted files into coorrect sub-directories by year and month&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Moving&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; /storage/sdcard/Android/media/photos.crypt

&lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;f &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
    if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
        continue
    fi
    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-qE&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$template&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;continue

    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;... &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;year&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;sed&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-Ee&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;s/&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$template&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;4/&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;month&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;sed&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-Ee&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;s/&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$template&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;5/&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;day&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;sed&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-Ee&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;s/&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$template&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;6/&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

    &lt;span class=&quot;nv&quot;&gt;directory&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$year&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$month&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$directory&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;mv&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$directory&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/&quot;&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; -&amp;gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$directory&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Optionally, if I would want to synchronize the content of Photos remote with my cloud storage, the process would be fairly easy – I just have to add a new rclone remote and then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sync&lt;/code&gt; them by adding the following lines at the end of the script:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Optionally synchronize with cloud&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;set&lt;/span&gt; +u
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;--sync&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Syncing&quot;&lt;/span&gt;
    rclone &lt;span class=&quot;nb&quot;&gt;sync&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--progress&lt;/span&gt; Photos:/ cloud:/Photos/
&lt;span class=&quot;k&quot;&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;final-words&quot;&gt;Final words&lt;/h2&gt;

&lt;p&gt;I have tried several options on how to securely store photos on an unencrypted SD card. In the end, I have settled for a script that takes care of the encryption for me and also can synchronize my files with my cloud storage. Now I can set up a cron job inside the Termux or use something like Tasker or Automate to have it run in periodical intervals. My internal storage space is saved!&lt;/p&gt;

            </content>
        </item>
        
        
        
        <item>
            <title>Creating safe WiFi abroad, Vol. 2</title>
            <description>&lt;p&gt;&lt;a href=&quot;/2022/07/15/safe-wifi-abroad.html&quot;&gt;Last time&lt;/a&gt;, I have mentioned that my first setup could be improved with an &lt;a href=&quot;https://www.czc.cz/netis-wf2116/205328/produkt&quot;&gt;USB WiFi adapter&lt;/a&gt;, that I have forgotten home. So I have gone back to Croatia once again to test my hypothesis in action. How did in turn out?&lt;/p&gt;

</description>
            <pubDate>Sun, 28 Aug 2022 00:00:00 +0000</pubDate>
            <link>https://blog.ahlava.cz/2022/08/28/safe-wifi-abroad-vol-2.html</link>
            <guid isPermaLink="true">https://blog.ahlava.cz/2022/08/28/safe-wifi-abroad-vol-2.html</guid>
            
            <category>sysadmin</category>
            
            
            <content>
            &lt;p&gt;&lt;a href=&quot;/2022/07/15/safe-wifi-abroad.html&quot;&gt;Last time&lt;/a&gt;, I have mentioned that my first setup could be improved with an &lt;a href=&quot;https://www.czc.cz/netis-wf2116/205328/produkt&quot;&gt;USB WiFi adapter&lt;/a&gt;, that I have forgotten home. So I have gone back to Croatia once again to test my hypothesis in action. How did in turn out?&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;the-state-of-apartment-complex-wifi&quot;&gt;The state of apartment complex WiFi&lt;/h2&gt;

&lt;p&gt;This time, the apartment complex had two WiFi APs - let’s call them &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;complex&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;complex_5G&lt;/code&gt;. As you may have already guessed, the second one was the same as the first, only their frequencies had differed. Furthermore, both my RPi Zero and the &lt;a href=&quot;https://www.czc.cz/netis-wf2116/205328/produkt&quot;&gt;USB WiFi adapter&lt;/a&gt; are capable of receiving only 2.4GHz WiFi APs, so we will ignore the 5GHz one for now. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;complex&lt;/code&gt; WiFi had a good signal strength and its DHCP server was working, so we already were in a better situation in comparison to the last time. When measuring the connection speed on the AP, I have been able to get speeds up to 4Mbit/s for both upload and download. Though, during &lt;em&gt;rush&lt;/em&gt; hours, when most of the guests were present in the building, the top speed I have gained was about 0.5Mbit/s for download and 3Mbit/s for upload. Not good, not terrible.&lt;/p&gt;

&lt;h2 id=&quot;setup-1&quot;&gt;Setup #1&lt;/h2&gt;

&lt;p&gt;Directly improving upon the last time, I have prepared a following setup:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;the core of the setup was once again my &lt;a href=&quot;https://rpishop.cz/zero/647-raspberry-pi-zero-w-4053199547425.html&quot;&gt;Raspberry Pi Zero W&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;the RPi was connected to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;complex&lt;/code&gt; AP through an external (and more powerful) &lt;a href=&quot;https://www.czc.cz/netis-wf2116/205328/produkt&quot;&gt;USB WiFi adapter&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;through an &lt;a href=&quot;https://www.czc.cz/premiumcord-usb-2-0-hub-4-portovy-bez-napajeni/254030/produkt&quot;&gt;USB HUB&lt;/a&gt; was also connected an &lt;a href=&quot;https://www.czc.cz/axagon-ade-xr-adapter-usb2-0-na-fast-ethernet-externi/280251/produkt&quot;&gt;USB Ethernet adapter&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;on the other side of the Ethernet Adapter was a &lt;a href=&quot;https://www.czc.cz/tp-link-tl-wr841n_2/75522/produkt&quot;&gt;TP-Link WiFi router&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;the WiFi router has acted as my AP and DHCP server for my network&lt;/li&gt;
  &lt;li&gt;the RPi has accepted all traffic from my AP and routed it through a secure VPN tunnel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/secure-wifi-abroad-2/tp-top.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;The first setup, nicely packed under the night table&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Even though this setup was a direct upgrade upon the last one, when it was used in a real environment, it still shared some flaws with the &lt;a href=&quot;/2022/07/15/safe-wifi-abroad.html#what-to-improve&quot;&gt;original one&lt;/a&gt;. Most notably, the degradation of the TP-Link WiFI router speed has occurred – when measured with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iperf3&lt;/code&gt; from my laptop to the RPi zero, the overall throughput sometimes was only about 0.3 Mbit/s, which is clearly not even close to the advertised speed of &lt;a href=&quot;https://www.czc.cz/tp-link-tl-wr841n_2/75522/produkt&quot;&gt;300 Mbit/s&lt;/a&gt;. It was clear that the WiFi router is the weak link, even when used only as an AP. It had to be removed.&lt;/p&gt;

&lt;h2 id=&quot;setup-2&quot;&gt;Setup #2&lt;/h2&gt;

&lt;h3 id=&quot;hardware&quot;&gt;Hardware&lt;/h3&gt;

&lt;p&gt;A great thing about RPi Zero W is that it has its own WiFi antenna, which can connect to or host an access point (or &lt;a href=&quot;https://docs.raspap.com/ap-sta/#how-does-ap-sta-work&quot;&gt;both at the same time&lt;/a&gt;, but that option is unreliable for real-world usage). Its antenna is fairly small and its signal weak, but since we have rented only one room, it should provide us with enough coverage for all of it. So, in this setup:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;the core of the setup is still &lt;a href=&quot;https://rpishop.cz/zero/647-raspberry-pi-zero-w-4053199547425.html&quot;&gt;Raspberry Pi Zero W&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;the RPi is connected to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;complex&lt;/code&gt; AP through an external (and more powerful) &lt;a href=&quot;https://www.czc.cz/netis-wf2116/205328/produkt&quot;&gt;USB WiFi adapter&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;my AP is transmitted from RPi’s internal antenna&lt;/li&gt;
  &lt;li&gt;the RPi accepts all traffic from my AP and is routing it through a secure VPN tunnel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After the TP-Link router is removed, I can also remove the USB hub and Ethernet adapter, making the night-stand setup even nicer-looking:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/secure-wifi-abroad-2/pi-top.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;The second setup, nicely packed under the night table without bloat&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/secure-wifi-abroad-2/pi-side.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;The second setup from the side with easily distinguishable components&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;software&quot;&gt;Software&lt;/h3&gt;

&lt;p&gt;Of course, hosting the whole AP by myself has some implications – if I do want something more than static IP addressing, I have to run a DHCP server. &lt;em&gt;(And a DNS server, but I can just reuse the already working &lt;a href=&quot;https://github.com/DNSCrypt/dnscrypt-proxy/&quot;&gt;dnscrypt-proxy&lt;/a&gt; from the original setup).&lt;/em&gt; I already have experience with &lt;a href=&quot;https://wiki.debian.org/DHCP_Server&quot;&gt;isc-dhcp-server&lt;/a&gt;, which surely would be a reliable option, but what if there was a better way?&lt;/p&gt;

&lt;p&gt;The answer is &lt;a href=&quot;https://dnsmasq.org/&quot;&gt;dnsmasq&lt;/a&gt; – a combination of both DHCP and DNS server, made for low-end machines (which RPi Zero certainly is). By default, it forwards all DNS requests to the system resolver, but, with a slight change of configuration, I can make it forward all requests to my &lt;a href=&quot;https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/&quot;&gt;DoH&lt;/a&gt; DNS proxy.&lt;/p&gt;

&lt;p&gt;For creating the hotspot itself, I like to use &lt;a href=&quot;https://wiki.gentoo.org/wiki/Hostapd&quot;&gt;hostapd&lt;/a&gt;, which provides a super easy way to create one. Just point it to an interface, add SSID, password, channel &amp;amp; version, and you are good to go.&lt;/p&gt;

&lt;p&gt;Or, let’s go even easier! One could use something like &lt;a href=&quot;https://raspap.com/&quot;&gt;RaspAP&lt;/a&gt;, which handles all these actions by itself. You can just use their single-command install script &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;curl -sL https://install.raspap.com | bash&lt;/code&gt; and afterwards connect to a web control interface. From there, you can adjust your hotspot, DNS and DHCP settings, restart services or access additional features like VPN, ad-blocking and more. Under the hood, RaspAP uses previously mentioned &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hostapd&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dnsmasq&lt;/code&gt;. Just beware, the version of RaspAP that I have used has set the default forward policy to ACCEPT, meaning that everything could be forwarded everywhere, even packets coming from the interface connected directly to the complex WiFi, which is not ideal. To mitigate this security risk, I have used &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iptables&lt;/code&gt; to set the default forward policy to DROP and allowed only forwarding from my AP to the VPN interface (effectively creating a VPN kill-switch for all clients connected to the AP).&lt;/p&gt;

&lt;h3 id=&quot;efficiency&quot;&gt;Efficiency&lt;/h3&gt;

&lt;p&gt;The size of the rented room together with the bathroom and the balcony was about 6x8 meters, so that even the small and weak RPi internal antenna was able to cover the whole area with a good-enough signal. When testing the throughput with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iperf3&lt;/code&gt; command from the most distant place, I have been able to measure stable speeds of about 6Mbit/s, which is not great, but given that the complex’s AP provided me with the maximum speed of 4Mbit/s, it is good enough.&lt;/p&gt;

&lt;h4 id=&quot;getting-more-speed&quot;&gt;Getting more speed&lt;/h4&gt;

&lt;p&gt;I have figured out that the complex’s AP distributes its available bandwidth evenly between all TCP streams. Fortunately, a few days before, I have written a Node.js application that allows me to split my UDP VPN connection into multiple TCP streams. Even though I have written this application for another purpose (and will possibly write another post about it), it seemed just perfect for this use case, when the single VPN connection gave me less bandwidth than browsing without any VPN tunnel.&lt;/p&gt;

&lt;p&gt;When testing on my multicore smartphone, I was able to gain up to 4 times more bandwidth when using 8 TCP streams, which seemed promising. Unfortunately, when I deployed this Node.js application on the single-core RPi Zero, it took about 60% of the CPU, leaving almost nothing for other services. In the end, the throughput was worse than using a single UDP connection directly to the VPN provider.&lt;/p&gt;

&lt;h2 id=&quot;final-words&quot;&gt;Final words&lt;/h2&gt;

&lt;p&gt;Overall, the second setup was a success – the AP was working reliably for a number of days until we left, there was almost no decrease of my AP bandwidth when compared to the complex’s AP and the traffic of all connected clients was protected from nosy neighbours lurking through the complex’s AP &lt;em&gt;(from UFW logs it was evident that someone has tried to port-scan the RPi)&lt;/em&gt;. Even though this was most probably my last vacation abroad this summer, I may reincarnate this series in the future. Maybe I will purchase a more powerful device? Or the rented room will be too large for the RPi’s internal antenna and I will have to use the TP-Link WiFI router as a signal repeater? Whatever the future brings, only one thing is certain - I will keep you all updated with my latest setup. So let me end this the same way as the original post – with the view at the sea.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/secure-wifi-abroad-2/balcony.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;The view from the room’s balcony&lt;/em&gt;&lt;/p&gt;

            </content>
        </item>
        
        
        
        <item>
            <title>Replacing more than two occurences with Python RegEx</title>
            <description>&lt;p&gt;Recently, I was faced with a fairly easy task – to use Python’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;re.sub&lt;/code&gt; to replace all occurrences of one text with another content. The only thing was that I was able to replace only the first two of them. But why?&lt;/p&gt;

</description>
            <pubDate>Sun, 14 Aug 2022 00:00:00 +0000</pubDate>
            <link>https://blog.ahlava.cz/2022/08/14/python-regex-replace.html</link>
            <guid isPermaLink="true">https://blog.ahlava.cz/2022/08/14/python-regex-replace.html</guid>
            
            <category>short</category>
            
            <category>dev</category>
            
            
            <content>
            &lt;p&gt;Recently, I was faced with a fairly easy task – to use Python’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;re.sub&lt;/code&gt; to replace all occurrences of one text with another content. The only thing was that I was able to replace only the first two of them. But why?&lt;/p&gt;

&lt;!--more--&gt;

&lt;p&gt;Of course, the situation was a little more complex – the project that I was modifying takes a Markdown text as an input and, with the help of &lt;a href=&quot;https://pandoc.org/&quot;&gt;Pandoc&lt;/a&gt;, returns its content as HTML code. After a few hours of debugging the whole transformation process, I have tracked the issue to a single function. Can you spot the mistake?&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;format_custom_tags&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;sh&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;
    Replaces all custom-defined tags as divs
    e.g. &amp;lt;ksi-tip&amp;gt; is replaced with &amp;lt;div class=&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;ksi-custom ksi-tip&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&amp;gt;
    :param source: HTML to adjust
    :return: adjusted HTML
    &lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;tags&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;ksi-tip&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;tag_escaped&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;re&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;escape&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;source&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;re&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;sub&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;fr&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tag_escaped&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;(.*?)&amp;gt;&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;fr&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&amp;lt;div class=&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;ksi-custom &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;\1&amp;gt;&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;re&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;IGNORECASE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;source&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;re&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;sub&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;fr&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tag_escaped&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;re&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;IGNORECASE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;source&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Side note: Why is this function required? During the conversion, I want to replace the occurrences of a custom tag &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;ksi-tip&amp;gt;&lt;/code&gt; replace to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;div class=&quot;ksi-custom ksi-tip&quot;&amp;gt;&lt;/code&gt;. If I did not perform this action, the Pandoc would sometimes take my custom tag as a plain text, which broke the formatting in certain cases. When the tag is specified as a class of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;div&lt;/code&gt;, everything works as expected.&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;solution&quot;&gt;Solution&lt;/h2&gt;

&lt;p&gt;Funnily enough, after search for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Why does Python re.sub replace only two occurences&lt;/code&gt; I have found out that I am not the only one who was loosing mind about this issue - there already was &lt;a href=&quot;https://stackoverflow.com/questions/27026228/re-sub-replaces-only-first-two-instances#27026248&quot;&gt;an exactly same question&lt;/a&gt;. In Python, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;re.IGNORECASE&lt;/code&gt;  equals to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2&lt;/code&gt; and the &lt;a href=&quot;https://docs.python.org/3/library/re.html#re.sub&quot;&gt;fourth parameter&lt;/a&gt; of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;re.sub&lt;/code&gt; is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;count&lt;/code&gt;, not &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;flags&lt;/code&gt;. That’s it. The whole issue was caused by a wrong order of parameters passed to the function, so, in the end, &lt;a href=&quot;https://github.com/fi-ksi/web-backend/commit/58ff788e94141ecc78b9fcff0a14f0cbd4462e1a&quot;&gt;the fix&lt;/a&gt; was quite straightforward.&lt;/p&gt;

            </content>
        </item>
        
        
        
        <item>
            <title>Storing luggage in Prague</title>
            <description>&lt;p&gt;Today, I am going to a concert in Prague. Naturally, while travelling by train, I am taking my laptop so that I can get some work done while traveling. The only problem is that the area, where the concert is going to be, does not allow notebooks, so I have to store it somewhere. The first part of this post is written while traveling to the destination, the second part will be amended after I successfully return home from the concert and regain my laptop.&lt;/p&gt;

</description>
            <pubDate>Mon, 25 Jul 2022 00:00:00 +0000</pubDate>
            <link>https://blog.ahlava.cz/2022/07/25/storing-luggage-in-prague.html</link>
            <guid isPermaLink="true">https://blog.ahlava.cz/2022/07/25/storing-luggage-in-prague.html</guid>
            
            <category>life</category>
            
            
            <content>
            &lt;p&gt;Today, I am going to a concert in Prague. Naturally, while travelling by train, I am taking my laptop so that I can get some work done while traveling. The only problem is that the area, where the concert is going to be, does not allow notebooks, so I have to store it somewhere. The first part of this post is written while traveling to the destination, the second part will be amended after I successfully return home from the concert and regain my laptop.&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;before-prague&quot;&gt;Before Prague&lt;/h2&gt;

&lt;p&gt;Before travelling, I have made sure to look for options where to store my laptop, so that I make sure that I will see it again. In this section, I describe the information I found on the web, but beware that this information (as evidenced by the post-Prague section) was not very accurate.&lt;/p&gt;

&lt;h3 id=&quot;what-do-i-need&quot;&gt;What do I need?&lt;/h3&gt;

&lt;p&gt;The train is going to arrive to Prague at about 12:00, and I am planing on going back at about 4:00. I do not plan to use the laptop while I am not traveling by train, so I want to store it from 12:00 to about 3:00. The best option would be some kind of self-serving storage box, possibly with a PIN set, so that I can access it multiple times throughout the day if I need to store something more. Also, the closer to the train station, the better.&lt;/p&gt;

&lt;h3 id=&quot;where-do-i-store-it&quot;&gt;Where do I store it?&lt;/h3&gt;

&lt;p&gt;When searching for storage options on the Internet, I have come across a few different solutions. Let’s examine them now one by one:&lt;/p&gt;

&lt;h4 id=&quot;travel-box&quot;&gt;Travel Box&lt;/h4&gt;

&lt;p&gt;&lt;a href=&quot;https://www.cubesave.cz/&quot;&gt;CubeSave&lt;/a&gt;/&lt;a href=&quot;http://www.travel-box.cz/&quot;&gt;Travel Box&lt;/a&gt; are self-serving boxes located directly at the main train station. All these boxes are supposed to be protected with a PIN, with options to book the boxes for 6 or 24 hours, while the 24 hours option is supposed to cost 125 CZK for a small box.  This seems like the perfect solution, but it may be unusable due to all boxes being possibly occupied at the moment of arrival.&lt;/p&gt;

&lt;p&gt;This storage service also provides a &lt;a href=&quot;https://play.google.com/store/apps/details?id=cz.green24.android.cubesave&quot;&gt;mobile application&lt;/a&gt;. With this application, the boxes can presumably be reserved after registration.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Side note: when verifying a registration email for the application, the verification link points to &lt;a href=&quot;https://app.cubesave.cz&quot;&gt;app.cubesave.cz&lt;/a&gt;. The login information provided in the app also work on this webpage, but upon login the only thing that is shown is a “permission denied” error, while the app seems to work just fine. Maybe this webpage is only for CubeSave admins and should not be presented to the regular users at all?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/storing-luggage-in-prague/web_cube_app.png&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;Screenshot after logging into &lt;a href=&quot;https://app.cubesave.cz&quot;&gt;app.cubesave.cz&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One thing, that does not encourage my trust in this service, is that when accessing a &lt;a href=&quot;https://www.travel-box.cz/&quot;&gt;secure version of Travel Box&lt;/a&gt;, the user is greeted with an invalid certificate made for the domain &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;*.vshosting.cz&lt;/code&gt;.&lt;/p&gt;

&lt;h4 id=&quot;luggage-storage-prague&quot;&gt;Luggage Storage Prague&lt;/h4&gt;

&lt;p&gt;According to their website, &lt;a href=&quot;https://luggagestorageprague.com/&quot;&gt;Luggage Storage Prague&lt;/a&gt; has an open branch at &lt;a href=&quot;https://www.openstreetmap.org/#map=19/50.08807/14.43352&quot;&gt;Masaryk railway station&lt;/a&gt;, which is about 10 minutes of walk from the main train station. This branch is supposed to be open from 03:00 to 23:45 and contain self-serving boxes. According to the &lt;a href=&quot;https://web.archive.org/web/20210507022740/https://luggagestorageprague.com/&quot;&gt;current prices&lt;/a&gt;, storing a single luggage should cost  175 CZK when stored for more than 3 hours and less than 2 days. Even though this location is a little further away, it still can be my second choice in case that Travel Box will fail me.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/storing-luggage-in-prague/trip_luggage.png&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;Walking route from Masaryk to the main railway station, &lt;a href=&quot;https://www.openstreetmap.org/directions?engine=fossgis_osrm_foot&amp;amp;route=50.0880%2C14.4325%3B50.0835%2C14.4344#map=16/50.0859/14.4334&quot;&gt;OpenStreetMap&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h4 id=&quot;honorable-mentions&quot;&gt;Honorable mentions&lt;/h4&gt;

&lt;p&gt;While looking for the perfect solution, I have come across a few that did not suite my needs, but my serve someone else’s, so I am including them nonetheless.&lt;/p&gt;

&lt;h5 id=&quot;easylocker&quot;&gt;EasyLocker&lt;/h5&gt;

&lt;p&gt;&lt;a href=&quot;https://www.easylocker.cz/&quot;&gt;Self-serving boxes&lt;/a&gt; near the historic center of Prague, but open only from 09:00 to 21:00.&lt;/p&gt;

&lt;h5 id=&quot;české-dráhys-storage&quot;&gt;České Dráhy’s storage&lt;/h5&gt;

&lt;p&gt;&lt;a href=&quot;https://www.prague.eu/cs/objekt/mista/3257/uschovna-zavazadel-praha-hlavni-nadrazi&quot;&gt;Main train station Prague&lt;/a&gt; offers its own storage option, operated manually.  Open from 06:00 to 23:00.&lt;/p&gt;

&lt;h5 id=&quot;mysterious-self-serving-boxes&quot;&gt;Mysterious self-serving boxes&lt;/h5&gt;

&lt;p&gt;As I remember, inside the main train station, apart from the TravelBox’s, there is another set of self-serving boxes. I have used them a few years ago, and I certainly do not remember their opening hours, they are there nevertheless. They are supposed to be located in the &lt;a href=&quot;https://www.openstreetmap.org/search?whereami=1&amp;amp;query=50.08387%2C14.43464#map=19/50.08387/14.43464&quot;&gt;north-most section&lt;/a&gt; of the station, but I fail to find any mention of them on the Internet.&lt;/p&gt;

&lt;h4 id=&quot;backup-option&quot;&gt;Backup option&lt;/h4&gt;

&lt;p&gt;In case everything would fail, I have though about – a little complicated – backup option. In the case that I would fail to find a solution that would meet my needs, I would go to a paper store and purchase a box. Then, I would put my laptop inside the box and send it through &lt;a href=&quot;https://www.zasilkovna.cz/en/sending&quot;&gt;Packeta&lt;/a&gt; to a pick-up point near my home. Sending a packet through Packeta can be done from almost any smaller shop. Typical delivery time of the packet is about 2 days and with a price of 79 CZK it is the cheapest option. Though, the package could possibly get damaged during transit, which is why this is only a backup option.&lt;/p&gt;

&lt;h3 id=&quot;final-pre-prague-words&quot;&gt;Final pre-Prague words&lt;/h3&gt;

&lt;p&gt;With all this preparation, I believe that I will be able to regain my laptop after the concert. I believe that the research I made will be enough for me to see my laptop once gain, yet I am still a little concerned, that something will turn wrong. Wish me luck.&lt;/p&gt;

&lt;h2 id=&quot;post-prague-update&quot;&gt;Post-Prague update&lt;/h2&gt;

&lt;h3 id=&quot;cube-save&quot;&gt;Cube Save&lt;/h3&gt;

&lt;p&gt;Right after arriving at the main railway station in Prague, I wanted to rent one of the Cube Save boxes. On the station, there are two sets of boxes, both right next to the main entrance from the Opletalova street. However, there were two surprises waiting for me – the first set of boxes that I have encountered was out of order.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/storing-luggage-in-prague/cube_out_of_order.jpg&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;The out-of-order notice on the first set of boxes&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This may be a problem, because now there is twice a demand for the second set of boxes, meaning that finding an empty box has become twice as hard. I have checked their mobile app, that indicated, that every box is now occupied. Nevertheless, I wanted to take a look at a working interface on the second set of boxes. Here I have found the second surprise:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/storing-luggage-in-prague/cube_closed.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;Working Cube Save box interface&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The box itself is &lt;em&gt;technically&lt;/em&gt; opened 24/7, but the hall, where the boxes are located, is closed between midnight and 04:00. This information makes Cube Save completely unusable, because my train leaves at 04:12 and if more people will want to check out right after hall’s opening time, I will have a hard time getting on the train in time. With that, let’s move on to the second option.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Side note 1: I have also tried to take a look at the mysterious self-serving boxes, but, due to a large crowd and the hall being closed up until 04:00, I have decided that they are not a worthy option.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Side note 2: I have checked the app again at about 15:15, and it showed at lease one medium and one large boxes available.&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;luggage-storage-prague-1&quot;&gt;Luggage Storage Prague&lt;/h3&gt;

&lt;p&gt;My second option was the &lt;em&gt;“most trusted Luggage Storage in Prague”&lt;/em&gt; (according to &lt;a href=&quot;https://luggagestorageprague.com/&quot;&gt;their About us section&lt;/a&gt;), so I knew I was in good hands. As previously stated, their boxes are not on the main railway station, but rather on the Masaryk railway station, though both stations are withing easily walk-able distance from each another. It is supposed to be open from 03:00 to 23:45, which is roughly exactly what I need. Surely, there won’t be any more surprises…&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/storing-luggage-in-prague/luggage_opening.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;The opening hours as shown on site&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The first surprise right when I have entered the location was that the opening hours differ from the ones listed on their website. They are opening a half an hour later than advertised. Though a little later than I would like, I have decided to continue, because I still could catch the train easily.&lt;/p&gt;

&lt;p&gt;When operating with the interface, I was asked to enter my email address. After returning home, I have found out that the box has sent me a message with my pickup code (which has fallen into my spam folder). The same pickup code was also printed on a receipt that I have received after a payment, so the email is more like a backup option for when you misplace your receipt.&lt;/p&gt;

&lt;p&gt;Still, there is one surprise left – the price. Their web says 170 CZK when storing for more than 3 hours. So what price options did the interface show?&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/storing-luggage-in-prague/luggege_prices.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;Prices for a small box&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Because I needed more than 5 hours of storage time, I would have to opt for a 24 hours storage, which would cost me a total of 249 CZK, roughly 50% more than I have expected and twice as much as the Cube Save prices! Even after the trip, I am still unable to find any indication of this price on their website. After considering the possible issues of executing the backup plan of sending my laptop back home through &lt;a href=&quot;https://www.zasilkovna.cz/en/sending&quot;&gt;Packeta&lt;/a&gt;, I have reluctantly accepted.&lt;/p&gt;

&lt;h3 id=&quot;the-pickup&quot;&gt;The pickup&lt;/h3&gt;

&lt;p&gt;At 03:33 I have entered again the Masaryk railway station and entered my pickup code. The box has asked me if I am only visiting the box or already checking out, which is neat and this feature should be advertised more beforehand. Because I had no intention of returning, I have chosen to check out. The box has opened, and I have seen my laptop once again.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/storing-luggage-in-prague/luggage_box.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;My laptop, umbrella and a new belt at 03:35&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;From the Masaryk railway station I have taken a bus that departed at 03:49 from the station and arrived at 03:51 at the main railway station, to which I have entered at about 03:55. The main hall has already been opened, and no crowd was to be seen near Cube Save boxes, so I probably could safely leave my laptop there.&lt;/p&gt;

&lt;h2 id=&quot;final-words&quot;&gt;Final words&lt;/h2&gt;

&lt;p&gt;Even though I have successfully regained my laptop, after considering all the pitfalls I have endured when trying to find a place to store it and the overall price versus me using the laptop only for about 20 minutes during the whole trip in total, next time I will probably just take a book instead. On the other hand, it was a fun little adventure and I could visit the Masaryk railway station, which looks really nice, so &lt;em&gt;(at least for me)&lt;/em&gt;  it was worth it.&lt;/p&gt;

            </content>
        </item>
        
        
        
        <item>
            <title>Creating safe WiFi abroad, Vol. 1</title>
            <description>&lt;p&gt;After successfully completing my bachelor’s finals, I have set for a vacation near a sea in Croatia. The apartment that I was to stay in has advertised to have WiFi connection, even though some reviews have stated that it is quite unstable. Nonetheless, this seemed like a perfect opportunity to test my secure WiFi AP setup.&lt;/p&gt;

</description>
            <pubDate>Fri, 15 Jul 2022 00:00:00 +0000</pubDate>
            <link>https://blog.ahlava.cz/2022/07/15/safe-wifi-abroad.html</link>
            <guid isPermaLink="true">https://blog.ahlava.cz/2022/07/15/safe-wifi-abroad.html</guid>
            
            <category>sysadmin</category>
            
            
            <content>
            &lt;p&gt;After successfully completing my bachelor’s finals, I have set for a vacation near a sea in Croatia. The apartment that I was to stay in has advertised to have WiFi connection, even though some reviews have stated that it is quite unstable. Nonetheless, this seemed like a perfect opportunity to test my secure WiFi AP setup.&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;the-state-of-apartment-complex-wifi&quot;&gt;The state of apartment complex WiFi&lt;/h2&gt;

&lt;p&gt;The whole apartment complex has two active WiFi AP - &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;place&amp;gt;123&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;place&amp;gt;123-EXT&lt;/code&gt;. My room is in a second floor, where the receiving signal of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;place&amp;gt;123-EXT&lt;/code&gt; is much stronger than the one of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;place&amp;gt;123&lt;/code&gt;. Unfortunately, as it seems, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;place&amp;gt;123-EXT&lt;/code&gt; is only a D-Link repeater for the base network, and no a good one – it has frequently lost connection to the base AP for an extended period of time, which makes it practically unsuitable. I was unable to pinpoint the precise location of the base AP and I suspect that it may be hidden inside the complex owner’s room. The only thing that I can say about it is that it is located on the ground floor and is manufactured by Huawei. As of writing of this post, I was unable to obtain any dynamic IP from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;place&amp;gt;123-EXT&lt;/code&gt; DHCP server for two days and the connection to the base AP is not possible from any of my standard devices, so I am unable to measure the precise network speed. The best measurement I have is the 8Mbit down/4M bit up from the day one of the vacation.&lt;/p&gt;

&lt;h2 id=&quot;secure-wifi-ap-setup&quot;&gt;Secure Wifi AP setup&lt;/h2&gt;

&lt;p&gt;The setup itself consists of the parts - a &lt;a href=&quot;https://www.czc.cz/tp-link-tl-wr841n_2/75522/produkt&quot;&gt;TP-Link WiFi router&lt;/a&gt; and a &lt;a href=&quot;https://www.czc.cz/raspberry-pi-zero-wh/234608/produkt&quot;&gt;Raspberry Pi Zero&lt;/a&gt;.  Fortunately, the TP-Link WiFi router (when placed on the right spot) is strong enough to connect to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;place&amp;gt;123&lt;/code&gt; AP directly, so I can skip the misbehaving repeater. The TP-Link router is also set to work in a WIPS mode – it connects to one AP and simultaneously emits another one. It also acts as an authoritative DHCP server. This DHCP server informs clients to use the IP address of the Raspberry Pi Zero both as a gateway and a DNS server.&lt;/p&gt;

&lt;p&gt;The Raspberry Pi is connected to the TP-Link router with and Ethernet cable and has a static default gateway set as the IP of the TP-Link router. It also accepts all incoming packets and forwards them through an VPN back to my home country, so I can both keep my streaming services running and protect any other guests in the complex from sniffing my traffic. To provide the DNS functionality, an &lt;a href=&quot;https://github.com/DNSCrypt/dnscrypt-proxy/&quot;&gt;dnscrypt-proxy&lt;/a&gt; server is running on the RPi.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/secure-wifi-abroad/setup.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;The working setup with the TP-Link router and RPi zero&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;throughput-of-the-setup&quot;&gt;Throughput of the setup&lt;/h2&gt;

&lt;p&gt;When I have tested this setup at home, I have been able to achieve a stable connection of about 24 Mbit/s, which is good enough for general usage. Somewhat mysteriously, the performance of the TP-Link router has started to degrade from the day one. Noted, it had shown marks of misbehaving in the past, but powering the device on and off has always solved the problem (rebooting it through the configuration interface had never any effect). Right now, when measured with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iperf3&lt;/code&gt; with server on my laptop and client on the Ethernet-connected RPi zero, the results are only around 1.5Mbit/s.&lt;/p&gt;

&lt;h2 id=&quot;what-to-improve&quot;&gt;What to improve&lt;/h2&gt;

&lt;p&gt;Evidently, these results are not enough for most of everyday usage as they are barely sustainable for reading news. So what can I do in the future to improve upon this setup? The most obvious answer is to buy a device that has a &lt;a href=&quot;https://www.tp-link.com/cz/support/download/tl-wr841n/v14/#Firmware&quot;&gt;firmware update since 2020&lt;/a&gt;, but I don’t want to do that – it is still a (mostly) functional device and I believe, that I can find a more elegant solution. I have also tough about flashing another open source firmware, but neither &lt;a href=&quot;https://openwrt.org/toh/hwdata/tp-link/tp-link_tl-wr841n_v14&quot;&gt;OpenWRT&lt;/a&gt; not &lt;a href=&quot;https://forum.dd-wrt.com/phpBB2/viewtopic.php?t=317272&quot;&gt;DD-WRT&lt;/a&gt; seem to support this device, while OpenWRT actively discourages from using it. As a third option, I want to try a setup in which I use my &lt;a href=&quot;https://www.czc.cz/netis-wf2116/205328/produkt&quot;&gt;USB WiFi adapter&lt;/a&gt;, which I have unfortunately forgotten home. It could act as a strong client/AP directly for the RPi.  So, until the next time, all I can do is to enjoy the view and not YouTube clips.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/secure-wifi-abroad/view.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;The view from the apparment balcony&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;post-publish-updates&quot;&gt;Post-publish updates&lt;/h2&gt;

&lt;h3 id=&quot;1-the-setup-is-working-again&quot;&gt;#1 The setup is working again&lt;/h3&gt;

&lt;p&gt;After publishing this post, the TP-Link router has stared to behave properly for an extended period of time. I have no idea what has changed and so I continue to search for any causes.&lt;/p&gt;

&lt;h3 id=&quot;2-why-the-complexs-repeater-does-not-work&quot;&gt;#2 Why the complex’s repeater does not work&lt;/h3&gt;

&lt;p&gt;I have been able to capture the precise model of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;place&amp;gt;123-EXT&lt;/code&gt; AP – the &lt;a href=&quot;https://www.czc.cz/d-link-dap-1330/185554/produkt&quot;&gt;D-Link DAP-1330&lt;/a&gt;. The device shows only one amber indication LED, which (according to the &lt;a href=&quot;https://support.dlink.com/resource/PRODUCTS/DAP-1330/REVA/DAP-1330_REVA_MANUAL_1.00_EN_US.PDF&quot;&gt;manual&lt;/a&gt;) means that it has a very poor connection to the base network. This could provide a hint as to why the extender AP has close-to-nonexistent connectivity to the Internet.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/secure-wifi-abroad/extender-manual.png&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;Manual showing that one amber LED means a very poor connection&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/blog/secure-wifi-abroad/ext-ap.jpeg&quot; alt=&quot;&quot; /&gt;
&lt;em&gt;A photo of the complex’s repeater AP&lt;/em&gt;&lt;/p&gt;

            </content>
        </item>
        
        
        
        <item>
            <title>Backing up the router configuration</title>
            <description>&lt;p&gt;Recently, I have been fond of with Mikrotik routers and their enterprise-grade RouterOS system. There is plenty of possible configuration, which gives me great freedom of choice in how I want my home network to operate. Because setting everything, including VPN, guest network and more may take quite a lot of time, it is a good idea to backup the configuration.&lt;/p&gt;

</description>
            <pubDate>Fri, 01 Jul 2022 00:00:00 +0000</pubDate>
            <link>https://blog.ahlava.cz/2022/07/01/backup-routeros-config.html</link>
            <guid isPermaLink="true">https://blog.ahlava.cz/2022/07/01/backup-routeros-config.html</guid>
            
            <category>short</category>
            
            <category>sysadmin</category>
            
            
            <content>
            &lt;p&gt;Recently, I have been fond of with Mikrotik routers and their enterprise-grade RouterOS system. There is plenty of possible configuration, which gives me great freedom of choice in how I want my home network to operate. Because setting everything, including VPN, guest network and more may take quite a lot of time, it is a good idea to backup the configuration.&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;the-idea&quot;&gt;The idea&lt;/h2&gt;

&lt;p&gt;My idea is to have a scheduled script that will save the router configuration to a directory inside my self-hosted &lt;a href=&quot;https://nextcloud.com/&quot;&gt;NextCloud&lt;/a&gt; instance. This will also save me the hassle with versioning the backups, as NextCloud itself takes care of this.&lt;/p&gt;

&lt;p&gt;The backup on the router is saved to its disc, which is accessible trough SFTP protocol, while the NextCloud can be accessed through WebDAV. &lt;a href=&quot;https://rclone.org/&quot;&gt;RClone&lt;/a&gt; is an open-source solution that can manage both.&lt;/p&gt;

&lt;h2 id=&quot;the-implementation&quot;&gt;The implementation&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;Create a new account and group on the router – &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;config-backup&lt;/code&gt;. When creating the group, I have to make sure to check ssh, ftp, read, write, policy, test and sensitive permissions.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Add router entry to a ssh config on the machine on which will the scheduled script run:&lt;/p&gt;

    &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Host router
 HostName router
 User config-backup
 IdentityFile /secret/ssh_id_rsa
 IdentitiesOnly yes
 StrictHostKeyChecking yes
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Create new RClone remote for the router using SFTP&lt;/p&gt;

    &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[router]
type = sftp
host = router
user = config-backup
disable_hashcheck = true
key_file = /secret/ssh_id_rsa
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Create new RClone remote for the NextCloud using WebDAV&lt;/p&gt;

    &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[adam-cloud]
type = webdav
url = https://cloud/remote.php/dav/files/Adam/
vendor = nextcloud
user = Adam
pass = *****
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Finally, combine all these setting inside a single Bash script&lt;/p&gt;

    &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;#!/bin/bash&lt;/span&gt;
ssh router &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; /system/backup/save &lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;config.backup dont-encrypt&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;no &lt;span class=&quot;nv&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;123456 &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;
rclone copy router:/config.backup adam-cloud:/Archive/router-config/ &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;
ssh router &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; /file/ remove config.backup
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And that’s it. After running the Bash script, the router configuration backup is saved into my NextCloud.&lt;/p&gt;

            </content>
        </item>
        
        
        
        <item>
            <title>More off-site backup storage is needed</title>
            <description>&lt;p&gt;Backing data up is vital if we do not want to lose it. My &lt;a href=&quot;https://nextcloud.com/&quot;&gt;NextCloud&lt;/a&gt; server saves all data to a BTRFS partition with daily &lt;a href=&quot;https://wiki.archlinux.org/title/Btrfs#Snapshots&quot;&gt;snapshots&lt;/a&gt;, which are then weekly synced with a local secondary disc in case of a primary disc failure. But what to do in case of a lightning strike that would overload surge protection? For that reason, I also keep an off-site &lt;a href=&quot;https://rpishop.cz/raspberry-pi-3b/896-raspberry-pi-3-model-b-plus-64-bit-1gb-ram-713179640259.html&quot;&gt;Raspberry PI&lt;/a&gt;, which works as a tertiary form of backup. The only problem is that it had just run out of storage.&lt;/p&gt;

</description>
            <pubDate>Wed, 15 Jun 2022 00:00:00 +0000</pubDate>
            <link>https://blog.ahlava.cz/2022/06/15/backup-server-hdd.html</link>
            <guid isPermaLink="true">https://blog.ahlava.cz/2022/06/15/backup-server-hdd.html</guid>
            
            <category>sysadmin</category>
            
            
            <content>
            &lt;p&gt;Backing data up is vital if we do not want to lose it. My &lt;a href=&quot;https://nextcloud.com/&quot;&gt;NextCloud&lt;/a&gt; server saves all data to a BTRFS partition with daily &lt;a href=&quot;https://wiki.archlinux.org/title/Btrfs#Snapshots&quot;&gt;snapshots&lt;/a&gt;, which are then weekly synced with a local secondary disc in case of a primary disc failure. But what to do in case of a lightning strike that would overload surge protection? For that reason, I also keep an off-site &lt;a href=&quot;https://rpishop.cz/raspberry-pi-3b/896-raspberry-pi-3-model-b-plus-64-bit-1gb-ram-713179640259.html&quot;&gt;Raspberry PI&lt;/a&gt;, which works as a tertiary form of backup. The only problem is that it had just run out of storage.&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;how-is-the-rpi-storage-connected&quot;&gt;How is the RPi storage connected?&lt;/h2&gt;

&lt;p&gt;Because I want to run the things as cost-effective as possible, the RPi runs with a single USB connected 4.5TiB external harddrive. It is also attached to an &lt;a href=&quot;https://www.unipi.technology/products/unipi-1-1-1-1-lite-19?categoryId=1&amp;amp;categorySlug=unipi-1-1&quot;&gt;UniPi extension&lt;/a&gt;, which allows me to control eight relays with REST API. This gives me an opportunity to power on and off the connected external hard drive whenever needed.&lt;/p&gt;

&lt;h2 id=&quot;what-is-the-off-site-backup-process&quot;&gt;What is the off-site backup process?&lt;/h2&gt;

&lt;p&gt;Every week, the RPi powers on the connected hard drive with a single &lt;a href=&quot;https://blog.tinned-software.net/create-a-luks-encrypted-partition-on-linux-mint/&quot;&gt;LUKS&lt;/a&gt;-encrypted BTRFS partition. Because the data inside the backup are write-mostly, I am using BTRFS’s &lt;a href=&quot;https://btrfs.wiki.kernel.org/index.php/Compression&quot;&gt;transparent compression&lt;/a&gt; to maximalize available virtual space. After a successful mount, the RPi syncs data from the primary NextCloud drive using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rsync&lt;/code&gt; over SSH (with options &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--inplace&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--no-whole-file&lt;/code&gt; to keep the advantage of BTRFS’s &lt;a href=&quot;https://wiki.archlinux.org/title/Btrfs#Copy-on-Write_(CoW)&quot;&gt;CoW&lt;/a&gt; feature). The privileges separation is designed so that at no point in time does any of the servers have write access to the other’s data to minimize the possibility of human error deleting all of it. After a successful sync, a new BTRFS snapshot is created.&lt;/p&gt;

&lt;h2 id=&quot;how-to-extend-the-storage&quot;&gt;How to extend the storage?&lt;/h2&gt;

&lt;p&gt;Thanks to the fact that the disc is formatted to use BTRFS, all I need to do is to call a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;btrfs device add&lt;/code&gt; command on a second device. This seems like a job for my old 640GB &lt;a href=&quot;https://web.archive.org/web/20120824181128/http://www.alza.cz/fujitsu-siemens-storagebird-35ev821-d104297.htm&quot;&gt;Fujitsu-SIEMENS Storagebird 35EV821&lt;/a&gt; (sale ended before 2012). Surely, before using it, I have to make sure that it still works properly, and because this old drive does not support SMART, I have to stick with a good-old baddblocks command (&lt;strong&gt;BEWARE: this command will rewrite the whole drive, do not copy it blindly&lt;/strong&gt;):&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; badblocks &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; badblocks.log &lt;span class=&quot;nt&quot;&gt;-svw&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-b&lt;/span&gt; 512 &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; 65536 /dev/sde
Checking &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;bad blocks &lt;span class=&quot;k&quot;&gt;in &lt;/span&gt;read-write mode
From block 0 to 1250263727
Testing with pattern 0xaa: &lt;span class=&quot;k&quot;&gt;done
&lt;/span&gt;Reading and comparing: &lt;span class=&quot;k&quot;&gt;done
&lt;/span&gt;Testing with pattern 0x55: &lt;span class=&quot;k&quot;&gt;done
&lt;/span&gt;Reading and comparing: &lt;span class=&quot;k&quot;&gt;done
&lt;/span&gt;Testing with pattern 0xff:done
Reading and comparing: &lt;span class=&quot;k&quot;&gt;done
&lt;/span&gt;Testing with pattern 0x00: 60.57% &lt;span class=&quot;k&quot;&gt;done&lt;/span&gt;, 42:04:35 elapsed. &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;0/0/0 errors&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
Interrupted at block 758382592
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I caused the interruption because the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;badblocks&lt;/code&gt; was unlikely to find new damaged blocks after multiple successful passes, so it was expected. Furthermore, the backup will be tested monthly with BTRFS’s scrub to ensure data consistency so that any potential hidden problems will be soon flagged.&lt;/p&gt;

&lt;p&gt;The disc seems OK, so it is time to connect it to the RPi. First - I need to cut the power wire and insert it into an UniPi relay so that I can power it on and off as needed, like the first disc.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/blog-realy-connected.jpeg&quot; alt=&quot;The connected relay&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;finishing-the-software-connection&quot;&gt;Finishing the software connection&lt;/h2&gt;

&lt;p&gt;First, the disc has to be encrypted. Because it is now empty, it has no ID except for the standard &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/dev/sda&lt;/code&gt; path:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; cryptsetup luksFormat /dev/sda
WARNING!
&lt;span class=&quot;o&quot;&gt;=====&lt;/span&gt;
This will overwrite data on /dev/sda irrevocably.
Are you sure? &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;Type uppercase &lt;span class=&quot;nb&quot;&gt;yes&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;: YES
Enter passphrase &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; /dev/sda: &lt;span class=&quot;k&quot;&gt;********&lt;/span&gt;
Verify passphrase: &lt;span class=&quot;k&quot;&gt;********&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Next, because the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/dev/sda&lt;/code&gt; is not permanent and can change based on the time the kernel detects a new drive, I want to use the drive UUID:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;ls&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-l&lt;/span&gt; /dev/disk/by-uuid/
total 0
lrwxrwxrwx 1 root root 9 Jun 4 18:06  1234UUID -&amp;gt; ../../sda
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now that we have the UUID of the disc, we can use it to open the LUKS encrypted content (which is currently empty):&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; cryptsetup luksOpen /dev/disk/by-uuid/12134UUID cryptBackup2
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And now comes the magic moment I have been waiting on – adding the storage:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;df&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-h&lt;/span&gt; /media/backup/
Filesystem Size Used Avail Use% Mounted on
/dev/mapper/cryptBackup1 4.6T 4.6T 33G 100% /media/backup
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; btrfs device add &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; /dev/mapper/cryptBackup2 /media/backup/
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;df&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-h&lt;/span&gt; /media/backup/
Filesystem Size Used Avail Use% Mounted on
/dev/mapper/cryptBackup1 5.2T 4.6T 629G 89% /media/backup
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Done! My off-site backup server has almost 600GB of new storage (effectively more because of the transparent BTRFS compression).&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;My off-site backup server has run out of storage, but thanks to the magic of BTRFS, extending it was a piece of cake! The process, of course, involved more steps like connecting the disc power to a controlled relay and using transparent encryption.&lt;/p&gt;

            </content>
        </item>
        
        
        
        <item>
            <title>How I found (and fixed) a critical bug a day before official launch day</title>
            <description>&lt;p&gt;A day before launching the new online seminar &lt;a href=&quot;https://naskoc.fi.muni.cz/&quot;&gt;Jump-start the FI&lt;/a&gt; (jump@fi for short), I have been doing some final testing that everything is OK and clear to launch. This new online seminar is expected to be officially advertised to the hundreds of newly accepted students of the &lt;a href=&quot;https://fi.muni.cz/&quot;&gt;Faculty of informatics at Masaryk University&lt;/a&gt;, so the stakes for making it behave correctly from the start are pretty high. Fortunately, about forty people have already tested the entirely new web frontend, so the assurance that nothing can go wrong was also relatively high. Unfortunately, everyone (me including) has managed to miss a critical bug in the application.&lt;/p&gt;

</description>
            <pubDate>Wed, 01 Jun 2022 00:00:00 +0000</pubDate>
            <link>https://blog.ahlava.cz/2022/06/01/bug-before-launchday.html</link>
            <guid isPermaLink="true">https://blog.ahlava.cz/2022/06/01/bug-before-launchday.html</guid>
            
            <category>dev</category>
            
            
            <content>
            &lt;p&gt;A day before launching the new online seminar &lt;a href=&quot;https://naskoc.fi.muni.cz/&quot;&gt;Jump-start the FI&lt;/a&gt; (jump@fi for short), I have been doing some final testing that everything is OK and clear to launch. This new online seminar is expected to be officially advertised to the hundreds of newly accepted students of the &lt;a href=&quot;https://fi.muni.cz/&quot;&gt;Faculty of informatics at Masaryk University&lt;/a&gt;, so the stakes for making it behave correctly from the start are pretty high. Fortunately, about forty people have already tested the entirely new web frontend, so the assurance that nothing can go wrong was also relatively high. Unfortunately, everyone (me including) has managed to miss a critical bug in the application.&lt;/p&gt;

&lt;!--more--&gt;

&lt;h2 id=&quot;how-does-jumpfi-work&quot;&gt;How does jump@fi work?&lt;/h2&gt;

&lt;p&gt;Jump@fi is an online seminar whose main components are tasks with tests that its participants can solve. These tasks are presented in the form of a directed acyclic graph, where the participant has first to solve all tasks with an edge pointing to the task before it is unlocked. So you would guess that this part is critical-bug-free as it was tested the most. You would be wrong.They are going to be stuck if someone does not fix it, which is certainly not the best 
start for an activity officially advertised by the Faculty of 
Informatics.&lt;/p&gt;

&lt;h2 id=&quot;the-bug&quot;&gt;The bug&lt;/h2&gt;

&lt;p&gt;To take a look at the bug, let’s visit our test environment. As seen in the picture below, we have a greyed-out egg-like task in the second row with an edge pointing to it from the turtle task.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/blog-critical-bug-initial.png&quot; alt=&quot;The initial state in the test environemnt&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This arrangement means that the participant must first solve the turtle task before the egg-like task is unlocked (and is not greyed out anymore). Easy, let me just solve the turtle task, and we can move to the egg-like one.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/blog-critical-bug-the-bug.png&quot; alt=&quot;The bug itself - first task is solved, but the second one is not open&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Oh no. I have solved the turtle task (as indicated by the green check), and now I want to move to the egg-like one, but it is still locked! This is an action that most participants will perform and end up with a locked task, even though it should be opened by now.  They are going to be stuck if someone does not fix it, which is certainly not the best start for an activity officially advertised by the Faculty of Informatics. We have found our critical bug, just a day before offical launchday.&lt;/p&gt;

&lt;h2 id=&quot;the-cause&quot;&gt;The cause&lt;/h2&gt;

&lt;p&gt;After investigating the issue, I have discovered that the source of this issue is a logical mistake made when designing the task icon component. This is the code responsible for updating the task icon:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;Input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;val&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;TaskWithIcon&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// subscribe to the task so that task info is updated on its solve, etc.&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;task$&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getTask&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;val&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;As you can see, this code takes care of updating the task icon whenever the task changes – this is how the green checkmark appeared in the turtle task icon. Do you see where the logical mistake is? The task itself is not updated; only its prerequisite is, and nobody asked the task icon to watch for changes of that.&lt;/p&gt;

&lt;h2 id=&quot;the-solution&quot;&gt;The solution&lt;/h2&gt;

&lt;p&gt;The solution is simple – just watch for the changes in prerequisites of the task. The only problem is that when a task is solved, the user is on another page, so the icon of the connected task does not exist at the moment. How can something watch for anything when it does not exist? To our advantage, the Angular with RxJS comes to the rescue because it internally remembers the component state when we come back to the graph page after solving the turtle task. Let’s edit the task-watching observable to account for the prerequisites changes:&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;Input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;val&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;TaskWithIcon&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// watch tasks requirements for state change&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;requirementsIDs&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Utils&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;flatArray&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;val&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;prerequisities&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;requirementsState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;requirementId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{};&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;requirementsChange$&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;combineLatest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;requirementsIDs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;watchedTaskId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getTask&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;watchedTaskId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;TaskWithIcon&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[])&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;changed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[];&lt;/span&gt;

      &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;requirement&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;of&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// compare states for changes&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;savedState&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;requirementsState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;requirement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;logger&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;debug&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`[TASK] requirement &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;requirement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; of task &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; was &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;savedState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; and now is &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;requirement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;savedState&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;requirement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;requirementsState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;requirement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;requirement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;if &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;savedState&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;undefined&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;nx&quot;&gt;changed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;requirement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
          &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;isChange&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;changed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;if &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;isChange&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;logger&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;debug&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`[TASK] requirement of task &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;val&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; have changed their state: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;changed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;isChange&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// refresh task&apos;s data when its requirements change&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;refreshTaskCache$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Observable&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;boolean&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;merge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;requirementsChange$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;mapTo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)),&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getTask&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;val&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;mapTo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// subscribe to the task and so that task info is updated on its solve, etc.&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;task$&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;refreshTaskCache$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;mergeMap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;refreshCache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getTaskOnce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;val&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;refreshCache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Wow. What an ugly piece of code. It works, but it could use a little separation into multiple smaller functions. Let me move the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requirementsChange$&lt;/code&gt; Observable to a separate function, away from the task icon component to the Task service, which is already responsible for watching for the changes in the task itself.&lt;/p&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// TASK SERVICE&lt;/span&gt;
&lt;span class=&quot;cm&quot;&gt;/**
 * Watches for changes in the state of the requirements of the task
 * @param task task to which requirements watch for
 * @return Observable&amp;lt;true&amp;gt; emits whenever the state the requirements has changed from the initial state
 */&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;watchTaskRequirementsStateChange&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;TaskWithIcon&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Observable&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// watch tasks requirements for state change&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;requirementsIDs&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Utils&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;flatArray&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;prerequisities&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;requirementsState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;requirementId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;kr&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{};&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;combineLatest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;requirementsIDs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;watchedTaskId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getTask&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;watchedTaskId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;TaskWithIcon&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[])&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;cm&quot;&gt;/* ... same as before ... */&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}),&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;mapTo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-typescript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// TASK ICON COMPONENT&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;Input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;val&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;TaskWithIcon&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// refresh task&apos;s data when its requirements change&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;refreshTaskCache$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Observable&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;boolean&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;merge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;watchTaskRequirementsStateChange&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;val&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;mapTo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)),&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getTask&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;val&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;mapTo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// subscribe to the task and so that task info is updated on its solve, etc.&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;task$&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;refreshTaskCache$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nf&quot;&gt;mergeMap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;refreshCache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getTaskOnce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;val&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;refreshCache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This is much better. Now we can call the service function from the component and have a nicer-looking code. Except for one small thing - it does not work anymore. As I found out, for some reason, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requirementsState&lt;/code&gt; dictionary empties itself when used inside the service, while it keeps its content when inside the component. Also, when I tried passing the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requirementsState&lt;/code&gt; from the component as a parameter to the service function, still it emptied itself every time. Curious.&lt;/p&gt;

&lt;h2 id=&quot;the-final-solution&quot;&gt;The final solution&lt;/h2&gt;

&lt;p&gt;At this point, I have concluded that I have to store the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requirementsState&lt;/code&gt; in some static cache. But when to decide what and when to store, to prevent unwanted memory consumption? Fortunately, the task service already has a caching mechanism for tasks. We can just expend that for also caching task prerequisites state! It also makes sense that we will not need to watch for changes in prerequisites of tasks that were not seen long enough for it to be exempt from the cache. So does that work?&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/blog-critical-bug-fix.png&quot; alt=&quot;All tasks are now &quot; /&gt;&lt;/p&gt;

&lt;p&gt;Yes! It does! Finally!&lt;/p&gt;

&lt;h2 id=&quot;why-has-nobody-discovered-this-bug-before&quot;&gt;Why has nobody discovered this bug before?&lt;/h2&gt;

&lt;p&gt;The only thing that remains is to answer why has not even one of the forty people (myself included) that have tested this web application noticed this critical bug? These forty people can be divided into three groups:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;developer (me) - 1 person&lt;/li&gt;
  &lt;li&gt;organizers of the &lt;a href=&quot;https://ksi.fi.muni.cz/&quot;&gt;Online seminar of Informatics&lt;/a&gt; (KSI for short) ~ 16 people&lt;/li&gt;
  &lt;li&gt;KSI participants ~ 22 people&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Note: KSI is another online seminar which uses a slightly modified version of the web application (e. g. different colours)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;As a developer of this web application, I have missed this bug, possibly because it would vanish when the page reloads itself, which automatically happens after a source code change. The KSI organizers see all tasks already unlocked, so they were unaffected by this bug. Nevertheless, the last group, KSI participants, are affected by the bug only if they have not already solved all tasks, which is possible because they were given access to the web application in the last part of the KSI year. Oh well.&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;Just a single day before the official launch day of the new online seminar, I discovered a critical bug in the web application that would make all its participants stuck without the possibility to progress. After four hours of debugging what exactly went wrong and why sometimes a dictionary kept its state and sometimes it did not, I have settled for a &lt;a href=&quot;https://github.com/fi-ksi/web-frontend-angular/commit/8eae34900ebaab3aa00975d8f211bebb668323e4&quot;&gt;solution&lt;/a&gt;. The most fun part is that this application was quite well tested, but everyone has missed this bug due to bad timing.&lt;/p&gt;

            </content>
        </item>
        
        
    </channel>
</rss>
