<?xml version="1.0" encoding="utf-8" standalone="yes"?><?xml-stylesheet href="https://brandonrozek.com/css/pretty-feed-v3.xsl" type="text/xsl"?>
<rss version="2.0"
    xmlns:atom="http://www.w3.org/2005/Atom"
    xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Brandon Rozek</title>
    <link>https://brandonrozek.com/</link>
    <image>
      <title>Brandon Rozek</title>
      <link>https://brandonrozek.com/</link>
      <url>https://brandonrozek.com/img/avatar.jpg</url>
    </image>
    <description>Software Developer, Researcher, and Linux Enthusiast.</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <managingEditor>brozek@brandonrozek.com (Brandon Rozek)</managingEditor>
    <webMaster>brozek@brandonrozek.com (Brandon Rozek)</webMaster>
    
	<atom:link href="https://brandonrozek.com/index.xml" rel="self" type="application/rss+xml" />
    
    
    <item>
      <title>Blog Posts Are The Ideal Form of the Written Word</title>
      <link></link>
      <pubDate>Mon, 02 Mar 2026 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<p>100%</p>]]></description>
      <content:encoded><![CDATA[<p>100%</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>15 years of blogging</title>
      <link></link>
      <pubDate>Sun, 01 Mar 2026 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<p>Congratulations to Nolan on 15 years of blogging! So much life unfolds in that time period and it takes a steadfast commitment to keep going. Here&rsquo;s to another 15 years!</p>]]></description>
      <content:encoded><![CDATA[<p>Congratulations to Nolan on 15 years of blogging! So much life unfolds in that time period and it takes a steadfast commitment to keep going. Here&rsquo;s to another 15 years!</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>A programmer&#39;s loss of a social identity</title>
      <link></link>
      <pubDate>Sun, 01 Mar 2026 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<p>I&rsquo;m unsure what it means to be a computer programmer in 2026. I agree that technology discourse has changed dramatically over the last three years.</p>
<p>Though that doesn&rsquo;t mean that there aren&rsquo;t people passionate about <a href="https://rust-lang.org/">crabs</a>, creating <a href="https://lofi.so/">local-first software</a>, etc. Those folks are still there, even if they&rsquo;re not the mainstream.</p>
<p>This is why I&rsquo;m such a big proponent of curating your web experience using RSS feeds. It might take some effort to find people on the small/personal/indie web, but it&rsquo;s soo worth it.</p>]]></description>
      <content:encoded><![CDATA[<p>I&rsquo;m unsure what it means to be a computer programmer in 2026. I agree that technology discourse has changed dramatically over the last three years.</p>
<p>Though that doesn&rsquo;t mean that there aren&rsquo;t people passionate about <a href="https://rust-lang.org/">crabs</a>, creating <a href="https://lofi.so/">local-first software</a>, etc. Those folks are still there, even if they&rsquo;re not the mainstream.</p>
<p>This is why I&rsquo;m such a big proponent of curating your web experience using RSS feeds. It might take some effort to find people on the small/personal/indie web, but it&rsquo;s soo worth it.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Home-cooked software</title>
      <link></link>
      <pubDate>Sun, 01 Mar 2026 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<p>It&rsquo;s heartwarming to see someone create an app that brings joy to the people around them. Ham&rsquo;s <a href="https://doppelkopf.club/">Doppelkopf Club</a> not only lets you play a traditional German card game, but also teaches you how to play as well! This preserves cultural knowledge.</p>
<p>Home cooking to me is a necessity (lest I go broke) as well as a craft. Recently, I&rsquo;ve been cooking Paella weekly slightly tweaking each time to get to a recipe that I enjoy.</p>]]></description>
      <content:encoded><![CDATA[<p>It&rsquo;s heartwarming to see someone create an app that brings joy to the people around them. Ham&rsquo;s <a href="https://doppelkopf.club/">Doppelkopf Club</a> not only lets you play a traditional German card game, but also teaches you how to play as well! This preserves cultural knowledge.</p>
<p>Home cooking to me is a necessity (lest I go broke) as well as a craft. Recently, I&rsquo;ve been cooking Paella weekly slightly tweaking each time to get to a recipe that I enjoy.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Saying &#39;No&#39; In an Age of Abundance</title>
      <link></link>
      <pubDate>Sun, 01 Mar 2026 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<blockquote>
<p>In an age of abundance, restraint becomes the only scarce thing left, which means saying &rsquo;no&rsquo; is more valuable than ever.</p></blockquote>
<p>I fully agree! We all excel at different tasks, and with time and focus we can cultivate them. Just because we can do something, doesn&rsquo;t mean we <em>should</em>. There&rsquo;s always a trade-off.</p>
<p>The piece was written in the context of building a product in a business, but honestly many of these points apply to our lives in general.</p>]]></description>
      <content:encoded><![CDATA[<blockquote>
<p>In an age of abundance, restraint becomes the only scarce thing left, which means saying &rsquo;no&rsquo; is more valuable than ever.</p></blockquote>
<p>I fully agree! We all excel at different tasks, and with time and focus we can cultivate them. Just because we can do something, doesn&rsquo;t mean we <em>should</em>. There&rsquo;s always a trade-off.</p>
<p>The piece was written in the context of building a product in a business, but honestly many of these points apply to our lives in general.</p>
<p>What&rsquo;s our story? What do we care about? Saying yes to everything muddies the waters. At the same time, however, don&rsquo;t say no to everything. The most rewarding oppurtunities are not always the most obvious.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>The Web is Larger Than Facebook</title>
      <link></link>
      <pubDate>Sun, 01 Mar 2026 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<blockquote>
<p>It annoys me how much of a grip Facebook still has on being able to digitally participate in your local community.</p></blockquote>
<p>Same with Instagram. Though I get it &ndash; it&rsquo;s so easy to pull up your phone and post on these platforms.</p>
<p>Recently, I found <a href="https://the518.com/">The 518</a> which is a newsletter which covers events happening in the capital region of New York. Luckily they surface all the events posted on Instagram for me. Though I do wonder what my life would be like if I acquiesced and signed up for Meta&rsquo;s platforms.</p>]]></description>
      <content:encoded><![CDATA[<blockquote>
<p>It annoys me how much of a grip Facebook still has on being able to digitally participate in your local community.</p></blockquote>
<p>Same with Instagram. Though I get it &ndash; it&rsquo;s so easy to pull up your phone and post on these platforms.</p>
<p>Recently, I found <a href="https://the518.com/">The 518</a> which is a newsletter which covers events happening in the capital region of New York. Luckily they surface all the events posted on Instagram for me. Though I do wonder what my life would be like if I acquiesced and signed up for Meta&rsquo;s platforms.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>VSPursuer: A Tool for Finding Matrices Witnessing the Variable Sharing Property</title>
      <link>https://brandonrozek.com/paper/2602.01/</link>
      <pubDate>Mon, 09 Feb 2026 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid>https://brandonrozek.com/paper/2602.01/</guid>
      <description><![CDATA[]]></description>
      <content:encoded><![CDATA[]]></content:encoded>
      
    </item>
    
    <item>
      <title>Bringing this website to the Tor network</title>
      <link>https://brandonrozek.com/blog/on-the-tor-network/</link>
      <pubDate>Sun, 01 Feb 2026 17:47:15 -0500</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid>https://brandonrozek.com/blog/on-the-tor-network/</guid>
      <description><![CDATA[<p>I believe in the freedom of information. By making my website available as a Tor hidden service, you can be sure to access the information even if it&rsquo;s blocked on the clearweb.</p>
<p>In this post, I&rsquo;ll share the steps I took and what I learned along the way. Huge credit to Christian <a href="https://cleberg.net/blog/self-hosting-tor.html">who wrote their own succinct version</a> of this post and helped me troubleshoot via email.</p>
<h3 id="getting-an-address">Getting an Address</h3>
<p>Unlike the clear web, we don&rsquo;t register a domain with anyone. <a href="https://github.com/torproject/torspec/blob/main/rend-spec-v3.txt">An address on Tor is a hash of your public key.</a></p>]]></description>
      <content:encoded><![CDATA[<p>I believe in the freedom of information. By making my website available as a Tor hidden service, you can be sure to access the information even if it&rsquo;s blocked on the clearweb.</p>
<p>In this post, I&rsquo;ll share the steps I took and what I learned along the way. Huge credit to Christian <a href="https://cleberg.net/blog/self-hosting-tor.html">who wrote their own succinct version</a> of this post and helped me troubleshoot via email.</p>
<h3 id="getting-an-address">Getting an Address</h3>
<p>Unlike the clear web, we don&rsquo;t register a domain with anyone. <a href="https://github.com/torproject/torspec/blob/main/rend-spec-v3.txt">An address on Tor is a hash of your public key.</a></p>
<p>This is why onion URLs are long and unreadable. Take a look at the following onion URL which takes you to the Tor homepage.</p>
<pre tabindex="0"><code>http://2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion
</code></pre><p>Notice that the address starts with <code>http</code>. Unlike the clearweb, <em>all</em> traffic via Tor is encrypted. Our hidden service will use the public key behind the URL during the protocol exchange.</p>
<p>The primary benefit of these addresses are that they are entirely decentralized (we create our own keys). They are also incredibly difficult to spoof. The downside is that they are not human readable. We can get a little bit closer to that though with vanity keys.</p>
<p>To create a vanity key, we can use <a href="https://github.com/cathugger/mkp224o#requirements">mkp224o</a>. After following the installation instructions, we can create our own onion URL starting with some <code>prefix</code> by running the following:</p>
<pre tabindex="0"><code>./mkp224o prefix
</code></pre><p>Given the current algorithms and hardware, we can easily generate keys which have a URL prefix length of up to six characters in minutes. Beyond that, the generation quickly becomes infeasible&hellip; However, we want this to be the case. If it was feasible to generate a key-pair for an entire onion URL then anyone can spoof your hidden service.</p>
<p>The script by default will create many folders in the current directory which have the onion URLs as their name and the contents of the keys within. Pick your favorite and copy it over to <code>/var/lib/tor/somehiddenservicename</code>.</p>
<p><strong>While you&rsquo;re at it, make a backup of these keys because if we lose it then we lose control over the domain.</strong></p>
<h2 id="installing-and-configuring-tor">Installing and Configuring Tor</h2>
<p>Now that we have our address. Let&rsquo;s get Tor installed and set up. In this guide, I&rsquo;ll show how to do so via Podman Quadlets.</p>
<p>We&rsquo;ll use the Docker container <a href="https://github.com/dockur/tor">dockur/tor</a>. Since Tor is a cryptographic software, we need to be extremely careful where we download it from. As of this time of writing if we look at the Dockerfile in this repo, we can see that this is a simple wrapper over Alpine&rsquo;s tor packages. Currently, I feel that it&rsquo;s safe to trust the Alpine maintainers.</p>
<p>After selecting the Docker container, we need to write the Quadlet definition file. Here&rsquo;s how I have <code>/etc/containers/systemd/tor.container</code> configured:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#66d9ef">[Container]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">ContainerName</span><span style="color:#f92672">=</span><span style="color:#e6db74">tor</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">HostName</span><span style="color:#f92672">=</span><span style="color:#e6db74">tor</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Image</span><span style="color:#f92672">=</span><span style="color:#e6db74">docker.io/dockurr/tor</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">AutoUpdate</span><span style="color:#f92672">=</span><span style="color:#e6db74">registry</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Volume</span><span style="color:#f92672">=</span><span style="color:#e6db74">/etc/tor:/etc/tor:ro,Z</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Volume</span><span style="color:#f92672">=</span><span style="color:#e6db74">/var/lib/tor:/var/lib/tor:Z,U</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[Service]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Restart</span><span style="color:#f92672">=</span><span style="color:#e6db74">always</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[Install]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">WantedBy</span><span style="color:#f92672">=</span><span style="color:#e6db74">default.target</span>
</span></span></code></pre></div><p>Before we run it, we&rsquo;ll need to create our main Tor configuration file. This lives in <code>/etc/tor/torrc</code>.</p>
<pre tabindex="0"><code>SocksPort 0
HiddenServiceDir /var/lib/tor/somehiddenservicename
HiddenServicePort 80 IP_OF_NGINX_CONTAINER:80
</code></pre><p>Replace <code>IP_OF_NGINX_CONTAINER</code> with the internal IP of the Nginx container or of the host machine if it is running on there. Here&rsquo;s an explanation of each line:</p>
<table>
  <thead>
      <tr>
          <th>Key</th>
          <th>Value</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>SocksPort</code></td>
          <td>By default, the tor service will open a SOCKS port so that we can have other HTTP clients proxy to the dark web. Setting this to 0 disables that behavior.</td>
      </tr>
      <tr>
          <td><code>HiddenServiceDir</code></td>
          <td>The location of our hostname file and the keys for our hidden service.</td>
      </tr>
      <tr>
          <td><code>HiddenServicePort</code></td>
          <td>The port on the Tor network followed by the <code>address:port</code> to proxy the traffic to. We may have multiple of these per hidden service.</td>
      </tr>
  </tbody>
</table>
<p>Since we&rsquo;re proxying traffic to our application, let&rsquo;s configure our target next.</p>
<h2 id="configuring-nginx">Configuring Nginx</h2>
<p>My website is a static site served by Nginx. These files are linked together via absolute URLs. This adds some complications because the onion hidden service URL is completely different than my clearnet one.</p>
<p>One solution would be to make my URLs relative. However, I want my website to be as portable as possible. For example, you can run my website in a subfolder. Thus, our solution here is to have an entirely separate copy of the website where the only difference is the internal URLs.</p>
<p>I&rsquo;ve set this version of my website to live in <code>/var/www/brozekhs</code>.</p>
<p>As such, we have to create an Nginx configuration file solely for our hidden service. We&rsquo;ll make sure to point the root to the folder which contains the hidden service version of my website.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#66d9ef">server</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">listen</span> <span style="color:#ae81ff">80</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">server_name</span> <span style="color:#e6db74">ONION_URL</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#f92672">root</span> <span style="color:#e6db74">/var/www/brozekhs</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">index</span> <span style="color:#e6db74">index.html</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#75715e"># ...
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span>	<span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#f92672">allow</span> <span style="color:#e6db74">IP_OF_TOR_CONTAINER</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">deny</span> <span style="color:#e6db74">all</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># ...
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>	}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Replace <code>/var/www/brozekhs</code>, <code>ONION_URL</code> and <code>IP_OF_TOR_CONTAINER</code> with their respective values. We need to configure the <code>allow</code> and <code>deny</code> lines because we don&rsquo;t want someone to confirm that our machine responds to tor addresses over the clearnet.</p>
<p>After this when running my <code>nginx</code> container I came across the following error:</p>
<pre tabindex="0"><code>date time [emerg] 1#1: could not build server_names_hash, you should increase server_names_hash_bucket_size: 64
</code></pre><p>As stated, the solution is to increase the server name hash bucket size. I doubled it from 64 to 128 in the <code>http</code> block of <code>/etc/nginx/nginx.conf</code>:</p>
<pre tabindex="0"><code>server_names_hash_bucket_size 128;
</code></pre><p>Lastly if you have a clearnet site and you want to advertise the onion URL for users visiting the clearnet URL on the Tor browser, then we can add an <a href="https://community.torproject.org/onion-services/advanced/onion-location/"><code>Onion-Location</code> header</a> to the Nginx config of our clearnet site.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#66d9ef">location</span> <span style="color:#e6db74">/</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#75715e"># ...
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#f92672">add_header</span> <span style="color:#e6db74">Onion-Location</span> <span style="color:#e6db74">ONION_URL</span>$request_uri;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Replace <code>ONION_URL</code> with your own and make sure that <code>$request_uri</code> stays at the end of the header value. This will help the Tor browser with the redirection.</p>
<h2 id="start-em-up">Start em up!</h2>
<p>With that, we can start our two services:</p>
<pre tabindex="0"><code>sudo systemctl start nginx
sudo systemctl start tor
</code></pre><p>When we look at <code>journalctl -U tor</code>, we should see something like the following:</p>
<pre tabindex="0"><code>datetime hostname systemd[1]: Started tor.service.
datetime hostname tor[681220]: datetime [notice] Tor can&#39;t help you if you use it wrong! Learn how to be safe at https://support.torproject.org/faq/staying-anonymous/
datetime hostname tor[681220]: datetime [notice] Read configuration file &#34;/etc/tor/torrc&#34;.
datetime hostname tor[681220]: datetime [notice] Parsing GEOIP IPv4 file /usr/share/tor/geoip.
datetime hostname tor[681220]: datetime [notice] Parsing GEOIP IPv6 file /usr/share/tor/geoip6.
datetime hostname tor[681220]: datetime [notice] Bootstrapped 0% (starting): Starting
datetime hostname tor[681220]: datetime [notice] Starting with guard context &#34;default&#34;
datetime hostname tor[681220]: datetime [notice] Bootstrapped 5% (conn): Connecting to a relay
datetime hostname tor[681220]: datetime [notice] Bootstrapped 10% (conn_done): Connected to a relay
datetime hostname tor[681220]: datetime [notice] Bootstrapped 14% (handshake): Handshaking with a relay
datetime hostname tor[681220]: datetime [notice] Bootstrapped 15% (handshake_done): Handshake with a relay done
datetime hostname tor[681220]: datetime [notice] Bootstrapped 45% (requesting_descriptors): Asking for relay descriptors
datetime hostname tor[681220]: datetime [notice] Bootstrapped 50% (loading_descriptors): Loading relay descriptors
datetime hostname tor[681220]: datetime [notice] Bootstrapped 55% (loading_descriptors): Loading relay descriptors
datetime hostname tor[681220]: datetime [notice] Bootstrapped 60% (loading_descriptors): Loading relay descriptors
datetime hostname tor[681220]: datetime [notice] Bootstrapped 66% (loading_descriptors): Loading relay descriptors
datetime hostname tor[681220]: datetime [notice] Bootstrapped 75% (enough_dirinfo): Loaded enough directory info to build circuits
datetime hostname tor[681220]: datetime [notice] Bootstrapped 90% (ap_handshake_done): Handshake finished with a relay to build circuits
datetime hostname tor[681220]: datetime [notice] Bootstrapped 95% (circuit_create): Establishing a Tor circuit
datetime hostname tor[681220]: datetime [notice] Bootstrapped 100% (done): Done
</code></pre><p>We won&rsquo;t be able to access the hidden service until the bootstrapping process reaches 100%. Depending on the state of the network, this might take awhile&hellip;</p>
<p>I mention the network state since it&rsquo;s common for me to see this in the logs:</p>
<pre tabindex="0"><code>datetime hostname tor[295456]: datetime [warn] Detected possible compression bomb with input size = 17413 and output size = 515472 (compression factor = 29.60)
datetime hostname tor[295456]: datetime [warn] Possible compression bomb; abandoning stream.
datetime hostname tor[295456]: datetime [warn] Detected possible compression bomb with input size = 23289 and output size = 774624 (compression factor = 33.26)
datetime hostname tor[295456]: datetime [warn] Possible compression bomb; abandoning stream.
</code></pre><p>This compression or <a href="https://en.wikipedia.org/wiki/Zip_bomb">zip bomb</a> appears to be an attempt at a distributed <a href="https://en.wikipedia.org/wiki/Denial-of-service_attack">denial of service attack</a>. Luckily, the Tor software is smart enough to guard against this.</p>
<p>Every so often my Tor service goes down and I need to restart the daemon for it to come back up. I haven&rsquo;t gotten around to writing monitoring scripts for my hidden service so if anyone has any tips feel free to get in touch.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Tano Bonfanti</title>
      <link></link>
      <pubDate>Sun, 01 Feb 2026 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<p>A nice interview with a concept artist. What Tano said about overcoming creative blocks reasonated with me.</p>
<blockquote>
<p>You have to sit through that uncomfortable feeling, once you have done this enough you realize that is just a fleeting moment of insecurity, be aware that sometimes it will be present, and then it will pass, the inspiration comes back.</p></blockquote>
<blockquote>
<p>The problem is how much importance we give to this feeling and how afraid we are that they will never go away. Once you realize that they do fade, you have  learned how to deal with the block.</p>]]></description>
      <content:encoded><![CDATA[<p>A nice interview with a concept artist. What Tano said about overcoming creative blocks reasonated with me.</p>
<blockquote>
<p>You have to sit through that uncomfortable feeling, once you have done this enough you realize that is just a fleeting moment of insecurity, be aware that sometimes it will be present, and then it will pass, the inspiration comes back.</p></blockquote>
<blockquote>
<p>The problem is how much importance we give to this feeling and how afraid we are that they will never go away. Once you realize that they do fade, you have  learned how to deal with the block.</p></blockquote>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>The RAM Nightmare: How I Lost My Sanity (and Almost My Deadline)</title>
      <link></link>
      <pubDate>Sun, 01 Feb 2026 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<p>Ouch a RAM stick going bad sounds no fun. I haven&rsquo;t had that happen to me before, so it&rsquo;s informative to see the possible symptoms.</p>]]></description>
      <content:encoded><![CDATA[<p>Ouch a RAM stick going bad sounds no fun. I haven&rsquo;t had that happen to me before, so it&rsquo;s informative to see the possible symptoms.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Tiny Talk with Keith Wehmeyer</title>
      <link></link>
      <pubDate>Sun, 01 Feb 2026 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<p>I really enjoyed this interview. It&rsquo;s cool to read about the life of a farmer. It&rsquo;s also wholesome to see how the folks in their community help each other out.</p>]]></description>
      <content:encoded><![CDATA[<p>I really enjoyed this interview. It&rsquo;s cool to read about the life of a farmer. It&rsquo;s also wholesome to see how the folks in their community help each other out.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Optimization Countermeasures</title>
      <link></link>
      <pubDate>Tue, 27 Jan 2026 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<p>Every so often one has to fight the compiler to prevent certain optimizations from taking place. This article talks about using value barriers (which are essentially empty inline assembly blocks) to prevent the compiler from optimizing code based on the value of a variable. Miguel writes about how these techniques are used all the time when writing Cryptographic code which fights against the constant-time threat model.</p>]]></description>
      <content:encoded><![CDATA[<p>Every so often one has to fight the compiler to prevent certain optimizations from taking place. This article talks about using value barriers (which are essentially empty inline assembly blocks) to prevent the compiler from optimizing code based on the value of a variable. Miguel writes about how these techniques are used all the time when writing Cryptographic code which fights against the constant-time threat model.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Tales of Christmas Trees</title>
      <link>https://brandonrozek.com/blog/tales-of-christmas-trees/</link>
      <pubDate>Sun, 25 Jan 2026 17:59:15 -0500</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid>https://brandonrozek.com/blog/tales-of-christmas-trees/</guid>
      <description><![CDATA[<p>In 2020, Clare and I got our first live Christmas tree. We were living in Virginia at the time, and we showed up to a farm that had many trees planted in rows. They gave us a bow saw, and it was up to us to chop one down and bring it home.</p>
<p><img src="/files/images/blog/transportingtree2020.jpg" alt="Brandon hauling his tree in a cart."></p>
<p>I enjoyed having a live tree in my apartment. I still remember the smell of fresh conifer. After several weeks, the holidays pass and the needles start falling to the ground. I don&rsquo;t quite remember how we got rid of this tree, but I wouldn&rsquo;t be surprised if we just threw it in our dumpster.</p>]]></description>
      <content:encoded><![CDATA[<p>In 2020, Clare and I got our first live Christmas tree. We were living in Virginia at the time, and we showed up to a farm that had many trees planted in rows. They gave us a bow saw, and it was up to us to chop one down and bring it home.</p>
<p><img src="/files/images/blog/transportingtree2020.jpg" alt="Brandon hauling his tree in a cart."></p>
<p>I enjoyed having a live tree in my apartment. I still remember the smell of fresh conifer. After several weeks, the holidays pass and the needles start falling to the ground. I don&rsquo;t quite remember how we got rid of this tree, but I wouldn&rsquo;t be surprised if we just threw it in our dumpster.</p>
<p>We later moved to New York. For the next few years, we would travel to see friends and family during the holidays. I used that as an excuse to not have a tree setup at home. Eventually Clare convinces me that we should still be festive at home, and we bought a plastic tree.</p>
<p>Around us in upstate New York, there are several bonfire events in January. There&rsquo;s one in particular that we&rsquo;ve attended for several New Year&rsquo;s Eve. What&rsquo;s special about this bonfire is that the fuel of the fire is not prepared as chopped wood in advance. Instead, it is sourced from the community.</p>
<p>Let me paint a picture. It&rsquo;s a cold winter night, and there is a group of around 20-30 people bundled up in full winter gear staying close to the bonfire. As time progresses, the fire gets smaller and smaller, and we get closer to try to feel some of its heat.  Then, a person in a pickup truck arrives, gets out of their vehicle, and chucks their Christmas tree from the bed of the truck onto the fire. The fire rages.</p>
<p><img src="/files/images/blog/bonfire2025.jpg" alt="Christmas tree burning in a bonfire"></p>
<p>Now I find this very cool, but some trees have different fates. Ton writes on <a href="https://www.zylstra.org/blog/2026/01/not-the-elves/">his blog</a> that they have the same tree every year. Instead of sawing their tree down, the tree gets delivered to their place with the root ball intact. Once the holidays are over, the growers come back and pick it up.</p>
<p>Through an email exchange, I learn that this is a <a href="https://adopteereenkerstboom.nl/">national service</a> over in the Netherlands. Now that&rsquo;s pretty cool.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Crypto grifters are recruiting open-source AI developers</title>
      <link></link>
      <pubDate>Sun, 25 Jan 2026 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<blockquote>
<p>This system relies on your celebrity target being dazzled by receiving a large sum of free money. If you came to them before the money was there, they might ask questions like “why wouldn’t people just directly donate to me?”, or “are these people who think they’re supporting me going to lose all their money?“. But in the warm glow of a few hundred thousand dollars, it’s easy to think that it’s all working out excellently.</p>]]></description>
      <content:encoded><![CDATA[<blockquote>
<p>This system relies on your celebrity target being dazzled by receiving a large sum of free money. If you came to them before the money was there, they might ask questions like “why wouldn’t people just directly donate to me?”, or “are these people who think they’re supporting me going to lose all their money?“. But in the warm glow of a few hundred thousand dollars, it’s easy to think that it’s all working out excellently.</p></blockquote>
<p>Personally, I find it weird how some crypto platforms are trying to conflate supporting a creator and expectations of profit. I agree, why wouldn&rsquo;t people just directly donate?</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Why there&#39;s no European Google?</title>
      <link></link>
      <pubDate>Sun, 25 Jan 2026 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<p>This article is a great reminder of all the successful technology projects that came out of Europe: the web, Linux, git, OpenStreetMap, VLC &ndash; to name a few. While some of these projects might not be multi-billion dollar corporations, we can all agree that they have made a large impact on the world.</p>]]></description>
      <content:encoded><![CDATA[<p>This article is a great reminder of all the successful technology projects that came out of Europe: the web, Linux, git, OpenStreetMap, VLC &ndash; to name a few. While some of these projects might not be multi-billion dollar corporations, we can all agree that they have made a large impact on the world.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Wikipedia&#39;s 25th birthday proves the power of free speech</title>
      <link></link>
      <pubDate>Sun, 25 Jan 2026 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<p>Happy birthday to Wikipedia! Here&rsquo;s to another 25 years.</p>
<p>In this article, Kunal writes about how Wikipedia is able to flourish in part due to a lack of censorship by the US. This, however, should not be taken for granted. Free speech (like many rights) have to be continously fought for.</p>]]></description>
      <content:encoded><![CDATA[<p>Happy birthday to Wikipedia! Here&rsquo;s to another 25 years.</p>
<p>In this article, Kunal writes about how Wikipedia is able to flourish in part due to a lack of censorship by the US. This, however, should not be taken for granted. Free speech (like many rights) have to be continously fought for.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Some internet history reading</title>
      <link></link>
      <pubDate>Sat, 24 Jan 2026 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<p>Title says it all. A nice list of books which go over the history of the early Internet.</p>]]></description>
      <content:encoded><![CDATA[<p>Title says it all. A nice list of books which go over the history of the early Internet.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>The Toll No One Charges but Everyone Pays</title>
      <link></link>
      <pubDate>Sat, 24 Jan 2026 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<p>We all pay in time for sitting in traffic. Even with that, Kyle points out that there&rsquo;s no positive incentive to carpool. I see this in my own life. To get to my office, I can drive 7 minutes and park near the building. Or, I can walk 10 minutes to the bus stop, take a 20 minute bus ride, and then walk 10 minutes to my building. A trip that nearly takes 6 times the amount of time!</p>]]></description>
      <content:encoded><![CDATA[<p>We all pay in time for sitting in traffic. Even with that, Kyle points out that there&rsquo;s no positive incentive to carpool. I see this in my own life. To get to my office, I can drive 7 minutes and park near the building. Or, I can walk 10 minutes to the bus stop, take a 20 minute bus ride, and then walk 10 minutes to my building. A trip that nearly takes 6 times the amount of time!</p>
<p>If as a society we had better incentives, then we would have a lot more resources to get me to my office. Imagine if there was a van that can pick me up from my apartment and take me directly to my office.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Blogging as an Invitation for Dialogue</title>
      <link>https://brandonrozek.com/blog/blogging-as-dialogue-invitation/</link>
      <pubDate>Thu, 15 Jan 2026 11:21:29 -0500</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid>https://brandonrozek.com/blog/blogging-as-dialogue-invitation/</guid>
      <description><![CDATA[<p>We have many ways to share ideas today. We can:</p>
<ul>
<li>Text</li>
<li>Email</li>
<li>Pen a letter</li>
<li>Post on a microblog (Mastodon/X/Pixelfed/etc.)</li>
<li>Write a blog post</li>
</ul>
<p>But not all of these methods inherently create a conversation or dialogue. When I write a technical blog post, I don&rsquo;t expect a reply. Similarly, when I toot on Mastodon, I&rsquo;m fine if no one favorited the post. As such, (micro-)blogging differs greatly from texting and calling someone and is instead much closer to recording a postcast or uploading a video &ndash; a one-way transmission of information.</p>]]></description>
      <content:encoded><![CDATA[<p>We have many ways to share ideas today. We can:</p>
<ul>
<li>Text</li>
<li>Email</li>
<li>Pen a letter</li>
<li>Post on a microblog (Mastodon/X/Pixelfed/etc.)</li>
<li>Write a blog post</li>
</ul>
<p>But not all of these methods inherently create a conversation or dialogue. When I write a technical blog post, I don&rsquo;t expect a reply. Similarly, when I toot on Mastodon, I&rsquo;m fine if no one favorited the post. As such, (micro-)blogging differs greatly from texting and calling someone and is instead much closer to recording a postcast or uploading a video &ndash; a one-way transmission of information.</p>
<p>Ploum wrote about how he views the <a href="https://ploum.net/2025-12-15-communication-entertainment.html">ActivityPub protocol as a conversation</a>, and as such servers should not filter posts based on <a href="https://ploum.net/2025-12-04-pixelfed-against-fediverse.html">whether they have a picture</a>. Now I&rsquo;m not on Pixelfed, so I do not have a stake in this issue. However since I view Pixelfed as a microblogging platform, I tend to see it as more of a one-way transmission of information rather than soliciting a response from my friends. This view would put me in the category of folks that view AcitvityPub as a &ldquo;content consumption platform.&rdquo; Though I find that term derogative<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>.</p>
<p>Now should Pixelfed filter posts based on whether it contains a picture? I&rsquo;m not sure. But if I was on Mastodon and I specifically @&rsquo;d  someone on Pixelfed,  then I would sure hope that either they received that message or I was shown an error.</p>
<p>So if we&rsquo;re not having a conversation with these (micro-)blog posts, what are we doing? Some of us are trying to teach, keep a public journal, or share our perspectives.  Some of us don&rsquo;t even want responses<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>. Though I find that many of us do:</p>
<blockquote>
<p>Blog posts become invitations that never expire
- <a href="https://dri.es/20-years-of-blogging">Dries</a></p></blockquote>
<p>When I send a postcard to a loved one, they don&rsquo;t need to reply. However, I send it because it&rsquo;s an acknowledgement of our relationship and it&rsquo;s an invitation to reach out.</p>
<p>Similarly when I write a blog post, by default I&rsquo;m only transmitting information. However, any of you readers can choose to promote this from a transmission to an exchange. From a communication to a dialogue.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>The idea that folks are doom-scrolling and are only consuming empty calories misses how communities are  formed on these platforms &ndash; complete with their own social norms.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Every so often I get sad when I want to reply to someone, but I don&rsquo;t see an email to reach out to.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded>
      
    </item>
    
    <item>
      <title></title>
      <link></link>
      <pubDate>Sun, 11 Jan 2026 23:16:43 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<p>Wikipedia turns 25 this upcoming Thursday 🎂 🎈 🎉 </p><p><a href="https://meta.wikimedia.org/wiki/Event:Wikipedia_25_Virtual_Celebration" target="_blank" rel="nofollow noopener" translate="no"><span class="invisible">https://</span><span class="ellipsis">meta.wikimedia.org/wiki/Event:</span><span class="invisible">Wikipedia_25_Virtual_Celebration</span></a></p><p>Here&#39;s to another 25 years!</p>]]></description>
      <content:encoded><![CDATA[<p>Wikipedia turns 25 this upcoming Thursday 🎂 🎈 🎉 </p><p><a href="https://meta.wikimedia.org/wiki/Event:Wikipedia_25_Virtual_Celebration" target="_blank" rel="nofollow noopener" translate="no"><span class="invisible">https://</span><span class="ellipsis">meta.wikimedia.org/wiki/Event:</span><span class="invisible">Wikipedia_25_Virtual_Celebration</span></a></p><p>Here&#39;s to another 25 years!</p>]]></content:encoded>
      
    </item>
    
    <item>
      <title>Simulating consumption</title>
      <link></link>
      <pubDate>Sun, 11 Jan 2026 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<p>Steven brings up an interesting trend that simulation games tend to simulate how we consume an experience rather than perform it. The strongest example he gives here is sports simulation games.</p>
<p>Part of it, I think is that watching sports is already a hobby. So why not make a game that engages people in their hobby more.</p>
<p>Another reason I think is that first-person perspective games are rare in general. Even in the shooter genre, we see third-person perspectives.</p>]]></description>
      <content:encoded><![CDATA[<p>Steven brings up an interesting trend that simulation games tend to simulate how we consume an experience rather than perform it. The strongest example he gives here is sports simulation games.</p>
<p>Part of it, I think is that watching sports is already a hobby. So why not make a game that engages people in their hobby more.</p>
<p>Another reason I think is that first-person perspective games are rare in general. Even in the shooter genre, we see third-person perspectives.</p>
<p>Funny enough, vehicle simulation games such as Truck Simulator and Flight Simulator are closer to the actual experience. I remember when I first played the former I was frustrated that I couldn&rsquo;t park an 18-wheeler easily ;D</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Not the Elves</title>
      <link></link>
      <pubDate>Sat, 10 Jan 2026 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<p>This is the first time I&rsquo;ve heard of a service where you have a Christmas tree at your house for a month out of the year and it spends the rest of its time at a farm. Very interesting!</p>]]></description>
      <content:encoded><![CDATA[<p>This is the first time I&rsquo;ve heard of a service where you have a Christmas tree at your house for a month out of the year and it spends the rest of its time at a farm. Very interesting!</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>WHOIS is dead, long live RDAP</title>
      <link></link>
      <pubDate>Wed, 07 Jan 2026 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<p>Public service announcement that we should be using RDAP now instead of WHOIS.</p>]]></description>
      <content:encoded><![CDATA[<p>Public service announcement that we should be using RDAP now instead of WHOIS.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>It’s Uncomfortable To Sit With “I Don’t Know”</title>
      <link></link>
      <pubDate>Tue, 06 Jan 2026 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<p>Overall as humans, <a href="https://en.wikipedia.org/wiki/Confidence#Perceptions_of_self-confidence_in_others">we like self-confident people</a>. I&rsquo;m not an expert in large language models (LLMs), but I can only imagine that during fine-tuning they were optimized to exhibit confident behavior.</p>
<p>What this means for us is that we need to be skeptical and take responses with a grain of salt. Though that isn&rsquo;t a new issue. What&rsquo;s more interesting to me is how we treat these LLMs similar to <a href="https://en.wikipedia.org/wiki/Gell-Mann_amnesia_effect">how we treat news outlets</a>: trusting that coverage in an unfamiliar area is correct even if we don&rsquo;t trust it&rsquo;s reporting in an area that we&rsquo;re familiar with.</p>]]></description>
      <content:encoded><![CDATA[<p>Overall as humans, <a href="https://en.wikipedia.org/wiki/Confidence#Perceptions_of_self-confidence_in_others">we like self-confident people</a>. I&rsquo;m not an expert in large language models (LLMs), but I can only imagine that during fine-tuning they were optimized to exhibit confident behavior.</p>
<p>What this means for us is that we need to be skeptical and take responses with a grain of salt. Though that isn&rsquo;t a new issue. What&rsquo;s more interesting to me is how we treat these LLMs similar to <a href="https://en.wikipedia.org/wiki/Gell-Mann_amnesia_effect">how we treat news outlets</a>: trusting that coverage in an unfamiliar area is correct even if we don&rsquo;t trust it&rsquo;s reporting in an area that we&rsquo;re familiar with.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>How does a smoke alarm work?</title>
      <link></link>
      <pubDate>Sun, 04 Jan 2026 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<p>Wow, I didn&rsquo;t think that a smoke detector is primarily a photo sensor! This post shows a cool picture-by-picture breakdown of one that was in Andreas&rsquo; house. Simple, but gets the job done.</p>]]></description>
      <content:encoded><![CDATA[<p>Wow, I didn&rsquo;t think that a smoke detector is primarily a photo sensor! This post shows a cool picture-by-picture breakdown of one that was in Andreas&rsquo; house. Simple, but gets the job done.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>NarraScope is open for submissions</title>
      <link></link>
      <pubDate>Sun, 04 Jan 2026 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<p>It&rsquo;s super cool to see an interactive narrative conference happening in my neck of the woods! If you&rsquo;re interested, definitely check it out.</p>]]></description>
      <content:encoded><![CDATA[<p>It&rsquo;s super cool to see an interactive narrative conference happening in my neck of the woods! If you&rsquo;re interested, definitely check it out.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Reasons to Love the Field of Programming Languages</title>
      <link></link>
      <pubDate>Sun, 04 Jan 2026 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<p>Great collection of human, mathematical, and pragmatic reasons to love programming languages.</p>]]></description>
      <content:encoded><![CDATA[<p>Great collection of human, mathematical, and pragmatic reasons to love programming languages.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Backing up my data with Restic, Btrfs, and MinIO</title>
      <link>https://brandonrozek.com/blog/backups-with-restic-btrfs/</link>
      <pubDate>Tue, 30 Dec 2025 11:51:07 -0500</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid>https://brandonrozek.com/blog/backups-with-restic-btrfs/</guid>
      <description><![CDATA[<p>For the past year, I settled on a backup strategy that serves my needs. In this post, I&rsquo;ll share the properties I look for in a backup solution and how my current solution addresses them. As always, if you have any suggestions or improvements, feel free to get in touch.</p>
<p>The first step before talking technology is to identify exactly what data we want to backup. In my homelab, I rely on <a href="https://immich.app/">Immich</a> for photo storage, <a href="https://www.navidrome.org/">Navidrome</a> for music streaming, databases for my website, and other personal documents. This amounts to a little over 250 GB of data that would be very difficult for me to replace if it was lost.</p>]]></description>
      <content:encoded><![CDATA[<p>For the past year, I settled on a backup strategy that serves my needs. In this post, I&rsquo;ll share the properties I look for in a backup solution and how my current solution addresses them. As always, if you have any suggestions or improvements, feel free to get in touch.</p>
<p>The first step before talking technology is to identify exactly what data we want to backup. In my homelab, I rely on <a href="https://immich.app/">Immich</a> for photo storage, <a href="https://www.navidrome.org/">Navidrome</a> for music streaming, databases for my website, and other personal documents. This amounts to a little over 250 GB of data that would be very difficult for me to replace if it was lost.</p>
<p>Not all my data lives on that one server though. I also have a storage VPS which runs <a href="https://nextcloud.com/">Nextcloud</a> and <a href="https://hedgedoc.org/">Hedgedoc</a>. Both of those services combined have less than 100 GB of data, but just like the homelab, that data is precious. The storage VPS has a total capacity of 1.5 TB.</p>
<p>The <a href="https://www.backblaze.com/blog/the-3-2-1-backup-strategy/">3-2-1 backup strategy</a> coined by Peter Krogh suggests that we should have three copies of our data, stored on two different media, and with one being off-site.</p>
<p>Both of my servers have enough storage capacity to hold a copy of all of my data. For my third copy, I decided to rely on <a href="https://www.backblaze.com/cloud-storage">Backblaze b2</a>. This is a S3 object storage service that charges low rates for what I store and the number of transactions I make.</p>
<p>My homelab sits in my house, the storage VPS sits in Montreal, and Backblaze stores my data in Phoenix. Therefore, I have <strong>3</strong> copies of my data and it&rsquo;s stored in more than <strong>1</strong> location.</p>
<p>In order to address having at least <strong>2</strong> different storage media, I backup my data using two different approaches: Restic and Btrfs Snapshots. I don&rsquo;t currently have enough data such that hot online storage is cost-prohibitive.</p>
<h3 id="restic">Restic</h3>
<p><a href="https://restic.net/">Restic</a> is a modern and open-source backup software that both deduplicates and encrypts file contents at the blob level. In combination with a metadata local cache, this makes this Restic snappy and efficient to use.</p>
<p>It supports a variety of backup targets:</p>
<ul>
<li>Local filesystem</li>
<li>SFTP</li>
<li>REST server</li>
<li>S3</li>
<li>and <a href="https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#">several others</a>, including those implemented by <a href="https://rclone.org/">Rclone</a>!</li>
</ul>
<p>While I could use the variety of backup targets to increase the number of approaches my data is backed up, to me, this does not feel sufficiently different enough to warrent the additional complexity. Therefore, I&rsquo;ll use one backup target for all three locations.</p>
<p>Since Backblaze b2 only supports the S3 protocol, this means then that my choice has been made for me. However even if it wasn&rsquo;t forced upon me, I would still pick that option. Designed by Amazon for their storage service, the S3 protocol is built with scale in mind and supports concurrent multipart file uploads. Additionally unlike SFTP, S3 separates its accounts from that of the server. There are ways to restrict SSH clients, however, it is not the default behavior. In S3, by default a user cannot do anything.</p>
<p>Thus, after choosing S3, I need to set up a S3 server on both my storage VPS and my homelab. I landed on MinIO for its ease of setup and longevity, however, any of the other solutions would work as well.</p>
<p>Since, we&rsquo;re storing copies of the data on all the servers, one idea is to set it up in a cluster configuration. However, since I&rsquo;m not managing the S3 server hosted by Backblaze, I wouldn&rsquo;t be able to include that in the cluster. Generally, it is not recommended to run a cluster with only two nodes. If we do, it can lead to what&rsquo;s called a <a href="https://en.wikipedia.org/wiki/Split-brain_(computing)"><em>split brain problem</em></a> where both nodes are unable to communicate with each other and think that they are the leader node.</p>
<p>To avoid this, we can run both MinIO instances inpendently and have Restic separately send the data to all servers.</p>
<h4 id="minio-setup">MinIO Setup</h4>
<p>If you already have a bucket setup and configured with the appropriate permissions, then feel free to skip this section.</p>
<p>The following code examples assume that <a href="https://min.io/docs/minio/container/index.html">MinIO is installed</a> and the <code>mc</code> client is available with the root credentials to each server (alias) set. These general steps can also be achieved using the management UI. For simplicity, I&rsquo;ll show how to do this with respect to one server <code>homelab</code>. However, we&rsquo;ll need to do this for every MinIO instance.</p>
<p>First, <a href="https://min.io/docs/minio/linux/reference/minio-mc/mc-mb.html">create a bucket</a> which will hold our backup data.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># alias_of_server/bucket_name</span>
</span></span><span style="display:flex;"><span>mc mb homelab/backups
</span></span></code></pre></div><p>Afterwards, create a <a href="https://min.io/docs/minio/linux/administration/identity-access-management/minio-user-management.html">backup user</a>. We&rsquo;ll need to specify the <code>ACCESS_KEY</code> and <code>SECRET_KEY</code> which corresponds to the username and password respectively. Store these as we&rsquo;ll later need to use them.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>export ACCESS_KEY<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;my_awesome_homelab_user&#34;</span>
</span></span><span style="display:flex;"><span>export SECRET_KEY<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;SUPER_SECURE_SECRET_KEY&#34;</span>
</span></span><span style="display:flex;"><span>mc admin user add homelab $ACCESSKEY $SECRETKEY
</span></span></code></pre></div><p><em>Note:</em> For a bit more security, I like to create a different access key and secret key for the other servers.</p>
<p>By default, our user cannot cannot do anything. Let&rsquo;s make it so that they have access to our <code>backups</code> bucket we created earlier. To do that, we&rsquo;ll need to create a <a href="https://min.io/docs/minio/linux/administration/identity-access-management/policy-based-access-control.html">policy</a> which allows the user to perform operations on that bucket.</p>
<p>We&rsquo;ll have to create a JSON file and follow the AWS IAM format.</p>
<pre tabindex="0"><code class="language-iam" data-lang="iam">{
 &#34;Version&#34;: &#34;2012-10-17&#34;,
 &#34;Statement&#34;: [
  {
   &#34;Effect&#34;: &#34;Allow&#34;,
   &#34;Action&#34;: [
    &#34;s3:ListBucket&#34;,
    &#34;s3:PutObject&#34;,
    &#34;s3:DeleteObject&#34;,
    &#34;s3:GetObject&#34;
   ],
   &#34;Resource&#34;: [
    &#34;arn:aws:s3:::backups/*&#34;,
    &#34;arn:aws:s3:::backups&#34;
   ]
  }
 ]
}
</code></pre><p>Assume that we saved the prior JSON at a location specified by the environmental variable <code>$POLICY_PATH</code>, then we can create the policy through the following:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>export POLICY_NAME<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;backup-policy&#34;</span>
</span></span><span style="display:flex;"><span>mc admin policy create homelab $POLICY_NAME $POLICY_PATH
</span></span></code></pre></div><p>After creating the policy, we need to assign it to our backup user.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>mc admin policy attach homelab $POLICY_NAME --user $ACCESS_KEY
</span></span></code></pre></div><h4 id="nginx-and-initializing-restic">Nginx and Initializing Restic</h4>
<p>With our bucket and user configured, now let&rsquo;s discuss how we&rsquo;ll perform our backups. Our bucket will have the following directory structure:</p>
<pre tabindex="0"><code>backups/
  homelab/
  vps/
</code></pre><p>Each folder will contain a restic repository. We separate them out instead of having one giant restic repository, so that we don&rsquo;t have to worry about multiple servers competing for a lock.</p>
<p>Instead of communicating over HTTP, we want a secure way to access our buckets from outside the server. For this, I use <a href="https://min.io/docs/minio/linux/integrations/setup-nginx-proxy-with-minio.html">nginx to proxy the traffic over to MinIO</a>. I configure <a href="https://letsencrypt.org/getting-started/">Certbot with LetsEncrypt</a> so that the connection can be secureted with HTTPS/TLS. In addition to the instructions listed on the MinIO page, I also restrict the IPs which are allowed to connect. For example, to only allow traffic from within your Wireguard network (ex subnet: <code>10.10.10.1/24</code>) then you can put the following within the <code>location</code> block of the nginx config.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#66d9ef">allow</span> <span style="color:#ae81ff">10</span><span style="color:#e6db74">.10.10.1/24</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">deny</span> <span style="color:#e6db74">all</span>; <span style="color:#66d9ef">//</span> <span style="color:#e6db74">Denies</span> <span style="color:#e6db74">all</span> <span style="color:#e6db74">other</span> <span style="color:#e6db74">traffic</span>
</span></span></code></pre></div><p>After securing the entryway to our S3 server, we&rsquo;re ready to use Restic. Before jumping in, you might want to <a href="https://restic.readthedocs.io/en/stable/080_examples.html#full-backup-without-root">create a dedicated account</a> so that we don&rsquo;t have to use the <code>root</code>  user.</p>
<p>Let&rsquo;s initialize each server&rsquo;s respective repository. Since our backups will be encrypted, we need to come up with another password and put it within the <code>RESTIC_PASSWORD</code> environmental variable. Save this password in a safe place, since we will not be able to decrypt the backup without it.</p>
<pre tabindex="0"><code>export RESTIC_REPOSITORY=s3:https://&lt;domain-of-s3-server&gt;/backups/homelab
export AWS_ACCESS_KEY_ID=$ACCESS_KEY
export AWS_SECRET_ACCESS_KEY=$SECRET_ACCESS_KEY
export RESTIC_PASSWORD=&#34;RESTIC_SUPER_SECURE_PASSWORD&#34;

restic init
</code></pre><p>From here, create a backup script at <code>/usr/local/bin/backup.sh</code>. This is where we specify which folders to backup.</p>
<p>Example:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/sh
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span>set -o errexit
</span></span><span style="display:flex;"><span>set -o nounset
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$EUID<span style="color:#e6db74">&#34;</span> -ne <span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>id -u restic<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">then</span> echo <span style="color:#e6db74">&#34;Please run as restic&#34;</span>
</span></span><span style="display:flex;"><span>  exit
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Usage: rbackup [message] [restic-tag] [backup-dir]</span>
</span></span><span style="display:flex;"><span>rbackup <span style="color:#f92672">()</span> <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  echo <span style="color:#e6db74">&#34;</span>$1<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>  restic backup <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    --tag <span style="color:#e6db74">&#34;</span>$2<span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#e6db74">&#34;</span>$3<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## BACKUP TO CLOUD</span>
</span></span><span style="display:flex;"><span>export RESTIC_REPOSITORY<span style="color:#f92672">=</span>s3:https://&lt;domain-of-cloud-server&gt;/backups/homelab
</span></span><span style="display:flex;"><span>export AWS_ACCESS_KEY_ID<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CLOUD_ACCESS_KEY&#34;</span>
</span></span><span style="display:flex;"><span>export AWS_SECRET_ACCESS_KEY<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CLOUD_SECRET_ACCESS_KEY&#34;</span>
</span></span><span style="display:flex;"><span>export RESTIC_PASSWORD<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;RESTIC_SUPER_SECURE_PASSWORD&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>rbackup <span style="color:#e6db74">&#34;Backing up documents&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  Documents <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  /home/user/Documents
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Backup other great directories...</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## BACKUP TO BACKBLAZE</span>
</span></span><span style="display:flex;"><span>export RESTIC_REPOSITORY<span style="color:#f92672">=</span>s3:https://&lt;domain-of-backblaze-server&gt;/backups/homelab
</span></span><span style="display:flex;"><span>export AWS_ACCESS_KEY_ID<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;BACKBLAZE_ACCESS_KEY&#34;</span>
</span></span><span style="display:flex;"><span>export AWS_SECRET_ACCESS_KEY<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;BACKBLAZE_SECRET_ACCESS_KEY&#34;</span>
</span></span><span style="display:flex;"><span>export RESTIC_PASSWORD<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;RESTIC_SUPER_SECURE_PASSWORD&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>rbackup <span style="color:#e6db74">&#34;Backing up documents&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  Documents <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  /home/user/Documents
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Backup other great directories...</span>
</span></span></code></pre></div><p>Unless you have unlimited storage, you probably want to prune old backups according to some schedule. In the same script I have the following function:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>prune <span style="color:#f92672">()</span> <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  echo <span style="color:#e6db74">&#34;Pruning old snapshots&#34;</span>
</span></span><span style="display:flex;"><span>  restic unlock
</span></span><span style="display:flex;"><span>  restic forget <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    --group-by <span style="color:#e6db74">&#34;tags&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    --keep-daily N_DAYS <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    --keep-weekly N_WEEKS <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    --keep-monthly N_MONTHS <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    --prune
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span></code></pre></div><p>Replace <code>N_*</code> to your liking. Restic will then ensure that it keeps enough snapshots such that those time-based rules are satisfied.</p>
<p>With the script written, we can then setup a systemd service and timer so that it runs daily. Write the following to <code>/etc/systemd/system/restic-backup.service</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#66d9ef">[Unit]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Description</span><span style="color:#f92672">=</span><span style="color:#e6db74">Executes backup script</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Requires</span><span style="color:#f92672">=</span><span style="color:#e6db74">network-online.target</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Wants</span><span style="color:#f92672">=</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[Service]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">User</span><span style="color:#f92672">=</span><span style="color:#e6db74">restic</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Group</span><span style="color:#f92672">=</span><span style="color:#e6db74">restic</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Type</span><span style="color:#f92672">=</span><span style="color:#e6db74">oneshot</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">ExecStart</span><span style="color:#f92672">=</span><span style="color:#e6db74">/usr/local/bin/backup.sh</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Environment</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;HOME=/home/restic&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[Install]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">WantedBy</span><span style="color:#f92672">=</span><span style="color:#e6db74">multi-user.target</span>
</span></span></code></pre></div><p>Write the timer to <code>/etc/systemd/system/restic-backup.timer</code></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#66d9ef">[Timer]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">OnCalendar</span><span style="color:#f92672">=</span><span style="color:#e6db74">daily</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Persistent</span><span style="color:#f92672">=</span><span style="color:#e6db74">true</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[Install]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">WantedBy</span><span style="color:#f92672">=</span><span style="color:#e6db74">timers.target</span>
</span></span></code></pre></div><h3 id="btrfs-snapshots">Btrfs snapshots</h3>
<p>Both my servers run Btrfs as the underlying filesystem. One cool feature is that Btrfs can create <em>snapshots</em>. These are immutable point-in-time views of a given subvolume. As such, we&rsquo;ll need to create a subvolume that will contain the directories we want.</p>
<p>If we&rsquo;re not starting from scratch, and instead want to create a subvolume from an existing folder, then I recommend performing the following steps from <a href="https://www.reddit.com/r/btrfs/comments/198hbod/converting_directory_into_subvolume/">this reddit thread</a>:</p>
<pre tabindex="0"><code>mv folder folder_backup
btrfs subvolume create folder
cp --archive --one-file-system --reflink=always folder_backup/. folder
</code></pre><p>From here, we can create our snapshots. Like with our Restic setup, I like having some daily, weekly, and monthly backups available. I&rsquo;ll store these snapshots in <code>/snapshots</code>, but you&rsquo;re free to change the location. Here&rsquo;s what it looks like on my machine:</p>
<pre tabindex="0"><code>/snapshots/
  daily
    20251201
      /home/user/Documents
      ...
      /home/user/Music
    ...
    20251227
  monthly
    ...
  weekly
    ...
</code></pre><p>Since these are backup snapshots, we want it to be <em>readonly</em>. Here is what it looks like to create a daily snapshot for our documents subvolume:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>SNAPSHOT_PATH<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/snapshots/daily/</span><span style="color:#66d9ef">$(</span>date +<span style="color:#e6db74">&#39;%Y%m%d&#39;</span><span style="color:#66d9ef">)</span><span style="color:#e6db74">/home/user/Documents&#34;</span>
</span></span><span style="display:flex;"><span>mkdir -p <span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>dirname $SNAPSHOT_PATH<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>btrfs subvolume snapshot -r /home/user/Documents <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>SNAPSHOT_PATH<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span></code></pre></div><p>We used the date in the folder name so that we can easily detect the oldest snapshots for deletion.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>OLDEST_SNAPSHOT<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>ls <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>SNAPSHOT_DIR<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> | sort | head -n 1<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> SUBVOLUME_PATH in <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>SUBVOLUMES[@]<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>    SNAPSHOT_PATH<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>SNAPSHOT_DIR<span style="color:#e6db74">}</span><span style="color:#e6db74">/</span><span style="color:#e6db74">${</span>OLDEST_SNAPSHOT<span style="color:#e6db74">}${</span>SUBVOLUME_PATH<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>    btrfs subvolume delete -c <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>SNAPSHOT_PATH<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>    
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">done</span>
</span></span><span style="display:flex;"><span>rm -rf <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>SNAPSHOT_DIR<span style="color:#e6db74">}</span><span style="color:#e6db74">/</span><span style="color:#e6db74">${</span>OLDEST_SNAPSHOT<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span></code></pre></div><p>I put together a script which takes as input the environment variable <code>SNAPSHOT_DIR</code> and handles creating and pruning snapshots. To get these snapshots at different time intervals, we use different systemd timers and change that input variable. Unlike with our restic setup, this will keep the same $N$ number of snapshots for each of our time intervals.  We copy this script to <code>/usr/local/bin/localbtrbak.sh</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span>set -o nounset
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>show_usage<span style="color:#f92672">()</span> <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>    echo <span style="color:#e6db74">&#34;Usage: localbtrback&#34;</span>
</span></span><span style="display:flex;"><span>    exit <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Check argument count</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$#<span style="color:#e6db74">&#34;</span> -ne <span style="color:#ae81ff">0</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    show_usage
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$EUID<span style="color:#e6db74">&#34;</span> -ne <span style="color:#ae81ff">0</span> <span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">then</span> echo <span style="color:#e6db74">&#34;Please run as root&#34;</span>
</span></span><span style="display:flex;"><span>    exit
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> -z <span style="color:#e6db74">&#34;</span>$SNAPSHOT_DIR<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">then</span> echo <span style="color:#e6db74">&#34;SNAPSHOT_DIR not defined&#34;</span>
</span></span><span style="display:flex;"><span>    exit
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># EDIT TO POINT TO YOUR SUBVOLUMES</span>
</span></span><span style="display:flex;"><span>SUBVOLUMES<span style="color:#f92672">=(</span><span style="color:#e6db74">&#34;/home/user/Documents&#34;</span>  <span style="color:#e6db74">&#34;/home/user/Music&#34;</span><span style="color:#f92672">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> SUBVOLUME_PATH in <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>SUBVOLUMES[@]<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>    SNAPSHOT_PATH<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>SNAPSHOT_DIR<span style="color:#e6db74">}</span><span style="color:#e6db74">/</span><span style="color:#66d9ef">$(</span>date +<span style="color:#e6db74">&#39;%Y%m%d&#39;</span><span style="color:#66d9ef">)</span><span style="color:#e6db74">${</span>SUBVOLUME_PATH<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># Create folder if not already exists</span>
</span></span><span style="display:flex;"><span>    mkdir -p <span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>dirname $SNAPSHOT_PATH<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># Create the readonly snapshot</span>
</span></span><span style="display:flex;"><span>    btrfs subvolume snapshot -r <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>SUBVOLUME_PATH<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>SNAPSHOT_PATH<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">done</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Calculate the number of snapshots</span>
</span></span><span style="display:flex;"><span>COUNT<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>ls <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>SNAPSHOT_DIR<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> | wc -l<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>COUNT<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> -gt <span style="color:#ae81ff">3</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    OLDEST_SNAPSHOT<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>ls <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>SNAPSHOT_DIR<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> | sort | head -n 1<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> SUBVOLUME_PATH in <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>SUBVOLUMES[@]<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>        SNAPSHOT_PATH<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>SNAPSHOT_DIR<span style="color:#e6db74">}</span><span style="color:#e6db74">/</span><span style="color:#e6db74">${</span>OLDEST_SNAPSHOT<span style="color:#e6db74">}${</span>SUBVOLUME_PATH<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>        btrfs subvolume delete -c <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>SNAPSHOT_PATH<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>    
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">done</span>
</span></span><span style="display:flex;"><span>    rm -rf <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>SNAPSHOT_DIR<span style="color:#e6db74">}</span><span style="color:#e6db74">/</span><span style="color:#e6db74">${</span>OLDEST_SNAPSHOT<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span></code></pre></div><p>For our systemd service in <code>/etc/systemd/system/btrlocalback@.service</code></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#66d9ef">[Unit]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Description</span><span style="color:#f92672">=</span><span style="color:#e6db74">Create a local BTRFS snapshot</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[Service]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Type</span><span style="color:#f92672">=</span><span style="color:#e6db74">oneshot</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Environment</span><span style="color:#f92672">=</span><span style="color:#e6db74">SNAPSHOT_DIR=&#34;/snapshots/%i&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">ExecStart</span><span style="color:#f92672">=</span><span style="color:#e6db74">/usr/local/bin/localbtrbak.sh</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[Install]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">WantedBy</span><span style="color:#f92672">=</span><span style="color:#e6db74">multi-user.target</span>
</span></span></code></pre></div><p>An example daily timer stored in <code>/etc/systemd/system/btrlocalbak@daily.timer</code></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#66d9ef">[Unit]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Description</span><span style="color:#f92672">=</span><span style="color:#e6db74">Create a daily local BTRFS snapshot</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[Timer]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">OnCalendar</span><span style="color:#f92672">=</span><span style="color:#e6db74">daily</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Persistent</span><span style="color:#f92672">=</span><span style="color:#e6db74">true</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[Install]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">WantedBy</span><span style="color:#f92672">=</span><span style="color:#e6db74">timers.target</span>
</span></span></code></pre></div><h3 id="conclusion">Conclusion</h3>
<p>Here is a visualization of the three servers and where the data gets backed up to:</p>
<p><img src="/files/images/blog/BackupSetup2025.svg" alt="Diagram of the backup setup I described"></p>
<p>The bucket image in the diagram denotes the S3 storage, while the cylinder database icon denotes the Btrfs snapshots. Pictorially, this also shows us how the 3-2-1 rule is satisfied.</p>
<ul>
<li>Three outgoing arrows on the two servers means that we have three copies of the data</li>
<li>The two icons on each of the servers show that we&rsquo;re backing it up in two different ways</li>
<li>I have more than one off-site backup since all three boxes are in different locations.</li>
</ul>
<p>If I accidentally delete a file, the Btrfs setup is useful since I can quickly access the old version at <code>/snapshots/daily/&lt;yesterday&gt;/path</code>. However, if my entire server goes down, then I can use one of the restic backups in either server to restore.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Friendly Little Wrapper Types</title>
      <link></link>
      <pubDate>Sun, 28 Dec 2025 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<p>I&rsquo;m all for introducing opaque wrapper types. It also makes it easier to identify the parts of the codebase we need to change when we want our user id to wrap over something else.</p>]]></description>
      <content:encoded><![CDATA[<p>I&rsquo;m all for introducing opaque wrapper types. It also makes it easier to identify the parts of the codebase we need to change when we want our user id to wrap over something else.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>My website is snowed in</title>
      <link></link>
      <pubDate>Sun, 28 Dec 2025 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<p>Oh no! I hope the solar panel has some windshield wipers to clear off the snow. At this time of writing, his <a href="https://dri.es/sensors/solar">solar dashboard</a> shows that the battery is at 11%.</p>]]></description>
      <content:encoded><![CDATA[<p>Oh no! I hope the solar panel has some windshield wipers to clear off the snow. At this time of writing, his <a href="https://dri.es/sensors/solar">solar dashboard</a> shows that the battery is at 11%.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Writing down (and searching through) every UUID</title>
      <link></link>
      <pubDate>Sun, 28 Dec 2025 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<p>This is such a fun idea! I also love Nolen&rsquo;s idea of designing a bijective function between the natural numbers and the UUIDs themselves. This allows for a seemingly random order, while also staying complete and quickly searchable.</p>]]></description>
      <content:encoded><![CDATA[<p>This is such a fun idea! I also love Nolen&rsquo;s idea of designing a bijective function between the natural numbers and the UUIDs themselves. This allows for a seemingly random order, while also staying complete and quickly searchable.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Drawing Truchet tiles in SVG</title>
      <link></link>
      <pubDate>Sat, 27 Dec 2025 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<p>These truchet tiles look very cool and funky. Another awesome example of algorithmically generated art.</p>]]></description>
      <content:encoded><![CDATA[<p>These truchet tiles look very cool and funky. Another awesome example of algorithmically generated art.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Grow slowly, stay small</title>
      <link></link>
      <pubDate>Sat, 27 Dec 2025 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<p>I really enjoyed the fisherman and businessman parable. To me, the journey of life is important and I believe that we should strive to appreciate each year. Otherwise, we&rsquo;ll get to the destination and realize it&rsquo;s not all we imagined it to be.</p>]]></description>
      <content:encoded><![CDATA[<p>I really enjoyed the fisherman and businessman parable. To me, the journey of life is important and I believe that we should strive to appreciate each year. Otherwise, we&rsquo;ll get to the destination and realize it&rsquo;s not all we imagined it to be.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>One weird trick for cheaper physical Switch 2 games?</title>
      <link></link>
      <pubDate>Sat, 27 Dec 2025 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<p>Buying a physical game and selling the box is such a funny strategy! Even if that strategy tanks with adoption, at least it&rsquo;d encourage physical copies more. I worry about the day where everything becomes digital and tied to your account.</p>]]></description>
      <content:encoded><![CDATA[<p>Buying a physical game and selling the box is such a funny strategy! Even if that strategy tanks with adoption, at least it&rsquo;d encourage physical copies more. I worry about the day where everything becomes digital and tied to your account.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>After 75 Miles of Running</title>
      <link>https://brandonrozek.com/blog/running-75-miles/</link>
      <pubDate>Fri, 26 Dec 2025 09:55:41 -0500</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid>https://brandonrozek.com/blog/running-75-miles/</guid>
      <description><![CDATA[<p>Back in August, I started running regularly and recording my sessions. I recently completed a total of 75 miles of running across all my sessions. In this post, I&rsquo;ll reflect on my journey so far and what I&rsquo;ve learned.</p>
<h2 id="the-beginning">The Beginning</h2>
<p>Over the summer, I lived and worked in Austin, Texas. For the prior months of the year, I&rsquo;ve lived a fairly sedimentary lifestyle. I enjoy these summers where I am forced to walk more places and <a href="/blog/exploring-via-public-transit/">take public transit</a> since I don&rsquo;t have access to a vehicle.</p>]]></description>
      <content:encoded><![CDATA[<p>Back in August, I started running regularly and recording my sessions. I recently completed a total of 75 miles of running across all my sessions. In this post, I&rsquo;ll reflect on my journey so far and what I&rsquo;ve learned.</p>
<h2 id="the-beginning">The Beginning</h2>
<p>Over the summer, I lived and worked in Austin, Texas. For the prior months of the year, I&rsquo;ve lived a fairly sedimentary lifestyle. I enjoy these summers where I am forced to walk more places and <a href="/blog/exploring-via-public-transit/">take public transit</a> since I don&rsquo;t have access to a vehicle.</p>
<p>Two of my coworkers were really into running and one of them even regularly ran a few miles outside in the <a href="/blog/embrace-the-heat/">95+ degree Fahrenheit weather</a>. This didn&rsquo;t immediately convince me to start myself, but it definitely planted the seed. Towards the end of the summer, I asked myself &ldquo;What&rsquo;s stopping me from running?&rdquo; and took this as an opportunity to prove something to myself.</p>
<blockquote>
<p>What do I need to get started?</p></blockquote>
<p>At the beginning, I didn&rsquo;t end up buying anything new. In Austin, I wore some sneakers from <a href="https://www.keenfootwear.com">Keen footwear</a> which I accidentally soaked a few times in sudden downpours. I also wear a Fitbit watch that has heart rate monitoring built in.</p>
<p>When I returned to New York, I ended up buying a <a href="https://www.brooksrunning.com/en_us">Brooks running shoe</a>. Shoes designed for running are usually lighter and have shock absorption. This makes for a more pleasant workout.</p>
<blockquote>
<p>Did you use a training plan?</p></blockquote>
<p>Initially, I used the <a href="https://www.nhs.uk/better-health/get-active/get-running-with-couch-to-5k/couch-to-5k-running-plan/">couch to 5k running plan</a> developed by the UK&rsquo;s National Health Service (NHS). After completing that plan, I charted out my own path, with most of my sessions consisting of 30 minutes of running surrounded by 5 minutes of walking.</p>
<p>When I returned to New York, I saw that my city was hosting a <a href="https://troyturkeytrot.com/">turkey trot</a> on Thanksgiving. That was a perfect amount of time to follow the program and see how things turn out!</p>
<h2 id="training-arc">Training Arc</h2>
<p>With a race in mind, I had to make sure to not skimp on my training. Initially, I had an every other day schedule. Though as I adjusted back to my new schedule, this has become a little chaotic.</p>
<h3 id="running-inside-vs-outside">Running Inside vs Outside</h3>
<p>The transition between summer and fall in New York state is lovely. It makes for a great time to run outside.  However, as the months rolled by it started getting colder and colder. Now that we&rsquo;re in the midst of winter, I&rsquo;ve mostly abandoned running outside.</p>
<p>Some people swear by running outdoors versus using a treadmill. I&rsquo;m fine with either, but there are definitely trade-offs.</p>
<p>With outdoor running there is much more visual stimuli. Instead of staring at a wall, the scenery changes as you move around. This is a huge pro when it comes to staving off boredom; which I find very important for a successful workout. The uneven ground also prevents repetitive strain injuries.</p>
<p>On the other hand, it&rsquo;s very easy to keep pace with a treadmill. Another benefit is that you don&rsquo;t have to plan out a route ahead of time. Though, I find it imperative to have some sort of distraction ready for when I do my treadmill runs. Initially I turned to watching YouTube videos, but I noticed that my posture would then suffer. I find podcasts to be a nice compromise.</p>
<h3 id="run-slowly">Run Slowly</h3>
<p>I&rsquo;m far from an expert in this area, but it&rsquo;s generally recommended to keep your <a href="https://en.wikipedia.org/wiki/Long_slow_distance">heart rate low</a> when you&rsquo;re running. This helps improve your cardiovascular fitness over the long run. An informal test is to see if you can carry a conversation while running.</p>
<p>Formally, it&rsquo;s recommended to stay in &ldquo;Zone 2&rdquo; while running which is about 60-70% of your max heart rate.  We can use the Bruce protocol to find out our max heart rate, or for a much simpler approximation, we can use the Tanaka, Monahan, &amp; Seals (2001) formula.
$$
hr_{max} = 208 - (0.7 * age)
$$
That means for a 30 year old their zone 2 heart rate lies between 112 and 130.</p>
<p>In Zone 2, the body can still supply energy using fat reserves and clear up the lactate before it builds up. It&rsquo;s easy when running to go past this heart rate. The best way I&rsquo;ve found to keeping it in zone 2 is to run <em>really slow</em>. Often much slower than you think you should be running. When I was first starting out, I wasn&rsquo;t going much faster than walking pace.</p>
<h3 id="running-with-others">Running with others</h3>
<p>A friend recently got me to attend the local run club in the city. At first, I was nervous about joining since I thought it would only be full of people who ran their whole life. It turns out that it had people at all different points in the running journey. I joined once a week until it started regularly staying below freezing. I hope to attend again next year.</p>
<p>Either way, running can be a great excuse to get together with others!</p>
<h2 id="the-turkey-trot">The Turkey Trot</h2>
<p>We then arrive at the big day! I showed up around thirty minutes before the event started. To give an idea of the scale, the Troy turkey trot had over 4000 finishers for the 5k.</p>
<p>At the beginning of the race, everyone lined up by flags marking different paces. I timed myself at a nearby track, and my last 5k was 35 minutes. Feeling optimistic, I lined up at the 11 minute flag.</p>
<p><img src="/files/images/blog/ttslpaceflags.jpg" alt="High-up view of my starting position"></p>
<p>Bam! The sound of the gun then went off signaling the start of the race &ndash; except there was no action. Since I was so far back from the start line, it took over 5 minutes before me and the others around me got to shuffling.</p>
<p>Luckily, when it comes to timing there&rsquo;s the gun time and the net time. The net time is the time it takes once you cross the &ldquo;start line&rdquo; to hit the &ldquo;finish line&rdquo;.</p>
<p>With the majority of my running sessions featuring me, myself, and I, running with so many other people gave a huge energy boost. I let the energy get to me, and I didn&rsquo;t end up running a consistent pace at all. I likely ran a little too fast in the beginning, resulting in short walking breaks towards the end. Also I underestimated the energy it would take to weave around other people.</p>
<p>I hit the finish line at <a href="https://www.zippy-reg.com/results/live/athlete/index.php?eid=228&amp;bib=1508">34 minutes and 14 seconds</a>! It&rsquo;s pretty exciting to hit a new personal best during a race.</p>
<p>Focusing on the race, I didn&rsquo;t end up taking too many photos. Instead here&rsquo;s a blurry snapshot from the <a href="https://www.youtube.com/watch?v=fLRtp4DtcB8">finish cam</a> and a selfie of me with my participation medal.</p>
<p><img src="/files/images/blog/ttfc2025.png" alt="Screenshot of me passing the finish line with many others"></p>
<p><img src="/files/images/blog/ttselfie2025.jpg" alt="Selfie of me with the participation medal"></p>
<h2 id="onto-the-future">Onto the future!</h2>
<p>With that milestone completed, now it&rsquo;s time to think about what&rsquo;s next. Do I train for a 10k? A half-marathon? Nothing is set in stone yet, and I&rsquo;m keeping my eye out for future events.</p>
<p>I mentioned that one of my challenges with running is starving off boredom. While the race does inject a new form of energy, I still currently can&rsquo;t see running for multiple hours!</p>
<p>Beyond running itself, I find it fun to look at my logs. For example, we can take the times from the Troy turkey trot, and plot my performance on the histogram of all participant net times.</p>
<p><img src="/files/images/blog/tthistnettime2025.png" alt="Histogram of net times for all the participants"></p>
<p>Additionally, from my overall training log, I can see that I run on average 2.5 miles during my 30 minute runs.  I&rsquo;m excited to see how these metrics improve over time.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Disabling Nat Source Port Randomization on OPNsense for Gaming</title>
      <link>https://brandonrozek.com/blog/disabling-nat-source-port-randomization-opnsense/</link>
      <pubDate>Tue, 23 Dec 2025 11:16:02 -0500</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid>https://brandonrozek.com/blog/disabling-nat-source-port-randomization-opnsense/</guid>
      <description><![CDATA[<blockquote>
<p>Let&rsquo;s play Mario Party tonight!</p></blockquote>
<p>After many years of friendship, I&rsquo;ve learned that playing online multiplayer games is almost never as simple as it seems. This night was no different.  In this post, I&rsquo;ll go over what I learned setting up my Nintendo Switch for online play. Luckily, this same concept applies to the PlayStation 5 as well<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>.</p>
<p>But first, I&rsquo;ll take a detour into how peer-to-peer (P2P) gaming typically works. So, feel free to <a href="#the-solution">skip</a> down to the solution.</p>]]></description>
      <content:encoded><![CDATA[<blockquote>
<p>Let&rsquo;s play Mario Party tonight!</p></blockquote>
<p>After many years of friendship, I&rsquo;ve learned that playing online multiplayer games is almost never as simple as it seems. This night was no different.  In this post, I&rsquo;ll go over what I learned setting up my Nintendo Switch for online play. Luckily, this same concept applies to the PlayStation 5 as well<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>.</p>
<p>But first, I&rsquo;ll take a detour into how peer-to-peer (P2P) gaming typically works. So, feel free to <a href="#the-solution">skip</a> down to the solution.</p>
<h2 id="peer-to-peer-gaming">Peer-to-Peer Gaming</h2>
<p>Mario Party is a board game where you move characters around in hopes of collecting the most stars. You play as a character from the Mario franchise, and between each turn on the board is a minigame. These minigames provide the illusion that this is a skill-based game. But trust me, you can win by just tapping A.</p>
<p>Sony and Nintendo both aren&rsquo;t forthcoming with information about how their games and systems work. Therefore, I&rsquo;ll be making a number of assumptions here.</p>
<p>Nintendo owns Mario Party, and from there we can presume that they use the standard Nintendo libraries when building the game. The homebrew community over the years reverse-engineered many of these libraries and due to this we can look at the protocols they use. The Nintendo networking library, called <a href="https://github.com/kinnay/NintendoClients/wiki/Pia-Overview">Pia</a>, uses a service called NEX to match players in games. This match-making protocol includes a <a href="https://en.wikipedia.org/wiki/UDP_hole_punching">network-address-translation (nat) traversal protocol</a>.</p>
<p>For sake of simplicity, I won&rsquo;t describe the protocol in full-depth. What&rsquo;s important to know is that when both players message the server, the server knows each player&rsquo;s IP address and the source port that they used to communicate. The server will then share the other console&rsquo;s information to facilitate that direct peer-to-peer connection</p>
<p>Unfortunately, I was not able to find either official or unofficial documentation on how P2P gaming works on the PlayStation 5. From browsing around, I have the impression that this is less standardized and developers are either relying on external SDKs or developing their own libraries.</p>
<h2 id="the-problem">The Problem</h2>
<p>Opnsense, in all their great wisdom, wants to protect me from <a href="https://en.wikipedia.org/wiki/TCP_sequence_prediction_attack">TCP hijacking</a> and spoofing attacks. Therefore, by default during nat my source port is randomized.</p>
<p>Now the game consoles will not tell us directly that this is the issue. Instead it will provide a &ldquo;score&rdquo; which is supposed to establish a rough sense of how easy it will be to establish a P2P connection.</p>
<p>Nintendo Switch: Settings -&gt; Internet -&gt; Test Connection</p>
<p>PS5: Settings -&gt; Network -&gt; Test Internet Connection</p>
<p>Before applying any changes, on the switch I had a &ldquo;D&rdquo; score on on the PS5 it was &ldquo;Type 3&rdquo;. I wish I can tell you what these scores mean, but the documentation is lacking to say the least&hellip;</p>
<p>However, I was finally able to determine that nat source port randomization was the problem after stumbling upon this <a href="https://www.reddit.com/r/OPNsenseFirewall/comments/g3sx2l/tip_opnsense_and_nintendo_switch_nat_rules/">Reddit thread</a> &ndash; which now takes us to the solution:</p>
<h2 id="the-solution">The Solution</h2>
<p>We need to tell our router that our gaming devices are special, and therefore does not need the additional security measure of randomizing the source port during nat. That way when the game is establishing a P2P connection, the ports aren&rsquo;t randomized and the connection can proceed smoothly.</p>
<p>On Opnsense, we can change this setting by navigating to Firewall -&gt; NAT -&gt; Outbound.</p>
<p>Now I do want this security measure to be applied to the rest of my devices. Therefore, we&rsquo;ll be setting the mode to &ldquo;Hybrid outbound NAT rule generation&rdquo;  which allows us to specify the custom rules.</p>
<p>From there, we can add a manual rule for each game console with the following:</p>
<ul>
<li>Interface: WAN</li>
<li>TCP/IP Version: IPv4</li>
<li>Protocol: UDP</li>
<li>Source Address: Single host or network
<ul>
<li>Insert Switch/PS5 address</li>
<li>Replace netmask with 32</li>
</ul>
</li>
<li>Source Port: any</li>
<li>Destination Address: any</li>
<li>Destination Port: Any</li>
<li>Static Port: <strong>Checked</strong></li>
</ul>
<p>The last option is what tells Opnsense to not randomize the source port.</p>
<p>From there, we can save, apply our settings, and rerun our connection tests. With this change, my Switch reports a NAT type of B and PS5 reports Type 2. Good enough for online gaming :)</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Sorry Xbox folks, I do not own one so I cannot tell you if this technique works for that platform as well.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded>
      <category>Networking</category>
      
    </item>
    
    <item>
      <title>Encryption vs. Compression</title>
      <link></link>
      <pubDate>Mon, 22 Dec 2025 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<p>No one can argue that both compression and encryption are highly useful. Who knew, however, that it&rsquo;s difficult to have both!</p>
<p>This post goes over why one would never want to first encrypt and then compress, and how the other way around can be dangerous when an adversary can control the payload.</p>]]></description>
      <content:encoded><![CDATA[<p>No one can argue that both compression and encryption are highly useful. Who knew, however, that it&rsquo;s difficult to have both!</p>
<p>This post goes over why one would never want to first encrypt and then compress, and how the other way around can be dangerous when an adversary can control the payload.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>This is Fine: an Interim Microblogging Protocols Update</title>
      <link></link>
      <pubDate>Mon, 22 Dec 2025 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<p>Nate seems to have tried out every protocol under the sun: Nostr, ActivityPub, Diaspora, AT, etc. It&rsquo;s always great to read their thoughts about the latest developments :)</p>]]></description>
      <content:encoded><![CDATA[<p>Nate seems to have tried out every protocol under the sun: Nostr, ActivityPub, Diaspora, AT, etc. It&rsquo;s always great to read their thoughts about the latest developments :)</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>On Creating My Own Cover Art</title>
      <link></link>
      <pubDate>Sun, 21 Dec 2025 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<p>I don&rsquo;t add image thumbnails to my own posts. However, if I was, then I will likely take a similar approach to Kevin; writing code to generate art is cool.</p>]]></description>
      <content:encoded><![CDATA[<p>I don&rsquo;t add image thumbnails to my own posts. However, if I was, then I will likely take a similar approach to Kevin; writing code to generate art is cool.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>On economics</title>
      <link></link>
      <pubDate>Sun, 21 Dec 2025 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<p>There&rsquo;s something alluring about feeling like one has the power to forsee the future. We see this in the rise of sports gambling and prediction markets. While I have no interest in betting with real money, I do think that the <a href="https://braff.co/advice/f/announcing-the-2025-narcissist-forecasting-contest">narcissist forecasting contest</a>, that David shared is a fun idea. The questions in this contest sample from many categories including business, celebrities, crime, and politics. Most of which, I&rsquo;m fully unqualified to make guesses in.</p>]]></description>
      <content:encoded><![CDATA[<p>There&rsquo;s something alluring about feeling like one has the power to forsee the future. We see this in the rise of sports gambling and prediction markets. While I have no interest in betting with real money, I do think that the <a href="https://braff.co/advice/f/announcing-the-2025-narcissist-forecasting-contest">narcissist forecasting contest</a>, that David shared is a fun idea. The questions in this contest sample from many categories including business, celebrities, crime, and politics. Most of which, I&rsquo;m fully unqualified to make guesses in.</p>
<p>One summer, I had a roommate who was studying for a job in finance. They told me that estimation is a key skill. In other words, trying to get into the &ldquo;ballpark&rdquo; of the solution. Maybe they&rsquo;ll excel in these contests.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>The Abilene Paradox</title>
      <link></link>
      <pubDate>Sun, 21 Dec 2025 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<p>We can&rsquo;t read each other&rsquo;s minds, so making decisions that maximizes happiness in a group is difficult. In an ideal world, everyone would be open about their preferences and how strong those are. From there, we can use
a classic algorithm like <a href="https://en.wikipedia.org/wiki/Plurality_voting">Plurality voting</a> to come to a decision.</p>
<p>Though sadly, sometimes we can&rsquo;t be open about our preferences. For our sanity&rsquo;s sake, I hope we&rsquo;re not in the Abilene Paradox too often.</p>]]></description>
      <content:encoded><![CDATA[<p>We can&rsquo;t read each other&rsquo;s minds, so making decisions that maximizes happiness in a group is difficult. In an ideal world, everyone would be open about their preferences and how strong those are. From there, we can use
a classic algorithm like <a href="https://en.wikipedia.org/wiki/Plurality_voting">Plurality voting</a> to come to a decision.</p>
<p>Though sadly, sometimes we can&rsquo;t be open about our preferences. For our sanity&rsquo;s sake, I hope we&rsquo;re not in the Abilene Paradox too often.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>December 8th is Nighthawk&#39;s Solstice</title>
      <link></link>
      <pubDate>Mon, 08 Dec 2025 00:00:00 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<blockquote>
<blockquote>
<p>I hereby raise awareness of Nighthawk&rsquo;s Solstice, which celebrates the day of the earliest sunset of the winter.</p></blockquote></blockquote>
<p>Reading this post got me excited. I <em>don&rsquo;t</em> have to wait until the winter solstice to get more sun in the evening? I agree that the Nighthawk&rsquo;s solstice should be discussed way more.</p>]]></description>
      <content:encoded><![CDATA[<blockquote>
<blockquote>
<p>I hereby raise awareness of Nighthawk&rsquo;s Solstice, which celebrates the day of the earliest sunset of the winter.</p></blockquote></blockquote>
<p>Reading this post got me excited. I <em>don&rsquo;t</em> have to wait until the winter solstice to get more sun in the evening? I agree that the Nighthawk&rsquo;s solstice should be discussed way more.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Fedora CoreOS: First Impressions</title>
      <link>https://brandonrozek.com/blog/fedora-coreos-first-impressions/</link>
      <pubDate>Fri, 28 Nov 2025 10:47:11 -0500</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid>https://brandonrozek.com/blog/fedora-coreos-first-impressions/</guid>
      <description><![CDATA[<p>I have a VPS whose contract ends in December. Instead of renewing, I decided to switch providers of that VPS to OVHCloud. The latter&rsquo;s commitment to <a href="https://corporate.ovhcloud.com/en/sustainability/environment/">sustainability</a> through renewables and component reuse is super cool. Now, I could&rsquo;ve kept the migration simple and keep the configuration the same. I don&rsquo;t write about it much here, but I have <a href="https://www.redhat.com/en/ansible-collaborative">Ansible</a> playbooks for all my servers. However, <a href="https://www.zdnet.com/article/what-is-immutable-linux-heres-why-youd-run-an-immutable-linux-distro/">immutable Linux distributions</a> have been receiving a lot of attention over the past few years and tragically I knew little about them.</p>]]></description>
      <content:encoded><![CDATA[<p>I have a VPS whose contract ends in December. Instead of renewing, I decided to switch providers of that VPS to OVHCloud. The latter&rsquo;s commitment to <a href="https://corporate.ovhcloud.com/en/sustainability/environment/">sustainability</a> through renewables and component reuse is super cool. Now, I could&rsquo;ve kept the migration simple and keep the configuration the same. I don&rsquo;t write about it much here, but I have <a href="https://www.redhat.com/en/ansible-collaborative">Ansible</a> playbooks for all my servers. However, <a href="https://www.zdnet.com/article/what-is-immutable-linux-heres-why-youd-run-an-immutable-linux-distro/">immutable Linux distributions</a> have been receiving a lot of attention over the past few years and tragically I knew little about them.</p>
<p><strong>What is an immutable Linux distribution?</strong> It is a Linux distribution with a read-only core. This prevents accidental modifications which overtime lead to an unstable system.</p>
<p>There are <a href="https://nixos.org/">many</a> <a href="https://ubuntu.com/core">different</a> <a href="https://microos.opensuse.org/">options</a> for these immutable distributions, but as you can see from the title of this post, I went with <a href="https://www.fedoraproject.org/coreos/">Fedora CoreOS</a>. The reason is simple. All my other servers run Fedora Server, so hopefully the changes I need to make to my playbooks are minimal.</p>
<p>At the time of writing, Fedore CoreOS uses <a href="https://ostreedev.github.io/ostree/introduction/">OSTree</a> to perform upgrades over the entire filesystem. These upgrades are <em>atomic</em> which suggests that we are not updating individual packages but the entire Linux distribution as a whole. Apart from the read-only core, there are two writable directories <code>/etc</code> and <code>/var</code>. In fact, your home directory lives in <code>/var/home</code>.</p>
<h2 id="initial-installation">Initial Installation</h2>
<p>OVHCloud does not have a way to upload an ISO and boot directly from there. Instead, we&rsquo;ll have to use their rescue mode feature. Luckily, Timothée wrote up a <a href="https://tim.siosm.fr/blog/2025/09/14/fedora-coreos-ovhcloud-vps/">great guide</a> on how to get started. If this doesn&rsquo;t exactly match your situation, the <a href="https://docs.fedoraproject.org/en-US/fedora-coreos/getting-started/">official documentation</a> has over 20 different provisioning guides.</p>
<p>When I was following the documentation, one of the parts I got tripped up on was how much configuration to put in my Butane file. As noted above, I already have Ansible setup to copy over various files I need. Then I saw that <em>ignition runs only once during the first boot of the system</em>. Hence, if we wanted to customize our storage partitions or lay out networking, then this is a good place to do this. Otherwise, if we want to setup <code>systemd</code> services and the like, we can always add those via Ansible later.</p>
<p>In other words, if you&rsquo;re fine with the defaults and there&rsquo;s a DHCP server running on your network, then the  <em>base Butane config outlined in the documentation is sufficient</em>.</p>
<p>From the official documentation:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">variant</span>: <span style="color:#ae81ff">fcos</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">version</span>: <span style="color:#ae81ff">1.6.0</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">passwd</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">users</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">core</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">ssh_authorized_keys</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#ae81ff">ssh-rsa AAAA...</span>
</span></span></code></pre></div><p>Where you replace the <code>ssh-rsa</code> line with your own SSH public key file.</p>
<h2 id="running-software">Running Software</h2>
<p>For the most part, using OSTree to install additional packages is <a href="https://docs.fedoraproject.org/en-US/fedora-coreos/faq/#_how_do_i_run_custom_applications_on_fedora_coreos">highly discouraged</a>. Instead, it&rsquo;s suggested to install and run things through <a href="https://docs.fedoraproject.org/en-US/fedora-coreos/running-containers/">containers</a>. The official documentation shows how to set up containers via the Butane configuration above, however, I kept my file as simple as shown above.  Instead since <code>/etc/containers/systemd</code> is writable, I wrote <a href="https://brandonrozek.com/blog/migrating-docker-compose-podman-quadlets/">Podman Quadlet files</a> directly (<a href="https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html">Official Quadlet Documentation</a>).</p>
<p>For example, here is my Wireguard Quadlet file</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#66d9ef">[Unit]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Description</span><span style="color:#f92672">=</span><span style="color:#e6db74">WireGuard VPN Container</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">After</span><span style="color:#f92672">=</span><span style="color:#e6db74">network-online.target</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Wants</span><span style="color:#f92672">=</span><span style="color:#e6db74">network-online.target</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[Container]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Image</span><span style="color:#f92672">=</span><span style="color:#e6db74">docker.io/linuxserver/wireguard:latest</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">ContainerName</span><span style="color:#f92672">=</span><span style="color:#e6db74">wireguard</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Give the container the ability to add network interfaces</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">AddCapability</span><span style="color:#f92672">=</span><span style="color:#e6db74">NET_ADMIN</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Have the wireguard network accessible on the host</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Network</span><span style="color:#f92672">=</span><span style="color:#e6db74">host</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Mount the WireGuard configuration directory</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Volume</span><span style="color:#f92672">=</span><span style="color:#e6db74">/etc/wireguard:/config/wg_confs:Z</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[Service]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Restart</span><span style="color:#f92672">=</span><span style="color:#e6db74">always</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">TimeoutStartSec</span><span style="color:#f92672">=</span><span style="color:#e6db74">900</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[Install]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">WantedBy</span><span style="color:#f92672">=</span><span style="color:#e6db74">multi-user.target default.target</span>
</span></span></code></pre></div><p>The following are my Ansible tasks which copy that file over to the machine.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Create /etc/wireguard directory</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">become</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ansible.builtin.file</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/etc/wireguard</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">state</span>: <span style="color:#ae81ff">directory</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">mode</span>: <span style="color:#e6db74">&#39;0700&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">owner</span>: <span style="color:#ae81ff">root</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">group</span>: <span style="color:#ae81ff">root</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Copy Quadlet container file</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">become</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ansible.builtin.copy</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">src</span>: <span style="color:#ae81ff">etc/containers/systemd/wireguard.container</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">dest</span>: <span style="color:#ae81ff">/etc/containers/systemd/wireguard.container</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">mode</span>: <span style="color:#e6db74">&#39;0644&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">owner</span>: <span style="color:#ae81ff">root</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">group</span>: <span style="color:#ae81ff">root</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">register</span>: <span style="color:#ae81ff">wireguardcontainer</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Reload systemd daemon to pick up Quadlet</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">become</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ansible.builtin.systemd</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">daemon_reload</span>: <span style="color:#66d9ef">yes</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">when</span>: <span style="color:#ae81ff">wireguardcontainer.changed</span>
</span></span></code></pre></div><p>Now not everything deserves a spot in <code>/etc/containers/systemd</code>. Take <code>fastfetch</code> for example.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>             .&#39;,;::::;,&#39;.                 core@toolbx
</span></span><span style="display:flex;"><span>         .&#39;;:cccccccccccc:;,.             ------------
</span></span><span style="display:flex;"><span>      .;cccccccccccccccccccccc;.          OS: Fedora Linux 43 (Toolbx Container Image) x86_64
</span></span><span style="display:flex;"><span>    .:cccccccccccccccccccccccccc:.        Host: OpenStack Nova (19.3.2)
</span></span><span style="display:flex;"><span>  .;ccccccccccccc;.:dddl:.;ccccccc;.      Kernel: Linux 6.17.1-300.fc43.x86_64
</span></span><span style="display:flex;"><span> .:ccccccccccccc;OWMKOOXMWd;ccccccc:.     Uptime: 22 hours, 16 mins
</span></span><span style="display:flex;"><span>.:ccccccccccccc;KMMc;cc;xMMc;ccccccc:.    Packages: 366 (rpm)
</span></span><span style="display:flex;"><span>,cccccccccccccc;MMM.;cc;;WW:;cccccccc,    Shell: bash 5.3.0
</span></span><span style="display:flex;"><span>:cccccccccccccc;MMM.;cccccccccccccccc:    Terminal: conmon
</span></span><span style="display:flex;"><span>:ccccccc;oxOOOo;MMM000k.;cccccccccccc:    CPU: 6 x Intel Core (Haswell, no TSX) (6) @ 2.99 GHz
</span></span><span style="display:flex;"><span>cccccc;0MMKxdd:;MMMkddc.;cccccccccccc;    GPU: Cirrus Logic GD 5446
</span></span><span style="display:flex;"><span>ccccc;XMO&#39;;cccc;MMM.;cccccccccccccccc&#39;    Memory: 920.56 MiB / 11.39 GiB (8%)
</span></span><span style="display:flex;"><span>ccccc;MMo;ccccc;MMW.;ccccccccccccccc;     Swap: Disabled
</span></span><span style="display:flex;"><span>ccccc;0MNc.ccc.xMMd;ccccccccccccccc;      Disk (/): 11.53 GiB / 99.44 GiB (12%) - overlay
</span></span><span style="display:flex;"><span>cccccc;dNMWXXXWM0:;cccccccccccccc:,       Disk (/run/host/boot): 277.41 MiB / 349.87 MiB (79%) - ext4 [Read-only]
</span></span><span style="display:flex;"><span>cccccccc;.:odl:.;cccccccccccccc:,.        Disk (/run/host/etc): 11.53 GiB / 99.44 GiB (12%) - xfs
</span></span><span style="display:flex;"><span>ccccccccccccccccccccccccccccc:&#39;.          
</span></span><span style="display:flex;"><span>:ccccccccccccccccccccccc:;,..             
</span></span><span style="display:flex;"><span> &#39;:cccccccccccccccc::;,.
</span></span></code></pre></div><p>All it does it displays system information. Now this is a very important task when showing off your system on Reddit, but it&rsquo;s more of a <em>system administration</em> tool than a software service that your server provides. For these types of tools, we can use <code>toolbox</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>toolbox create <span style="color:#75715e"># Run this once</span>
</span></span><span style="display:flex;"><span>toolbox enter
</span></span></code></pre></div><p>This will give us a new prompt:</p>
<pre tabindex="0"><code>⬢ [core@toolbx ~]$
</code></pre><p>From here, we can use <code>dnf</code> and treat it similarly as a mutable Fedora server machine.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo dnf install fastfetch
</span></span></code></pre></div><p>Now you&rsquo;ll notice that by default it mounts your home directory but the <code>/etc</code> and <code>/var</code> directories are those of the container. We can find all the files in the host system by accessing <code>/run/host</code>.</p>
<p>I use this when trying to view my Nginx logs:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>goaccess /run/host/var/log/nginx/access.log
</span></span></code></pre></div><h2 id="system-security">System Security</h2>
<p>A huge benefit to running all your services via Quadlets is that Podman is able to automatically set the SELinux contexts and configure your firewall rules for you. This means that we don&rsquo;t have to manually change the SELinux context of every file with <code>chcon</code>. If you&rsquo;re lazy, this means hopefully we don&rsquo;t have to disable SELinux!</p>
<p><strong>SELinux:</strong> Notice in my Wireguard Quadlet file I had the following</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#a6e22e">Volume</span><span style="color:#f92672">=</span><span style="color:#e6db74">/etc/wireguard:/config/wg_confs:Z</span>
</span></span></code></pre></div><p>The part after <code>wg_confs:</code> is an optional comma-separated list of options. Here are some that I found to be particularly relevant:</p>
<table>
  <thead>
      <tr>
          <th>Option</th>
          <th>Description</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>Z</code></td>
          <td>Label the content with a private unshared SELinux label.</td>
      </tr>
      <tr>
          <td><code>z</code></td>
          <td>Label the content with a shared SELinux content label so that two or more containers can access it.</td>
      </tr>
      <tr>
          <td><code>U</code></td>
          <td>Recursively change the owner and group of the source volume based on the UID and GID of the container</td>
      </tr>
      <tr>
          <td><code>ro</code></td>
          <td>The container can only read, not write to the volume</td>
      </tr>
  </tbody>
</table>
<p><strong>NFTables:</strong> My other Fedora server systems use <code>firewalld</code> as the primary way I interface with the firewall. Instead of using a CLI tool, the idea is that we edit <code>/etc/sysconfig/nftables.conf</code> with the rules we want. I&rsquo;m still getting used writing my firewall config this way, but I do like how it&rsquo;s all easily viewable in one place.</p>
<p>Here&rsquo;s a version of what I have:</p>
<pre tabindex="0"><code class="language-nftables" data-lang="nftables">#!/usr/sbin/nft -f

# Define the main table
table inet filter {
    
    # Data structure we&#39;ll use to keep track of rate-limiting
    set ssh_ratelimit {
        type ipv4_addr
        size 65536
        flags dynamic,timeout
        timeout 30s
    }

    chain input {
        type filter hook input priority filter; policy drop;

        # Allow loopback
        iif lo accept

        # Allow established/related connections
        ct state established,related accept

        # Allow ICMP (ping, etc)
        ip protocol icmp accept
        ip6 nexthdr icmpv6 accept

        # SSH with rate limiting
        tcp dport 22 ct state new limit rate over 12/minute burst 6 packets drop
        tcp dport 22 accept

        # Allow HTTP and HTTPS
        tcp dport { 80, 443 } accept

        # Drop everything else
        drop
    }

    chain forward {
        type filter hook forward priority filter; policy drop;
        
        # Allow established/related connections
        ct state established,related accept
    }

    chain output {
    	# Allow outgoing connections
        type filter hook output priority filter; policy accept;
    }
}
</code></pre><p>I won&rsquo;t go into detail how NFTables works here. Notice though how we don&rsquo;t specify anything about the Podman network. As I said before, Podman will automatically handle that for us.</p>
<p>However, what Podman won&rsquo;t automatically handle is if we try to change our NFTables configuration without a reboot. This is because reloading NFTables will <em>wipe the existing configuration</em>. As such, we need to manually invoke Podman to regenerate the rules.</p>
<p>Luckily, we can override the NFTables systemd service so that it happens automatically. Create the file <code>/etc/systemd/system/nftables.service.d/override.conf</code> with the following:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#66d9ef">[Service]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">ExecStartPost</span><span style="color:#f92672">=</span><span style="color:#e6db74">podman network reload --all</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">ExecReload</span><span style="color:#f92672">=</span><span style="color:#e6db74">podman network reload --all</span>
</span></span></code></pre></div><h2 id="conclusion">Conclusion</h2>
<p>That wraps up the bits and pieces I had to learn while I was setting my machine up. From there, I&rsquo;ve been running my CoreOS machine for a month and have not yet had any issues. It&rsquo;s too early for me to make any grand claims, but it <em>feels</em> incredibly reliable.</p>
<p>By default, updates are automatic. Since these are atomic, it means that the machine reboots regularly to apply these updates. This encourages us to setup everything to survive reboots and not require any manual intervention. Also, boot times are quick with it clocking under 12 seconds for my VPS.</p>
<p>Running everything in containers provides isolation between these services and the host. This is useful of course for security, but also allows everything to update on their own cadence and not conflict.</p>
<p>Overall, using an immutable distribution is different than traditional Linux server management. However hopefully with this setup, it incentivizes us to create less fragile systems. A community of maintainers will keep the stable read-only core in a great state, and if things go wrong, we can copy our container volumes to another machine.</p>
<p>So if you haven&rsquo;t already, I recommend giving Fedora CoreOS a shot. Next, I have to take a look at how it&rsquo;s like using an immutable distribution on a desktop.</p>
]]></content:encoded>
      <category>Fedora CoreOS</category>
      <category>Podman</category>
      <category>SELinux</category>
      <category>nftables</category>
      
    </item>
    
    <item>
      <title>Flattening Cases to Avoid Nesting in Lean 4</title>
      <link>https://brandonrozek.com/blog/flattening-cases-avoid-nesting-lean-4/</link>
      <pubDate>Sun, 05 Oct 2025 19:38:20 -0400</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid>https://brandonrozek.com/blog/flattening-cases-avoid-nesting-lean-4/</guid>
      <description><![CDATA[<p>Nested cases in proofs increase cognitive load for the reader since they have to process not only the case recently stated but also all the case splits prior. That&rsquo;s why if I can, I prefer to flatten out my cases so that we can see in one step all the variables we&rsquo;re segmenting.</p>
<p>I came across this recently in Lean when working on Lattice proofs over integers with $\infty$ and $-\infty$  In Lean, we can define this &ldquo;extended integer&rdquo; (<code>EInt</code>) by using <code>WithTop</code> and <code>WithBot</code></p>]]></description>
      <content:encoded><![CDATA[<p>Nested cases in proofs increase cognitive load for the reader since they have to process not only the case recently stated but also all the case splits prior. That&rsquo;s why if I can, I prefer to flatten out my cases so that we can see in one step all the variables we&rsquo;re segmenting.</p>
<p>I came across this recently in Lean when working on Lattice proofs over integers with $\infty$ and $-\infty$  In Lean, we can define this &ldquo;extended integer&rdquo; (<code>EInt</code>) by using <code>WithTop</code> and <code>WithBot</code></p>
<pre tabindex="0"><code class="language-lean4" data-lang="lean4">import Mathlib.Order.Interval.Basic

-- An Integer with a top (∞) and bottom (-∞) element
def EInt : Type := WithBot (WithTop Int)
deriving LinearOrder

@[simp] def EInt.ninf : EInt := (⊥ : WithBot (WithTop Int))
@[simp] def EInt.inf : EInt := (WithBot.some ⊤ : WithBot (WithTop Int))

notation &#34;-∞&#34; =&gt; EInt.ninf
notation &#34;∞&#34; =&gt; EInt.inf

-- Helper instances so I can later write numbers and have them casted
instance: IntCast EInt where
  intCast n := WithBot.some (WithTop.some n)

instance: OfNat EInt n where
  ofNat := some (some (Int.ofNat n))
</code></pre><p>Unfortunately, using <code>WithBot</code> and <code>WithTop</code> is just weird. Look at how we would define functions and write proofs using them.</p>
<pre tabindex="0"><code class="language-lean4" data-lang="lean4">def EIntFun : EInt → ℤ
  | none =&gt; 0
  | some ⊤ =&gt; 0
  | some (some _) =&gt; 0

lemma EIntFunIsZero (e: EInt) : EIntFun e = 0 := by
  cases e
  case none =&gt;
    rfl
  case some e =&gt;
    cases e
    case top =&gt;
      rfl
    case coe e =&gt;
      rfl
</code></pre><p>What&rsquo;s more intuitive is to break it up based on whether it&rsquo;s $-\infty$, $\infty$ or some integer $z$. Luckily, we&rsquo;re able to define our own way of splitting up cases in Lean.</p>
<pre tabindex="0"><code class="language-lean4" data-lang="lean4">def EInt.casesOn.{u} {motive : EInt -&gt; Sort u} (a : EInt)
  (ninf : motive -∞)
  (int : ∀ n : Int, motive ↑n)
  (pinf : motive ∞) :
motive a := by
  cases a
  case none =&gt;
    exact ninf
  case some v =&gt;
    cases v
    case top =&gt;
      exact pinf
    case coe n =&gt;
      exact int n
</code></pre><p>When we&rsquo;re doing a proof by cases, we&rsquo;re trying to prove some <code>motive a</code>. What the above definition says, is that if we&rsquo;re given some EInt <code>a</code> and three proofs regarding the motive of each of the cases, then performing the cases successfully proves the motive.</p>
<p>Notice how the proof now no longer has any nested cases:</p>
<pre tabindex="0"><code class="language-lean4" data-lang="lean4">lemma EIntFunIsZero2 (e: EInt) : EIntFun e = 0 := by
  cases e using EInt.casesOn
  case ninf =&gt;
    rfl
  case int z =&gt;
    rfl
  case pinf =&gt;
    rfl
</code></pre><p>In fact, we can even use this trick to simplify our function</p>
<pre tabindex="0"><code class="language-lean4" data-lang="lean4">def EIntFun2 (e: EInt) : ℤ := by
  cases e using EInt.casesOn with
  | ninf =&gt;
    exact 0
  | int z =&gt;
    exact 0
  | pinf =&gt;
    exact 0
</code></pre><h2 id="a-more-complicated-example">A more complicated example</h2>
<p>Personally I find utility in defining how we want our cases to go prior to the proof itself. For example, let&rsquo;s break up the domain further by considering the sign of our integers.</p>
<pre tabindex="0"><code class="language-lean4" data-lang="lean4">def EInt.casesOnSigns.{u} {motive : EInt -&gt; Sort u} (a : EInt)
  (ninf : motive -∞)
  (nint : ∀ n : Int, n &lt; 0 → motive ↑n)
  (zero: ∀ n : Int, n = 0 → motive ↑n)
  (pint : ∀ n : Int, n &gt; 0 → motive ↑n)
  (pinf : motive ∞) :
motive a := by
  cases a
  case none =&gt;
    exact ninf
  case some v =&gt;
    cases v
    case top =&gt;
      exact pinf
    case coe n =&gt;
      by_cases n &lt; 0
      case pos H =&gt;
        exact nint n H
      case neg H =&gt;
        by_cases n = 0
        case pos H2 =&gt;
          exact zero n H2
        case neg H2 =&gt;
          have H3 : n &gt; 0 := by
            have HH : n ≥ 0 := Int.not_lt.mp H
            have HH2 : n ≠ 0 := H2
            have HH3 : 0 ≠ n := Ne.symm HH2
            exact lt_of_le_of_ne HH HH3
          exact pint n H3
</code></pre><p>The more complicated we make our cases, the more Lean will struggle to establish definitional equality. I personally find it useful to create helper lemmas for each of the cases to later help establish our motive.</p>
<p><strong>Negative Infinity Case</strong></p>
<pre tabindex="0"><code class="language-lean4" data-lang="lean4">@[simp]
lemma EInt.casesOnSigns.is_ninf.{u} {motive : EInt -&gt; Sort u}
  (ninf : motive -∞)
  (nint : ∀ n : Int, n &lt; 0 → motive ↑n)
  (zero: ∀ n : Int, n = 0 → motive ↑n)
  (pint : ∀ n : Int, n &gt; 0 → motive ↑n)
  (pinf : motive ∞) : EInt.casesOnSigns (-∞) ninf nint zero pint pinf = ninf := rfl
</code></pre><p>The above says that if we perform a cases on $-\infty$ then the result will be equivalent to the <code>ninf</code> case.</p>
<p><strong>Negative Integer Case</strong></p>
<pre tabindex="0"><code class="language-lean4" data-lang="lean4">lemma EInt.casesOnSigns.is_nint.{u} {motive : EInt -&gt; Sort u}
  (z : Int)
  (Hz : z &lt; 0)
  (ninf : motive -∞)
  (nint : ∀ n : Int, n &lt; 0 → motive ↑n)
  (zero: ∀ n : Int, n = 0 → motive ↑n)
  (pint : ∀ n : Int, n &gt; 0 → motive ↑n)
  (pinf : motive ∞) :
  EInt.casesOnSigns (↑z) ninf nint zero pint pinf = nint z Hz := by
  unfold casesOnSigns
  show (casesOn (↑z) ninf (fun n =&gt; if h : n &lt; 0 then nint n h
                                    else if h : n = 0 then zero n h
                                    else pint n (by omega : n &gt; 0)) pinf) = nint z Hz
  change (if h : z &lt; 0 then nint z h
          else if h : z = 0 then zero z h
          else pint z (by omega : z &gt; 0)) = nint z Hz
  rw [dif_pos Hz]
</code></pre><p>With our usage of inequalities, we have to help guide Lean through this proof. We first unfold the <code>casesOnSigns</code> definition to get the goal shown in the <code>show</code> tactic. From there, we already know that our EInt is the integer <code>z</code>, so we can simplify the cases to our nested if-then-else statement.  After that, since we have the hypothesis that our integer <code>z</code> is negative, we can directly get the <code>nint z h</code> case from the consequent of the outer ite.</p>
<p><strong>Zero Case</strong></p>
<pre tabindex="0"><code class="language-lean4" data-lang="lean4">@[simp]
lemma EInt.casesOnSigns.is_zero.{u}  {motive : EInt -&gt; Sort u}
  (z : Int)
  (Hn : z = 0)
  (ninf : motive -∞)
  (nint : ∀ n : Int, n &lt; 0 → motive ↑n)
  (zero: ∀ n : Int, n = 0 → motive ↑n)
  (pint : ∀ n : Int, n &gt; 0 → motive ↑n)
  (pinf : motive ∞) : (EInt.casesOnSigns (z) ninf nint zero pint pinf)  = zero z Hn := by
    subst z
    rfl
</code></pre><p>Once we substitute zero in, Lean can automatically establish definitional equality.</p>
<p><strong>Positive Case</strong></p>
<pre tabindex="0"><code class="language-lean4" data-lang="lean4">lemma EInt.casesOnSigns.is_pint.{u}  {motive : EInt -&gt; Sort u}
  (z : Int)
  (Hz : z &gt; 0)
  (ninf : motive -∞)
  (nint : ∀ n : Int, n &lt; 0 → motive ↑n)
  (zero: ∀ n : Int, n = 0 → motive ↑n)
  (pint : ∀ n : Int, n &gt; 0 → motive ↑n)
  (pinf : motive ∞) : (EInt.casesOnSigns (z) ninf nint zero pint pinf)  = pint z Hz := by
  unfold casesOnSigns
  show (casesOn (↑z) ninf (fun n =&gt; if h : n &lt; 0 then nint n h
                                    else if h : n = 0 then zero n h
                                    else pint n (by omega : n &gt; 0)) pinf) = pint z Hz
  change (if h : z &lt; 0 then nint z h
          else if h : z = 0 then zero z h
          else pint z (Hz : z &gt; 0)) = pint z Hz
  have HH: ¬(z &lt; 0) := not_lt_of_gt Hz
  rw [dif_neg HH]
  have HH2: ¬(z = 0) := Int.ne_of_gt Hz
  rw [dif_neg HH2]
</code></pre><p>Similar to the negative case, but we need to do slightly more work to get to the last alternative within the nested if-then-else statement.</p>
<p><strong>Infinity Case</strong></p>
<pre tabindex="0"><code class="language-lean4" data-lang="lean4">@[simp]
lemma EInt.casesOnSigns.is_pinf.{u} {motive : EInt -&gt; Sort u}
  (ninf : motive -∞)
  (nint : ∀ n : Int, n &lt; 0 → motive ↑n)
  (zero: ∀ n : Int, n = 0 → motive ↑n)
  (pint : ∀ n : Int, n &gt; 0 → motive ↑n)
  (pinf : motive ∞) : EInt.casesOnSigns (∞) ninf nint zero pint pinf = pinf := rfl
</code></pre><hr>
<p>Like before, now that we have our new definition <code>EInt.casesOnSigns</code>, we can create our function which determines whether an EInt is positive cleanly.</p>
<pre tabindex="0"><code class="language-lean4" data-lang="lean4">def EInt.isPos (e: EInt) : Bool := by
  cases e using EInt.casesOnSigns with
  | ninf =&gt; exact false
  | nint _ =&gt; exact false
  | zero =&gt; exact false
  | pint _ =&gt; exact true
  | pinf =&gt; exact true
</code></pre><p>This is much better than if we worked with the original <code>WithTop</code> and <code>WithBot</code> version! Now let&rsquo;s prove that our EInt is positive iff it is greater than zero. To do this we&rsquo;ll need one helper lemma.</p>
<pre tabindex="0"><code class="language-lean4" data-lang="lean4">lemma EInt.coe_lt_coe {a b : Int}:  ((↑a : EInt) &lt; (↑b: EInt)) ↔ a &lt; b := by
  have H1 : ((↑a : EInt) &lt; (↑b: EInt)) → a &lt; b := by
    intro (H: (↑a : EInt) &lt; (↑b: EInt))
    apply WithTop.coe_lt_coe.mp
    exact WithBot.coe_lt_coe.mp H
  have H2 : a &lt; b → ((↑a : EInt) &lt; (↑b: EInt)) := by
    clear H1
    intro (H : a &lt; b)
    apply WithBot.coe_lt_coe.mpr
    exact WithTop.coe_lt_coe.mpr H
  exact Iff.intro H1 H2
</code></pre><p>The above lemma states that if the integer $a$ is less than the integer $b$, then the EInt version of $a$ is less than the EInt version of $b$ and vice versa.  Now for the main proof</p>
<p><strong>Soundness</strong></p>
<p>If our EInt <code>e</code> is positive, then <code>e</code> is greater than zero.</p>
<pre tabindex="0"><code class="language-lean4" data-lang="lean4">lemma EInt.isPos_sound (e: EInt) : e.isPos = true → e &gt; 0 := by
  intro H
  cases e using EInt.casesOnSigns
  case ninf =&gt;
    contradiction
  case nint n Hn =&gt;
    unfold EInt.isPos at H
    rw [EInt.casesOnSigns.is_nint n Hn] at H
    contradiction
  case zero n Hz =&gt;
    unfold EInt.isPos at H
    rw [EInt.casesOnSigns.is_zero n Hz] at H
    contradiction
  case pint n Hp =&gt;
    apply EInt.coe_lt_coe.mpr Hp
  case pinf =&gt;
    exact Batteries.compareOfLessAndEq_eq_lt.mp rfl
</code></pre><p><strong>Completeness</strong></p>
<p>If our EInt <code>e</code> is greater than zero, then <code>e</code> is positive.</p>
<pre tabindex="0"><code class="language-lean4" data-lang="lean4">lemma EInt.isPos_complete (e: EInt) : e &gt; 0 → e.isPos = true := by
  intro H
  cases e using EInt.casesOnSigns
  case ninf =&gt;
    contradiction
  case nint n Hn =&gt;
    have H2 : n &gt; 0 := EInt.coe_lt_coe.mp H
    have H3 : ¬(n &gt; 0) := not_lt_of_gt Hn
    contradiction
  case zero n Hz =&gt;
    have H2 : n &gt; 0 := EInt.coe_lt_coe.mp H
    have H3 : ¬(n &gt; 0) := Eq.not_gt Hz
    contradiction
  case pint n Hp =&gt;
    unfold EInt.isPos
    rw [EInt.casesOnSigns.is_pint n Hp _ _ _ _ _ ]
  case pinf =&gt;
    unfold EInt.isPos
    rw [EInt.casesOnSigns.is_pinf]
</code></pre>]]></content:encoded>
      <category>Lean</category>
      <category>Proof assistant</category>
      <category>Formal Proof</category>
      
    </item>
    
    <item>
      <title></title>
      <link></link>
      <pubDate>Sun, 07 Sep 2025 17:50:44 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[]]></description>
      <content:encoded><![CDATA[]]></content:encoded>
      
    </item>
    
    <item>
      <title>Cursed Knowledge: Javascript Arrays Are Objects</title>
      <link>https://brandonrozek.com/blog/cursed-knowledge-javascript-arrays-are-objects/</link>
      <pubDate>Mon, 01 Sep 2025 09:47:01 -0400</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid>https://brandonrozek.com/blog/cursed-knowledge-javascript-arrays-are-objects/</guid>
      <description><![CDATA[<p>My friend Ethan recently wrote a blog post on <a href="https://emar10.dev/posts/cursed-commands-part-1/">cursed commands</a>. Chris shared with me that Immich has a page on their site called <a href="https://immich.app/cursed-knowledge/">cursed knowledge</a>, and it looks like this has started a trend. I&rsquo;ve seen my fair share of the dark arts in programming, so I&rsquo;ll hop on and share what I know about JavaScript arrays.</p>
<p>JavaScript arrays are <a href="https://262.ecma-international.org/#sec-array-exotic-objects"><em>exotic objects</em></a> according to the ECMAScript specification. Therefore, they may lead to unintuitive behavior if we think of these arrays as C-like.</p>]]></description>
      <content:encoded><![CDATA[<p>My friend Ethan recently wrote a blog post on <a href="https://emar10.dev/posts/cursed-commands-part-1/">cursed commands</a>. Chris shared with me that Immich has a page on their site called <a href="https://immich.app/cursed-knowledge/">cursed knowledge</a>, and it looks like this has started a trend. I&rsquo;ve seen my fair share of the dark arts in programming, so I&rsquo;ll hop on and share what I know about JavaScript arrays.</p>
<p>JavaScript arrays are <a href="https://262.ecma-international.org/#sec-array-exotic-objects"><em>exotic objects</em></a> according to the ECMAScript specification. Therefore, they may lead to unintuitive behavior if we think of these arrays as C-like.</p>
<p>Let&rsquo;s play around.</p>
<h3 id="concept-1-javascript-arrays-are-not-continguous">Concept 1: JavaScript arrays are not continguous</h3>
<p>First, consider the following array:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#66d9ef">let</span> <span style="color:#a6e22e">x</span> <span style="color:#f92672">=</span> [<span style="color:#ae81ff">0</span>, <span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">2</span>];
</span></span></code></pre></div><p>As one might expect, <code>x.length</code> is equal to <code>3</code>. To tell whether or not an index is in an array, we can use the <code>in</code> operator.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ae81ff">3</span> <span style="color:#66d9ef">in</span> <span style="color:#a6e22e">x</span> <span style="color:#75715e">// Evaluates to false
</span></span></span></code></pre></div><p>If we try to access the 3rd index, the result will evaluate to <code>undefined</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#a6e22e">x</span>[<span style="color:#ae81ff">3</span>] <span style="color:#75715e">// Evaluates to undefined
</span></span></span></code></pre></div><p>Now let&rsquo;s assign an element to the 4th index. Keep in mind that we&rsquo;re skipping over the 3rd one.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#a6e22e">x</span>[<span style="color:#ae81ff">4</span>] <span style="color:#f92672">=</span> <span style="color:#ae81ff">4</span>;
</span></span></code></pre></div><p>Now when we check our <code>length</code> property, it&rsquo;ll say that our array is now of size <code>5</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#a6e22e">x</span>.<span style="color:#a6e22e">length</span> <span style="color:#75715e">// Evaluates to 5
</span></span></span></code></pre></div><p>However, the 3rd index still does not exist</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ae81ff">3</span> <span style="color:#66d9ef">in</span> <span style="color:#a6e22e">x</span> <span style="color:#75715e">// Evaluates to false
</span></span></span></code></pre></div><h3 id="concept-2-explicit-vs-implicit-undefined">Concept 2: Explicit vs Implicit <code>undefined</code></h3>
<p>Recall that <code>x[3]</code> evaluates to <code>undefined</code>. What happens when we set the value explicitly?</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#a6e22e">x</span>[<span style="color:#ae81ff">3</span>] <span style="color:#f92672">=</span> <span style="color:#66d9ef">undefined</span>;
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">3</span> <span style="color:#66d9ef">in</span> <span style="color:#a6e22e">x</span> <span style="color:#75715e">// Evaluates to true
</span></span></span></code></pre></div><p>So there is a difference on whether we have explicitly set an index to <code>undefined</code>! This distinction is not always used. For example, our trusty for-of loop does not care.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#a6e22e">x</span> <span style="color:#f92672">=</span> [<span style="color:#ae81ff">0</span>, <span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">2</span>];
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">x</span>[<span style="color:#ae81ff">4</span>] <span style="color:#f92672">=</span> <span style="color:#ae81ff">4</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> (<span style="color:#a6e22e">a</span> <span style="color:#66d9ef">of</span> <span style="color:#a6e22e">x</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#a6e22e">a</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Will print out</p>
<pre tabindex="0"><code>0
1
2
undefined
4
</code></pre><h3 id="concept-3-indices-are-actually-strings">Concept 3: Indices are actually strings</h3>
<p>Given that we have a length property and that we&rsquo;ve been indexing with numeric keys, it must mean that arrays have numeric indices. Right?</p>
<pre tabindex="0"><code>&#34;0&#34; in [&#34;a&#34;, &#34;b&#34;] // Evaluates to true
</code></pre><p>Okay, it looks like there&rsquo;s some conversion magic that&rsquo;s happening behind the scenes here. The ECMAScript specification says that an array index must be strictly less than $2^{32}$. So what happens if it is not?</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#66d9ef">let</span> <span style="color:#a6e22e">x</span> <span style="color:#f92672">=</span> [<span style="color:#ae81ff">0</span>];
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">x</span>[<span style="color:#ae81ff">4294967296</span>] <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">x</span> <span style="color:#75715e">// Evaluates to [ 0, &#39;4294967296&#39;: true ]
</span></span></span></code></pre></div><p>It looks like it no longer gets treated as an array item, but instead treats it as an arbitrary key-value pair. Why stop there, this must mean that we can store any sort of arbitrary data in our array.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#a6e22e">x</span>.<span style="color:#a6e22e">name</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Brandon&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">x</span> <span style="color:#75715e">// Evaluates to [ 0, &#39;4294967296&#39;: true, name: &#39;Brandon&#39; ]
</span></span></span></code></pre></div><h3 id="viewing-arrays-as-objects">Viewing arrays as objects</h3>
<p>Now everything starts to make more sense when we think of these arrays as objects.</p>
<pre tabindex="0"><code class="language-javscript" data-lang="javscript">let x = [0, 1, 2];
x[4] = 4;
</code></pre><p>Internally, this corresponds to the object:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;0&#34;</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">0</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;1&#34;</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">1</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;2&#34;</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">2</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;4&#34;</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">4</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>From this object, we can see that the keys are strings and that the 3rd key is not in the object. Now let&rsquo;s see what happens when we explicitly set <code>x[5] = undefined</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;0&#34;</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">0</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;1&#34;</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">1</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;2&#34;</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">2</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;4&#34;</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">4</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;5&#34;</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">undefined</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The 5th key is now in our object and it&rsquo;s set to an undefined value. We also get an undefined value when we try to retrieve a value of a key that is not in our object.</p>
<p>The length of our array is the highest &ldquo;numeric&rdquo; key within our object (subject to the size limit). When we iterate over our array using <code>for-of</code>, we&rsquo;re iterating from <code>&quot;0&quot;</code> to our length.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#66d9ef">for</span> (<span style="color:#a6e22e">a</span> <span style="color:#66d9ef">of</span> <span style="color:#a6e22e">x</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#a6e22e">a</span>);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Is the same as:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#a6e22e">x</span>[<span style="color:#ae81ff">0</span>]);
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#a6e22e">x</span>[<span style="color:#ae81ff">1</span>]);
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#a6e22e">x</span>[<span style="color:#ae81ff">2</span>]);
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#a6e22e">x</span>[<span style="color:#ae81ff">3</span>]);
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#a6e22e">x</span>[<span style="color:#ae81ff">4</span>]);
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#a6e22e">x</span>[<span style="color:#ae81ff">5</span>]);
</span></span></code></pre></div><p>That&rsquo;s an exotic object for you.</p>
]]></content:encoded>
      <category>JavaScript</category>
      
    </item>
    
    <item>
      <title></title>
      <link></link>
      <pubDate>Sat, 26 Jul 2025 15:56:50 +0000</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid></guid>
      <description><![CDATA[<p>The polls are now open over at USPS to determine which forever stamp to bring back.</p><p><a href="https://www.stampsforever.com/vote" target="_blank" rel="nofollow noopener" translate="no"><span class="invisible">https://www.</span><span class="">stampsforever.com/vote</span><span class="invisible"></span></a></p><p>The stamps that they make are always cool and it&#39;s great to see recognition for some of the earlier designs.</p>]]></description>
      <content:encoded><![CDATA[<p>The polls are now open over at USPS to determine which forever stamp to bring back.</p><p><a href="https://www.stampsforever.com/vote" target="_blank" rel="nofollow noopener" translate="no"><span class="invisible">https://www.</span><span class="">stampsforever.com/vote</span><span class="invisible"></span></a></p><p>The stamps that they make are always cool and it&#39;s great to see recognition for some of the earlier designs.</p>]]></content:encoded>
      
    </item>
    
    <item>
      <title>Deterministically Iterating over a set within Dafny functions</title>
      <link>https://brandonrozek.com/blog/deterministic-set-iteration-dafny/</link>
      <pubDate>Sun, 06 Jul 2025 12:27:01 -0400</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid>https://brandonrozek.com/blog/deterministic-set-iteration-dafny/</guid>
      <description><![CDATA[<p>Say we have a set that we want to iterate over within a pure Dafny function. For sake of example, we will look at a set of strings. In Dafny,  <code>var x :| condition</code> denotes &ldquo;let us define variable x such that [condition]&rdquo;. Therefore, a first attempt at writing our function might be:</p>
<pre tabindex="0"><code>function iterate_helper(collection: set&lt;string&gt;, acc: seq&lt;string&gt;): seq&lt;string&gt;
{
    if collection == {} then acc
    else
        var x :| x in collection;
        var newAcc := acc + [x];
        var newCollection := collection - {x};
        iterate_helper(newCollection, newAcc)
}
</code></pre><p>The issue is that Dafny will complain with the following error message:</p>]]></description>
      <content:encoded><![CDATA[<p>Say we have a set that we want to iterate over within a pure Dafny function. For sake of example, we will look at a set of strings. In Dafny,  <code>var x :| condition</code> denotes &ldquo;let us define variable x such that [condition]&rdquo;. Therefore, a first attempt at writing our function might be:</p>
<pre tabindex="0"><code>function iterate_helper(collection: set&lt;string&gt;, acc: seq&lt;string&gt;): seq&lt;string&gt;
{
    if collection == {} then acc
    else
        var x :| x in collection;
        var newAcc := acc + [x];
        var newCollection := collection - {x};
        iterate_helper(newCollection, newAcc)
}
</code></pre><p>The issue is that Dafny will complain with the following error message:</p>
<blockquote>
<p>to be compilable, the value of a let-such-that expression must be uniquely determined</p></blockquote>
<p>Dafny functions must be deterministic. This means that no matter how many times we call a function with some specified input, we will always get the same output. Therefore, as the error message suggests, we need to write a condition that <em>uniquely</em> determines <code>x</code>. One way to achieve this is to specify an order as described in <a href="https://www.microsoft.com/en-us/research/wp-content/uploads/2016/12/krml252.pdf">Rustan&rsquo;s paper</a>:</p>
<pre tabindex="0"><code>function iterate_helper(collection: set&lt;string&gt;, acc: seq&lt;string&gt;): seq&lt;string&gt;
{
    if collection == {} then acc
    else
        var x :| x in collection &amp;&amp; forall y | y in collection :: x &lt;= y;
        var newAcc := acc + [x];
        var newCollection := collection - {x};
        iterate_helper(newCollection, newAcc)
}
</code></pre><p>Unfortunately, this code will return two errors:</p>
<blockquote>
<p>cannot establish the existence of LHS values that satisfy the such-that predicate</p></blockquote>
<blockquote>
<p>to be compilable, the value of a let-such-that expression must be uniquely determined</p></blockquote>
<p>However, it&rsquo;s totally possible to define an ordering over strings. We&rsquo;ll just need to do some work to convince the verifier of this.</p>
<h3 id="comparing-two-strings">Comparing two strings</h3>
<p>Since strings in Dafny are a sequence of characters, it turns out that the <code>&lt;=</code> relation checks whether the left side is a prefix of the right side. Therefore, there is no such thing as a <em>unique</em> minimum element, since there are incomparable elements like &ldquo;a&rdquo; and &ldquo;b&rdquo;.</p>
<p>As such, our first step is to create a less than or equal to (<code>&lt;=</code>) relation that induces a total order.  Consider the following function that determines the order based on the left-most character.</p>
<pre tabindex="0"><code class="language-function" data-lang="function">function string_le(s1: string, s2: string): bool
    decreases |s1| + |s2|
{
    if |s1| == 0 &amp;&amp; |s2| &gt; 0 then
        true
    else if |s1| &gt; 0 &amp;&amp; |s2| == 0 then
        false
    else if |s1| == 0 &amp;&amp; |s2| == 0 then
        true
    else
        assert(|s1| &gt; 0);
        assert(|s2| &gt; 0);
        var c1 := s1[0];
        var c2 := s2[0];
        if c1 &lt; c2 then
            true
        else if c1 &gt; c2 then
            false
        else
            string_le(s1[1..], s2[1..])
}
</code></pre><h3 id="properties-of-our-comparison-function">Properties of our comparison function</h3>
<p>From here we need to prove that our relation induces a total order. For this, we need to show that it is reflexive, total, anti-symmetric, and transitive.</p>
<p>Reflexivity: All strings are less than or equal to themselves</p>
<pre tabindex="0"><code>lemma string_le_reflexive()
    ensures forall s :: string_le(s ,s)
{
    forall s ensures string_le(s, s)
    {
        string_le_reflexive_helper(s);
    }
}

lemma string_le_reflexive_helper(s1: string)
    ensures string_le(s1, s1)
{}
</code></pre><p>Totality: Given two strings, one is less than or equal to the other.</p>
<pre tabindex="0"><code>lemma string_le_totality()
    ensures forall s1, s2 :: string_le(s1, s2) || string_le(s2, s1)
{
    forall s1, s2 ensures string_le(s1, s2) || string_le(s2, s1)
    {
        string_le_totality_helper(s1, s2);
    }
}

lemma string_le_totality_helper(s1: string, s2: string)
    ensures string_le(s1, s2) || string_le(s2, s1)
{}
</code></pre><p>Antisymmetric: If one string is less than or equal to another string and that other string is also less than or equal to the original string then both strings are equivalent.</p>
<pre tabindex="0"><code>lemma string_le_antisymmetric()
    ensures forall s1, s2 :: string_le(s1, s2) &amp;&amp; string_le(s2, s1) ==&gt; s1 == s2
{
    forall s1, s2 | string_le(s1, s2) &amp;&amp; string_le(s2, s1)
    ensures s1 == s2
    {
        string_le_antisymmetric_helper(s1, s2);
    }
}

lemma string_le_antisymmetric_helper(s1: string, s2: string)
    requires string_le(s1, s2)
    requires string_le(s2, s1)
    ensures s1 == s2
{}
</code></pre><p>Transitive: If one string is less than or equal to another string, and that other string is less than or equal to some third string, then the first string is less than or equal to that third string.</p>
<pre tabindex="0"><code>lemma string_le_transitive()
    ensures forall s1, s2, s3 :: string_le(s1, s2) &amp;&amp; string_le(s2, s3) ==&gt; string_le(s1, s3)
{
    forall s1, s2, s3 | string_le(s1, s2) &amp;&amp; string_le(s2, s3)
    ensures string_le(s1, s3)
    {
        string_le_transitive_helper(s1, s2, s3);
    }
}

lemma string_le_transitive_helper(s1: string, s2: string, s3: string)
    requires string_le(s1, s2)
    requires string_le(s2, s3)
    ensures string_le(s1, s3)
{}
</code></pre><p>Then, we conviniently package all the properties together:</p>
<pre tabindex="0"><code>lemma string_le_properties()
    ensures forall s :: string_le(s, s)
    ensures forall s1, s2 :: string_le(s1, s2) &amp;&amp; string_le(s2, s1) ==&gt; s1 == s2
    ensures forall s1, s2, s3 :: string_le(s1, s2) &amp;&amp; string_le(s2, s3) ==&gt; string_le(s1, s3)
    ensures forall s1, s2 :: string_le(s1, s2) || string_le(s2, s1)
{
    string_le_reflexive();
    string_le_antisymmetric();
    string_le_transitive();
    string_le_totality();
}
</code></pre><h3 id="string-sets-have-a-minimum">String sets have a minimum</h3>
<p>With the total ordering of strings, we can prove that a smallest element exists within a non-empty set <code>s</code>. First, let us invoke our comparison properties lemma, so the verifier has access to those properties:</p>
<pre tabindex="0"><code>string_le_properties();
</code></pre><p>Since our set is non-empty, we can grab an arbitrary element from <code>s</code>.</p>
<pre tabindex="0"><code>var x :| x in s;
</code></pre><p>For this proof, we&rsquo;ll approach it inductively. First, let&rsquo;s consider when <code>s == {x}</code>.</p>
<ol>
<li>By construction, every element in <code>s</code> is equal to <code>x</code>.</li>
<li>Then by reflexivity, <code>x</code> is smaller than every element in <code>s</code>.</li>
</ol>
<pre tabindex="0"><code>assert forall y :: y in s ==&gt; y == x;
assert forall y :: y in s ==&gt; string_le(x, y);
</code></pre><p>Now let&rsquo;s consider the inductive case. The set in this case has more elements than just <code>x</code>. Let&rsquo;s consider <code>s'</code> the subset of <code>s</code> without the element <code>x</code>.</p>
<pre tabindex="0"><code>var s&#39; := s - {x};
assert s&#39; != {};
</code></pre><p>Since <code>s'</code> is non-empty, we can by induction say that  <code>s'</code> has a smallest element.</p>
<pre tabindex="0"><code>string_smallest_exists(s&#39;);
var x&#39; :| x&#39; in s&#39; &amp;&amp; forall y :: y in s&#39; ==&gt; string_le(x&#39;, y);
</code></pre><p>As <code>s'</code> is the subset of <code>s</code> without <code>x</code>, we can assert that <code>x'</code> and <code>x</code> are not the same:</p>
<pre tabindex="0"><code>assert x&#39; != x;
</code></pre><p>From here, we compare both <code>x</code> and <code>x'</code> (which we&rsquo;re able to do since <code>&lt;=</code> is total)</p>
<p>Case 1: <code>x &lt;= x'</code>: By transitivity, <code>x</code> will be less than all the elements of <code>s'</code>. Since <code>s'</code> is  <code>s</code>  without <code>x</code>, we can safely say that <code>x</code> is less than every element in <code>s</code>.</p>
<pre tabindex="0"><code>assert forall y :: y in s&#39; ==&gt; string_le(x, y);
assert forall y :: y in s ==&gt; string_le(x, y);
</code></pre><p>Case 2: <code>!(x &lt;= x')</code>. From totality, we have that <code>x' &lt;= x</code>. Since we know from the inductive hypothesis that <code>x'</code> is the minimum of <code>s'</code> and <code>s</code> is <code>s'</code> with the element <code>x</code>, we can conclude that <code>x'</code> is the smallest element of <code>s</code>.</p>
<pre tabindex="0"><code>assert !string_le(x, x&#39;);
assert string_le(x&#39;, x);
assert forall y :: y in s ==&gt; string_le(x&#39;, y);
</code></pre><p>With that, we&rsquo;ve proven that a smallest string exists! Here&rsquo;s the lemma in its entirety:</p>
<pre tabindex="0"><code>lemma string_smallest_exists(s: set&lt;string&gt;)
    requires s != {}
    decreases s
    ensures exists x :: x in s &amp;&amp; forall y :: y in s ==&gt; string_le(x, y)
{
    string_le_properties();

    var x :| x in s;

    // Base Case
    if s == {x} {
        assert forall y :: y in s ==&gt; y == x;
        assert forall y :: y in s ==&gt; string_le(x, y);
	
    // Inductive Case
    } else {
        var s&#39; := s - {x};
        assert s&#39; != {};
        string_smallest_exists(s&#39;);
        var x&#39; :| x&#39; in s&#39; &amp;&amp; forall y :: y in s&#39; ==&gt; string_le(x&#39;, y);
        assert x != x&#39;;

        if string_le(x, x&#39;) {
            assert forall y :: y in s&#39; ==&gt; string_le(x, y);
            assert forall y :: y in s ==&gt; string_le(x, y);
        } else {
            // x&#39; is smaller than x
            assert !string_le(x, x&#39;);
            assert string_le(x&#39;, x);
            assert forall y :: y in s ==&gt; string_le(x&#39;, y);
        }
    }
}
</code></pre><h3 id="select-the-smallest-element-from-a-set">Select the smallest element from a set</h3>
<p>Now that we have determined that a minimum exists in a set, we can use these lemmas to establish that we can uniquely determine the element that we select based on the ordering.</p>
<pre tabindex="0"><code>function select_string_from_set(collection: set&lt;string&gt;): string
    requires collection != {}
{
    string_le_properties();
    string_smallest_exists(collection);
    var value :| value in collection &amp;&amp; forall y | y in collection :: string_le(value, y);
    value
}
</code></pre><h3 id="conclusion">Conclusion</h3>
<p>Revisiting our iteration example, we can use our new <code>select_string_from_set</code> function to iterate over a set of strings in a pure deterministic function. More specifically, we&rsquo;ll visit all the elements in the collection in the order defined by our relation.</p>
<pre tabindex="0"><code>function iterate_helper(collection: set&lt;string&gt;, acc: seq&lt;string&gt;): seq&lt;string&gt;
    decreases collection
{
    if collection == {} then acc
    else
        var x := select_string_from_set(collection);
        var newAcc := acc + [x];
        var newCollection := collection - {x};
        iterate_helper(newCollection, newAcc)
}
</code></pre><p>From here you can generalize beyond a set of strings, as long as you&rsquo;re able to prove the properties of a total order. The full code for our string sets example is below:</p>
<pre tabindex="0"><code>function string_le(s1: string, s2: string): bool
    decreases |s1| + |s2|
{
    if |s1| == 0 &amp;&amp; |s2| &gt; 0 then
        true
    else if |s1| &gt; 0 &amp;&amp; |s2| == 0 then
        false
    else if |s1| == 0 &amp;&amp; |s2| == 0 then
        true
    else
        assert(|s1| &gt; 0);
        assert(|s2| &gt; 0);
        var c1 := s1[0];
        var c2 := s2[0];
        if c1 &lt; c2 then
            true
        else if c1 &gt; c2 then
            false
        else
            string_le(s1[1..], s2[1..])
}

lemma string_le_reflexive()
    ensures forall s :: string_le(s ,s)
{
    forall s ensures string_le(s, s)
    {
        string_le_reflexive_helper(s);
    }
}

lemma string_le_reflexive_helper(s1: string)
    ensures string_le(s1, s1)
{}

lemma string_le_totality()
    ensures forall s1, s2 :: string_le(s1, s2) || string_le(s2, s1)
{
    forall s1, s2 ensures string_le(s1, s2) || string_le(s2, s1)
    {
        string_le_totality_helper(s1, s2);
    }
}

lemma string_le_totality_helper(s1: string, s2: string)
    ensures string_le(s1, s2) || string_le(s2, s1)
{}

lemma string_le_antisymmetric()
    ensures forall s1, s2 :: string_le(s1, s2) &amp;&amp; string_le(s2, s1) ==&gt; s1 == s2
{
    forall s1, s2 | string_le(s1, s2) &amp;&amp; string_le(s2, s1)
    ensures s1 == s2
    {
        string_le_antisymmetric_helper(s1, s2);
    }
}

lemma string_le_antisymmetric_helper(s1: string, s2: string)
    requires string_le(s1, s2)
    requires string_le(s2, s1)
    ensures s1 == s2
{}


lemma string_le_transitive()
    ensures forall s1, s2, s3 :: string_le(s1, s2) &amp;&amp; string_le(s2, s3) ==&gt; string_le(s1, s3)
{
    forall s1, s2, s3 | string_le(s1, s2) &amp;&amp; string_le(s2, s3)
    ensures string_le(s1, s3)
    {
        string_le_transitive_helper(s1, s2, s3);
    }
}

lemma string_le_transitive_helper(s1: string, s2: string, s3: string)
    requires string_le(s1, s2)
    requires string_le(s2, s3)
    ensures string_le(s1, s3)
{}

lemma string_le_properties()
    ensures forall s :: string_le(s, s)
    ensures forall s1, s2 :: string_le(s1, s2) &amp;&amp; string_le(s2, s1) ==&gt; s1 == s2
    ensures forall s1, s2, s3 :: string_le(s1, s2) &amp;&amp; string_le(s2, s3) ==&gt; string_le(s1, s3)
    ensures forall s1, s2 :: string_le(s1, s2) || string_le(s2, s1)
{
    string_le_reflexive();
    string_le_antisymmetric();
    string_le_transitive();
    string_le_totality();
}


lemma string_smallest_exists(s: set&lt;string&gt;)
    requires s != {}
    decreases s
    ensures exists x :: x in s &amp;&amp; forall y :: y in s ==&gt; string_le(x, y)
{
    string_le_properties();

    var x :| x in s;

    if s == {x} {
        assert forall y :: y in s ==&gt; y == x;
        assert forall y :: y in s ==&gt; string_le(x, y);
    } else {
        // For sets with more than one element, we use induction-like reasoning
        var s&#39; := s - {x};

        assert s&#39; != {};
        string_smallest_exists(s&#39;);

        var x&#39; :| x&#39; in s&#39; &amp;&amp; forall y :: y in s&#39; ==&gt; string_le(x&#39;, y);
        assert x != x&#39;;

        if string_le(x, x&#39;) {
            assert forall y :: y in s&#39; ==&gt; string_le(x, y);
            assert forall y :: y in s ==&gt; string_le(x, y);
        } else {
            // x&#39; is smaller than x
            assert !string_le(x, x&#39;);
            assert string_le(x&#39;, x);
            assert forall y :: y in s ==&gt; string_le(x&#39;, y);
        }
    }
}

function select_string_from_set(collection: set&lt;string&gt;): string
    requires collection != {}
{
    string_le_properties();
    string_smallest_exists(collection);
    var value :| value in collection &amp;&amp; forall y | y in collection :: string_le(value, y);
    value
}

function iterate_helper(collection: set&lt;string&gt;, acc: seq&lt;string&gt;): seq&lt;string&gt;
    decreases collection
{
    if collection == {} then acc
    else
        var x := select_string_from_set(collection);
        var newAcc := acc + [x];
        var newCollection := collection - {x};
        iterate_helper(newCollection, newAcc)
}
</code></pre>]]></content:encoded>
      <category>Dafny</category>
      <category>Formal methods</category>
      <category>Deterministic algorithm</category>
      <category>Total order</category>
      
    </item>
    
    <item>
      <title>Dealing with Web Scrapers</title>
      <link>https://brandonrozek.com/blog/anti-scraper-techniques/</link>
      <pubDate>Wed, 02 Jul 2025 09:10:23 -0400</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid>https://brandonrozek.com/blog/anti-scraper-techniques/</guid>
      <description><![CDATA[<p>Nowadays it seems like every tech company is eager to scrape the web. Unfortunately, it seems like
<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> the majority of traffic that comes to this small site are scrapers. While my static website is able to handle the load, the same cannot be said about everyone.</p>
<p>Overall, the techinques I&rsquo;ve seen website owners use aim to make scraping more difficult. Though it&rsquo;s a balance. The harder we make it for bots to access a website, the more we turn away regular humans as well. Here&rsquo;s a short and non-exhaustive list of techinques:</p>]]></description>
      <content:encoded><![CDATA[<p>Nowadays it seems like every tech company is eager to scrape the web. Unfortunately, it seems like
<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> the majority of traffic that comes to this small site are scrapers. While my static website is able to handle the load, the same cannot be said about everyone.</p>
<p>Overall, the techinques I&rsquo;ve seen website owners use aim to make scraping more difficult. Though it&rsquo;s a balance. The harder we make it for bots to access a website, the more we turn away regular humans as well. Here&rsquo;s a short and non-exhaustive list of techinques:</p>
<ol>
<li>User Agent Filtering</li>
<li>CAPTCHA solving</li>
<li>Rate Limiting</li>
<li>Proof of work</li>
<li>Identification</li>
<li>Paywall</li>
</ol>
<h3 id="user-agent-filtering">User Agent Filtering</h3>
<p>When a person/bot requests a page from a website, the HTTP header of the request has a field called <code>User-Agent</code>.  This is to denote the type of client that the requester is using. For example, when I visited a website just now, I sent the user agent <code>Mozilla/5.0 (X11; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0</code>.</p>
<p>Filtering based on this string is the easiest technique to employ and also has a low chance of impacting regular humans visiting the website. <a href="https://www.rfc-editor.org/rfc/rfc9309.html">RFC 9309 Robots Exclusion Protocol</a>, more commonly known as <code>robots.txt</code>, is the most common way of implementing this technique.</p>
<p>How it works is that you create a file named <code>robots.txt</code> at the root directory of your website and write a set of rules that different robots <em>should</em> follow. Here&rsquo;s an example from <a href="https://developers.google.com/search/docs/crawling-indexing/robots/create-robots-txt">Google&rsquo;s search documentation</a>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>User-agent: Googlebot
</span></span><span style="display:flex;"><span>Disallow: /nogooglebot/
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>User-agent: *
</span></span><span style="display:flex;"><span>Allow: /
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Sitemap: https://www.example.com/sitemap.xml
</span></span></code></pre></div><p>The <code>*</code> here is the Klenne star which means that it can match any string. Before the bot requests a page, the idea is that they first request this <code>robots.txt</code> file, find the rules that match their user agent, and follow it&rsquo;s instructions.</p>
<p>As you might imagine, not everyone writes scrapers that follow these rules. This depends on how well-written the bot was and how considerate the developer is. An alternative to this approach is to block the request at the web server. For example, here&rsquo;s how you would do that using <code>nginx</code></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#e6db74">(</span>$http_user_agent = <span style="color:#e6db74">&#34;Googlebot&#34;)</span>{
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">return</span> <span style="color:#ae81ff">403</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This returns an empty response with the HTTP code <code>403 Forbidden</code>.</p>
<p>The downside to this approach is that it&rsquo;s easy to pretend that you have a different user agent. For example on my machine, the user agent set by <code>curl</code> is <code>curl/8.9.1</code>. However, I can use the same user agent as my browser by adding a flag:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl --user-agent <span style="color:#e6db74">&#34;Mozilla/5.0 (X11; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0&#34;</span> https://brandonrozek.com
</span></span></code></pre></div><h3 id="captcha-solving">CAPTCHA Solving</h3>
<p>The Completely Automated Public Turing test to tell Computers and Humans Apart (CAPTCHA) is a challenge-response approach to dealing with bots. The idea is that the webserver would present some sort of challenge that is supposedly hard for computers to solve but easy for humans. The human responds to the challenge and then is granted access to the website.</p>
<p>In the paper &ldquo;Recent advances of Captcha security analysis: a short literature review&rdquo; by Nghia Trong Dinh and Vinh Truong Hoang, they show that for the majority of CAPTCHA systems, bots are successful at solving them over 50% of the time. Specifically, the best bots are able to solve Google&rsquo;s image-based CAPTCHAs with 70.78% accuracy.</p>
<p>Unfortunately<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> the success rate of bots are bound to improve over time. Additionally, CAPTCHA systems are annoying to humans. For example, when I use a VPN, I don&rsquo;t bother with Google search since I don&rsquo;t want to select pictures of stairs, fire hydrants, or crosswalks 10 times before being granted a search query.</p>
<h3 id="rate-limiting">Rate Limiting</h3>
<p>Computers are inherently faster than us. In the paper &ldquo;How many words do we read per minute? A review and meta-analysis of reading rate&rdquo; by Marc Brysbaert, he writes that the average human adult reads 238 words per minute of non-fiction silently. Thus, it would take a human on average almost 11 hours to read all my prior blog posts (assuming they don&rsquo;t get tired or distracted). Meanwhile a bot can scrape this site in under a minute.</p>
<p>From this insight, one technique is to limit the number of requests that an IP address can make at any given time. This is formally known as <em>rate limiting</em>.</p>
<p>It sounds simple in concept but can be difficult to implement without impacting user experience. How many requests is a human reasonably allowed to make in a minute? Human traffic is typically bursty, where a page load can request many different files (CSS, JS, media) in a short period of time.  How quick can I expect someone to reasonably click around my website? If this isn&rsquo;t dialed in properly, then rate limiting can cause frustration with your visitors.</p>
<p>I&rsquo;m also unsure how successful this is against the LLM web scrapers. Nowadays there are bot farms where they each have their own IP address. It&rsquo;s difficult to determine whether a request is from a human visitor or part of a larger bot collection network.</p>
<h3 id="proof-of-work">Proof of work</h3>
<p>We talked about how CAPTCHAs are difficult for computers but easy for humans. Proof of work is difficult for both computers and humans. This helps reduce the number of scrapers by making it <em>costly</em> to request resources from the website. By making the web browser solve some proof of work challenge (usually involving hash functions), the request consumes additional CPU cycles and takes additional time.</p>
<p>Similar to rate limiting, how <em>difficult</em> you make the problem has a direct impact on user experience. The more difficult, the longer it&rsquo;ll take for the web browser to solve it. This will deter more bots, but after a few seconds will also deter human visitors. <a href="https://web.archive.org/web/20250121155519/https://www.thinkwithgoogle.com/marketing-strategies/app-and-mobile/page-load-time-statistics/">According to a study performed by Google and SOASTA Research in 2017</a>, if a user has to wait 3 seconds instead of 1 second, then the probability that they <em>bounce</em> (leave the page) increases by 32%.</p>
<p>Recently, open-source projects <a href="https://anubis.techaro.lol/">Anubis</a> and <a href="https://git.gammaspectra.live/git/go-away">go-away</a> gained popularity for making it easy to implement this technique. It&rsquo;s popular for git forges like <a href="https://git.sr.ht/">sourcehut&rsquo;s</a> as scraping those incurs a lot of CPU cycles in traversing git repositories.</p>
<h3 id="identification">Identification</h3>
<p>Another tactic is to ask the requester to provide some information that a human would likely have but a bot less so. Examples include email addresses, phone number, government ID, etc. Of course, a bot can supply false information, but as with the other techinques this adds an additional barrier. Watch out for the <a href="https://gregoryhammond.ca/blog/never-to-connect-phone-numbers-a-project/">fake phone numbers</a>.</p>
<h3 id="paywall">Paywall</h3>
<p>Lastly, you can require users to pay to see the contents of your website. This is popular with news organizations where they ask you to pay for a subscription in order to see content. This ties in well with the previous tactic, because if the user pays for a subscription, then you likely have a lot of identifying information about that user.</p>
<p>Another interesting idea that I haven&rsquo;t seen widely implemented is requiring some amount of money per interaction. This can be in the form of the <a href="https://webmonetization.org/">Web Monetization API</a> or via cryptocurrency like Bitcoin on the <a href="https://lightning.network/">Lightning network</a>. <a href="https://stacker.news/">Stacker news</a> is an example of a Reddit-like platform where users need to pay a small fee in order to upvote a post. The idea is to make it cheap for a human to do on a small scale (like 1 cent per up-vote), but expensive for a bot to do at scale.</p>
<h3 id="conclusion">Conclusion</h3>
<p>We&rsquo;re in a special time period where everyone is fighting to become the top AI company. Long term, I feel that the scraper activity will die down. Similar to how there weren&rsquo;t as many web search scrapers out there.</p>
<p>In the meantime, these are multiple techniques to consider if your website is suffering under heavy load. As for myself, I don&rsquo;t currently implement any of these as my website is mostly static and I haven&rsquo;t noticed my servers being overloaded.</p>
<p>However if you do, I urge you to exercise some caution. For the most part, we share on the web for information to flow freely, and if we&rsquo;re not careful we may drive people away.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>At least I don&rsquo;t think a human using Chrome would try to visit my homepage every minute.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Or fortunately, if we want to get closer to AGI&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded>
      <category>web scraping</category>
      <category>CAPTCHA</category>
      <category>rate limiting</category>
      <category>robots.txt</category>
      <category>proof of work</category>
      
    </item>
    
    <item>
      <title>Exploring via Public Transit</title>
      <link>https://brandonrozek.com/blog/exploring-via-public-transit/</link>
      <pubDate>Sun, 15 Jun 2025 21:38:02 -0400</pubDate>
      <author>brozek@brandonrozek.com (Brandon Rozek)</author>
      <guid>https://brandonrozek.com/blog/exploring-via-public-transit/</guid>
      <description><![CDATA[<p>Last weekend, on my way back north from visiting downtown, I took a break near the Crestview lightrail station. There, I had a wonderful meal at the <a href="https://kurasushi.com/locations/austin-tx-airport-blvd/">Kura revolving sushi bar</a>. Afterwards, I walked around the <a href="https://usa.kinokuniya.com/stores-kinokuniya-austin">Kinokuniya Bookstore</a>. Honestly, this spot is not something I would&rsquo;ve naturally discovered on my own.</p>
<p><img src="/files/images/blog/202506071440.png" alt="Image of Sushi Conveyor Belt"></p>
<p>Using public transportation is a great way to explore the neighborhoods around you. Busses often don&rsquo;t take highways, and instead will take you through areas that you would&rsquo;ve otherwise skipped. You can find many great restaurants to eat in the Bay Area right next to the Mountain View Caltrain station. A few years ago when Clare and I visited Portland, Maine, we got to explore the thousand islands by a <a href="https://www.cascobaylines.com/maine-boat-tours/specialty-cruises/mailboat/">mail boat</a>.</p>]]></description>
      <content:encoded><![CDATA[<p>Last weekend, on my way back north from visiting downtown, I took a break near the Crestview lightrail station. There, I had a wonderful meal at the <a href="https://kurasushi.com/locations/austin-tx-airport-blvd/">Kura revolving sushi bar</a>. Afterwards, I walked around the <a href="https://usa.kinokuniya.com/stores-kinokuniya-austin">Kinokuniya Bookstore</a>. Honestly, this spot is not something I would&rsquo;ve naturally discovered on my own.</p>
<p><img src="/files/images/blog/202506071440.png" alt="Image of Sushi Conveyor Belt"></p>
<p>Using public transportation is a great way to explore the neighborhoods around you. Busses often don&rsquo;t take highways, and instead will take you through areas that you would&rsquo;ve otherwise skipped. You can find many great restaurants to eat in the Bay Area right next to the Mountain View Caltrain station. A few years ago when Clare and I visited Portland, Maine, we got to explore the thousand islands by a <a href="https://www.cascobaylines.com/maine-boat-tours/specialty-cruises/mailboat/">mail boat</a>.</p>
<p>Even if you have a car, it&rsquo;s worth taking a look at the transit maps to see if there are any hidden gems.</p>
]]></content:encoded>
      <category>public transport</category>
      
    </item>
    
  </channel>
</rss>