<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>jfx's site</title><link>https://jfx.ac/</link><description>Recent content in Blogs on jfx's site</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Sat, 11 Oct 2025 00:00:00 +0000</lastBuildDate><atom:link href="https://jfx.ac/blog/index.xml" rel="self" type="application/rss+xml"/><item><title>Moving to Podman Quadlets</title><link>https://jfx.ac/blog/moving-to-podman-quadlets/</link><pubDate>Sat, 11 Oct 2025 00:00:00 +0000</pubDate><guid>https://jfx.ac/blog/moving-to-podman-quadlets/</guid><description>&lt;h2 id="prelude"&gt;Prelude&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;My personal website &amp;amp; blog is &lt;a href="https://jfx.ac"&gt;jfx.ac&lt;/a&gt; where I blog about once a month&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;That was a lie, where have I been? I got back from a holiday, moved to a MacBook at work, switched to GrapheneOS, caught the flu, and have been trying out Linux on some interesting mini PCs. Too many distractions, and I hope to get back into my blogging routine.&lt;/p&gt;
&lt;h2 id="introduction"&gt;Introduction&lt;/h2&gt;
&lt;p&gt;For the past few years I&amp;rsquo;ve been using Podman containers to run most of my services. Before moving to containers, I would install services via a package manager, or build them from source, and run the binaries and manage the lifecycle with &lt;code&gt;systemd&lt;/code&gt; services.&lt;/p&gt;
&lt;p&gt;The move to containers gave huge benefits. It made spinning services quick and easy, but managing the lifecycle of containers and performing mass upgrades for my many services across a couple of hosts felt cumbersome. I wanted to avoid heavy solutions and was yearning for something similar to the past. It was much easier to run a system upgrade and rely on systemd to manage my services.&lt;/p&gt;
&lt;p&gt;I eventually started using a systemd template unit to manage my &lt;code&gt;podman&lt;/code&gt; containers in rootless fashion, and over the years landed on the template below:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-systemd" data-lang="systemd"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# /etc/systemd/system/podman-compose@.service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;%i rootless pod (podman-compose)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Wants&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;After&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;RequiresMountsFor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;%t/containers&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;PODMAN_SYSTEMD_UNIT=%n&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;RestartSec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;20s&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;TimeoutStartSec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;2min&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;User&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;%i&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;RemainAfterExit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/etc/podman/%i&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/local/bin/podman-compose up -d --remove-orphans&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ExecStop&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/local/bin/podman-compose down&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;default.target&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The interface was familiar, and I could simply run &lt;code&gt;systemctl status podman-compose@homeassistant.service&lt;/code&gt; to manage my container for &lt;a href="https://www.home-assistant.io/"&gt;Home Assistant&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The term after the &lt;code&gt;@&lt;/code&gt; and before the type-suffix (in this case &lt;code&gt;.service&lt;/code&gt;) is the instance of the unit, and is substituted in the &lt;code&gt;%i&lt;/code&gt; variable in the definition. This substitution is used twice: once for the &lt;code&gt;User=&lt;/code&gt; which runs the container, and a second time in the &lt;code&gt;WorkingDirectory=&lt;/code&gt; path. This resulted in a simple and standardised way of managing rootless containers.&lt;/p&gt;
&lt;p&gt;The unit file above worked OK, but occasionally after a reboot some containers would fail to start - which would annoyingly require manual intervention.&lt;/p&gt;
&lt;p&gt;It always felt like I was forcing Podman into a &lt;code&gt;systemd&lt;/code&gt; shape it wasn’t designed for.&lt;/p&gt;
&lt;h2 id="enter-quadlets"&gt;Enter Quadlets&lt;/h2&gt;
&lt;p&gt;Quadlets integrate Podman directly with systemd, making containers behave like first-class system services.&lt;/p&gt;
&lt;h3 id="what-are-quadlets"&gt;What are Quadlets?&lt;/h3&gt;
&lt;p&gt;The official documentation says:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Quadlet is a tool for running Podman containers under systemd in an optimal way by allowing containers to run under systemd in a declarative way.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;The (now frozen) Quadlet repository gives a longer explanation:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Containers are often used in a cloud context, and they are then used in combination with an orchestrator like Kubernetes. They are also commonly used during development and testing to manually manage containers on an ad-hoc basis.&lt;/p&gt;
&lt;p&gt;However, there are also use cases where you want some kind of automatic container management, but on a smaller, single-node scale, and often more tightly integrated with the rest of the system. Typical examples of this can be embedded or automotive use, where there is no system administrator, or disconnected or edge servers.&lt;/p&gt;
&lt;p&gt;The recommended way to do this is to use systemd to orchestrate the containers, since this is an already running process manager, and since podman containers are just child processes. There are many documents that describe how to use podman with systemd directly, but the end result are generally large, hard to maintain systemd config files. And often the container setup isn&amp;rsquo;t optimal.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;Basically, anywhere you want to run a containerised system service without requiring human intervention, it&amp;rsquo;s wise to use &lt;code&gt;systemd&lt;/code&gt; via Quadlets to manage your locally running Podman containers.&lt;/p&gt;
&lt;p&gt;Because systemd carefully manages your container, you can rest easy that the lifecycle is tightly coupled to systemd.&lt;/p&gt;
&lt;h3 id="availability"&gt;Availability&lt;/h3&gt;
&lt;p&gt;Quadlets were first available to the public via their own repository on September 27th 2021. The project was then merged into Podman, and first released in Podman 4.4 on February 1st 2023.&lt;/p&gt;
&lt;p&gt;Many of my hosts had Debian 12 bookworm installed, which shipped with Podman 4.3.1, missing the cutoff for Quadlet support. Debian 13 trixie, which was recently released on August 9th 2025 included Podman 5.4.2, which has support for Quadlets.&lt;/p&gt;
&lt;h3 id="getting-started-with-quadlets"&gt;Getting started with Quadlets&lt;/h3&gt;
&lt;p&gt;An example of a Quadlet container is:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-systemd" data-lang="systemd"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# sleep@.container&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;The sleep container&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;After&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;local-fs.target&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Container]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;registry.access.redhat.com/ubi9-minimal:latest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Exec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;sleep %i&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The definition uses the familiar &lt;code&gt;systemd&lt;/code&gt; unit file format, and the attributes under the &lt;code&gt;[Container]&lt;/code&gt; section match those of a Docker Compose file. The complete spec for the &lt;code&gt;[Container]&lt;/code&gt; section is available here: &lt;a href="https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html#container-units-container"&gt;https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html#container-units-container&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;A benefit of Quadlets is the containers have specific directories they will live, these are:&lt;/p&gt;
&lt;p&gt;For system container files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/run/containers/systemd/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/etc/containers/systemd/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/usr/share/containers/systemd/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For rootless container files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;$XDG_RUNTIME_DIR/containers/systemd/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$XDG_CONFIG_HOME/containers/systemd/ or ~/.config/containers/systemd/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/etc/containers/systemd/users/$(UID)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/etc/containers/systemd/users/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once you place the &lt;code&gt;sleep@.container&lt;/code&gt; in one of the directories - you can then run &lt;code&gt;systemctl {--user} daemon-reload&lt;/code&gt; followed by &lt;code&gt;systemctl {--user} start sleep@1000.container&lt;/code&gt; to start the container.&lt;/p&gt;
&lt;h2 id="moving-to-quadlets"&gt;Moving to Quadlets&lt;/h2&gt;
&lt;p&gt;I was keen to use Quadlets and quickly upgraded my hosts to Debian 13 to leverage the capability. As one can imagine, the biggest obstacle is converting compose files to the Quadlet unit service file format.&lt;/p&gt;
&lt;p&gt;To make this easier, there&amp;rsquo;s an open-source tool available called &lt;a href="https://github.com/containers/podlet"&gt;podlet&lt;/a&gt;, created by the containers community, which outputs Quadlet files from an existing Podman command or compose file. I had about 20 services to convert and &lt;code&gt;podlet&lt;/code&gt; made it a very quick process.&lt;/p&gt;
&lt;p&gt;My existing Home Assistant container had some complex device and group mappings to avoid running in privileged mode, but I was able to migrate without adjusting any of the output from &lt;code&gt;podlet&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;My old compose file looked like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;3.8&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;homeassistant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;homeassistant&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ghcr.io/home-assistant/home-assistant:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;crun&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;group_add&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;keep-groups&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;TZ=Australia/Melbourne&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;PYTHONPATH=/config/deps&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;/etc/podman/homeassistant/homeassistantconfig:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;network_mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;host&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;/dev/ttyUSB0:/dev/ttyUSB0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# zigbee USB&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;journald&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To migrate I ran:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;podlet compose /etc/podman/homeassistant/compose.yaml &amp;gt; /etc/containers/systemd/users/1001/homeassistant.container
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I wanted the container to auto-start on boot and recover automatically, so I added to the bottom of the service file the following snippet:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-systemd" data-lang="systemd"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="na"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="na"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target default.target&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This resulted in the following Quadlet container file, which lives in &lt;code&gt;/etc/containers/systemd/users/1001/homeassistant.container&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-systemd" data-lang="systemd"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# homeassistant.container&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Container]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;AddDevice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/dev/ttyUSB0:/dev/ttyUSB0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ContainerName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;homeassistant&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;TZ=Australia/Melbourne PYTHONPATH=/config/deps&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ghcr.io/home-assistant/home-assistant:latest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;LogDriver&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;journald&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Network&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;host&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;PodmanArgs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;--group-add keep-groups&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/etc/podman/homeassistant/homeassistantconfig:/config&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;GlobalArgs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;--runtime crun&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;AutoUpdate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;registry&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target default.target&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now the container is able to be managed by systemd as my rootless user, &lt;code&gt;homeassistant&lt;/code&gt; which has UID &lt;code&gt;1001&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl --user --daemon-reload
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl --user start homeassistant.service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl --user status homeassistant.service
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="handling-upgrades"&gt;Handling upgrades&lt;/h2&gt;
&lt;p&gt;I also wanted to be able to check and apply upgrades easily, so for all my containers I add &lt;code&gt;AutoUpdate=Registry&lt;/code&gt; under the &lt;code&gt;[Container]&lt;/code&gt; section. This is to use the &lt;a href="https://docs.podman.io/en/latest/markdown/podman-auto-update.1.html"&gt;podman auto-update&lt;/a&gt; feature.&lt;/p&gt;
&lt;p&gt;Don&amp;rsquo;t let the name fool you - it only auto-upgrades if you enable the &lt;code&gt;podman-auto-update.timer&lt;/code&gt;. I wanted to avoid auto-upgrades as many services (such as Home Assistant) have breaking changes on upgrade. I like my lamps working in my home unless I choose to break them :)&lt;/p&gt;
&lt;p&gt;You can check for upgrades with a simple command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;podman auto-update --dry-run --format &lt;span class="s1"&gt;&amp;#39;{{.Image}} {{.Updated}}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And apply them when needed:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;podman auto-update
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="how-things-are-a-month-later"&gt;How things are a month later&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve been using Quadlets for well over a month and my systemd units are still running:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;homeassistant@medusa:~$ systemctl status --user homeassistant.service
● homeassistant.service
Loaded: loaded (/etc/containers/systemd/users/1001/homeassistant.container; generated)
Active: active (running) since Tue 2025-08-26 00:31:53 AEST; 1 month 16 days ago
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I don&amp;rsquo;t regret the move and am so glad this technology is available. It makes self-hosting so much easier. Hope this helps others who have a similar need.&lt;/p&gt;
&lt;h2 id="see-also"&gt;See also&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html"&gt;https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.redhat.com/en/blog/quadlet-podman"&gt;https://www.redhat.com/en/blog/quadlet-podman&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>My KVM is better than yours</title><link>https://jfx.ac/blog/my-kvm-is-better-than-yours/</link><pubDate>Fri, 18 Apr 2025 00:00:00 +0000</pubDate><guid>https://jfx.ac/blog/my-kvm-is-better-than-yours/</guid><description>&lt;p&gt;Damn right, it&amp;rsquo;s better than yours.&lt;/p&gt;
&lt;h2 id="introduction"&gt;Introduction&lt;/h2&gt;
&lt;p&gt;My KVM (Keyboard, video, mouse) switch setup lets me use my home desktop and work laptop with ease.&lt;/p&gt;
&lt;p&gt;Without giving away &lt;em&gt;too much&lt;/em&gt; of the implementation (don&amp;rsquo;t worry, I&amp;rsquo;ll tell you in a bit), the key features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;USB 3.0 device support&lt;/li&gt;
&lt;li&gt;Can control with an API :)&lt;/li&gt;
&lt;li&gt;Controllable via Stream Deck or keyboard bindings&lt;/li&gt;
&lt;li&gt;Supports HDMI/DisplayPort to any supported resolution and refresh rate of the monitor
&lt;ul&gt;
&lt;li&gt;My 240hz 1080p monitor works great&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;No display disconnecting
&lt;ul&gt;
&lt;li&gt;On Windows, if you disconnect a display, windows get moved to other monitors. This is incredibly annoying and does not happen on my setup&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Relatively quick&lt;/li&gt;
&lt;li&gt;Cheap
&lt;ul&gt;
&lt;li&gt;My end costs were $40 AUD&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Things I wanted to avoid:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Software-based KVM solutions - my home and work machines should not be aware of each other. I don&amp;rsquo;t feel comfortable sending keystrokes across machines.&lt;/li&gt;
&lt;li&gt;An external monitor switcher hub device
&lt;ul&gt;
&lt;li&gt;Mainly due to the lack of 240hz support and display disconnecting&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Spending lots of money
&lt;ul&gt;
&lt;li&gt;How do I justify the need to spend $300 on a high quality KVM to my company? or myself?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Slowness&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here&amp;rsquo;s how I did it&lt;/p&gt;
&lt;h2 id="handling-usb"&gt;Handling USB&lt;/h2&gt;
&lt;p&gt;To switch my USB devices, I got a boring USB 3.0 hub which can switch between two machines from Amazon. It took a day to arrive, and costs about ~$30 AUD on sale.&lt;/p&gt;
&lt;p&gt;I bought the popular &lt;a href="https://www.amazon.com.au/UGREEN-Computers-Peripheral-Switcher-One-Button/dp/B01N6GD9JO"&gt;UGREEN USB 3.0 Switch Selector&lt;/a&gt;. I&amp;rsquo;ve tried about 3 other ones and none were as good as this for the price, performance, and USB 3.0 support.&lt;/p&gt;
&lt;p&gt;The USB hub comes with a toggle button on the top and no API. This worked well for a while, but I got tired of leaning across the table to press the button. I wanted it to have an API so I could switch it via my &lt;a href="https://www.elgato.com/ww/en/s/welcome-to-stream-deck"&gt;Stream Deck&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I bought a second USB hub, pulled it apart, and saw the electronics are simple. There&amp;rsquo;s a tactile switch which grounds a circuit momentarily to &amp;ldquo;toggle&amp;rdquo; which machine the device is serving USB to. Playing around with a multi-meter I was able to toggle the connected machine easily.&lt;/p&gt;
&lt;a href="https://jfx.ac/assets/blog/kvm-better/disassembled.png" target="_blank"&gt;
&lt;img src="https://jfx.ac/assets/blog/kvm-better/disassembled.png" alt="" style="max-width: min(95vw, 120%)"&gt;
&lt;/a&gt;
&lt;p&gt;I had a Raspberry Pico lying around ($10 AUD) so I soldered a cable to the PCB in the spot to ground. I then wrote a simple program to quickly ground a pin on the Pico and connected the soldered cable to the Pico.&lt;/p&gt;
&lt;p&gt;To my amazement the program actually worked:&lt;/p&gt;
&lt;a href="https://jfx.ac/assets/blog/kvm-better/trial.gif" target="_blank"&gt;
&lt;img src="https://jfx.ac/assets/blog/kvm-better/trial.gif" alt="" style="max-width: min(95vw, 120%)"&gt;
&lt;/a&gt;
&lt;p&gt;Next I wrote a dirty HTTP server, connected it to my WiFi, and I was able to do the same via &lt;code&gt;curl&lt;/code&gt;. For $40 (Pico + USB hub), I had a USB 3.0 hub I could control with an API.&lt;/p&gt;
&lt;h2 id="handling-displays"&gt;Handling Displays&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;d tried an external device for switching between displays, but due to my monitors high refresh rate and Windows freaking out when displays were disconnected I found it not suitable. Luckily, I discovered that it&amp;rsquo;s possible to control displays with software.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a not well-known protocol called &lt;a href="https://en.wikipedia.org/wiki/Display_Data_Channel"&gt;Display Data Channel (DDC)&lt;/a&gt; developed by the Video Electronics Standards Association (VESA). DDC is essentially a bus that allows for digital communication between a computer display and video card.&lt;/p&gt;
&lt;p&gt;Separately, there&amp;rsquo;s another protocol called &lt;a href="https://en.wikipedia.org/wiki/Monitor_Control_Command_Set"&gt;Monitor Control Command Set (MCCS)&lt;/a&gt; which allows for controlling the properties of a monitor. Using DDC as a data channel, you can send MCCS messages bidirectionally between a computer and monitor.&lt;/p&gt;
&lt;p&gt;With MCCS, you can do things like adjust the brightness or contrast of your monitor - but even more useful, you can set the current input of the monitor! The same as pressing the input button on your monitor yourself.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a ton of software based implementations of MCCS via DDC on the internet. I decided to use &lt;a href="https://github.com/newAM/monitorcontrol"&gt;monitorcontrol&lt;/a&gt; cause it was written in Python and worked across many platforms.&lt;/p&gt;
&lt;p&gt;With a simple shell command, I could set my monitor to HDMI for my work laptop: &lt;code&gt;monitorcontrol --monitor=2 --set-input-source HDMI1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The best part is this is completely free, so you do not need to pay for another device. You only need cables for each display and device.&lt;/p&gt;
&lt;h2 id="integrating-usb-and-display"&gt;Integrating USB and Display&lt;/h2&gt;
&lt;p&gt;I wanted to add custom buttons to my Stream Deck which allowed switching between my devices. To do this, I use this &lt;a href="https://github.com/abcminiuser/python-elgato-streamdeck"&gt;third-party Stream Deck Python library&lt;/a&gt; as Elgato&amp;rsquo;s Stream Deck software doesn&amp;rsquo;t work on Linux (shakes fist).&lt;/p&gt;
&lt;p&gt;I added two buttons on my StreamDeck, one for switching to my laptop, and one for switching to my desktop. Both buttons effectively:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Send a web request to my Raspberry PI Pico to toggle the switch&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;monitorcontrol&lt;/code&gt; to set the device appropriately&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I also created a third button that toggles only the USB device.&lt;/p&gt;
&lt;p&gt;The reason why I wanted a button for each machine and not a toggle button - is cause displays get disconnected due to device sleep or cables slipping, so I had the need to explicitly select which device to switch to.&lt;/p&gt;
&lt;p&gt;Unfortunately, I had no way of keeping track of which USB device my KVM connected to. This is cause the Pico has no state and grounds the pin whenever it receives a web request. I thought about tapping into the light on my KVM and using that for state management, then I realised there&amp;rsquo;s a simpler solution.&lt;/p&gt;
&lt;p&gt;My Stream Deck is always directly connected to my desktop (not via KVM), and my desktop machine is always on. Due to this I could cheat a little.&lt;/p&gt;
&lt;p&gt;When I press the button on my Stream Deck, my script runs &lt;code&gt;lsusb&lt;/code&gt; and checks whether the USB hub is listed as a connected device. If I press the button to switch to my desktop, and the USB hub is connected, then there&amp;rsquo;s no need to call my Pico API to toggle the USB hub as the USB hub is connected to the correct device. If the USB hub did not appear in &lt;code&gt;lsusb&lt;/code&gt;, then this would mean my USB hub was connected to my laptop, so my Stream Deck code correctly makes a web request to my Pico API to toggle the USB hub.&lt;/p&gt;
&lt;p&gt;Cause of this cheat, I had a way to reliably detect whether the USB device was connected to the right PC or not, and only switch when appropriate. I combined this with &lt;code&gt;monitorcontrol&lt;/code&gt;, and with a single button press I have a way to switch my display and USB devices.&lt;/p&gt;
&lt;a href="https://jfx.ac/assets/blog/kvm-better/final.gif" target="_blank"&gt;
&lt;img src="https://jfx.ac/assets/blog/kvm-better/final.gif" alt="" style="max-width: min(95vw, 120%)"&gt;
&lt;/a&gt;
&lt;h2 id="a-simpler-alternative"&gt;A simpler alternative&lt;/h2&gt;
&lt;p&gt;I reflected on my KVM setup and I realised that there is a fairly simple alternative which does not require soldering or taking apart the UGREEN KVM.&lt;/p&gt;
&lt;p&gt;Using &lt;a href="https://en.wikipedia.org/wiki/Udev"&gt;udev rules&lt;/a&gt;, you can detect when a device is connected or disconnected from your Linux machine and run a shell command. You can create these rules on a single &amp;ldquo;main&amp;rdquo; machine - and listen to events on the USB hub device. When disconnected, you could run &lt;code&gt;monitorcontrol&lt;/code&gt; to switch the display input to your other machine - and when connected switch back to the main machines display input.&lt;/p&gt;
&lt;p&gt;For the UGREEN KVM, here are two rules which implement the above:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;ACTION&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;add&amp;#34;&lt;/span&gt;, &lt;span class="nv"&gt;SUBSYSTEM&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;usb&amp;#34;&lt;/span&gt;, ENV&lt;span class="o"&gt;{&lt;/span&gt;DEVTYPE&lt;span class="o"&gt;}==&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;usb_device&amp;#34;&lt;/span&gt;, ENV&lt;span class="o"&gt;{&lt;/span&gt;PRODUCT&lt;span class="o"&gt;}==&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;5e3/610/663&amp;#34;&lt;/span&gt;, &lt;span class="nv"&gt;RUN&lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;/bin/su -c &amp;#39;/home/jfx/.local/bin/monitorcontrol --monitor=2 --set-input-source DP1&amp;#39; jfx&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;ACTION&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;remove&amp;#34;&lt;/span&gt;, &lt;span class="nv"&gt;SUBSYSTEM&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;usb&amp;#34;&lt;/span&gt;, ENV&lt;span class="o"&gt;{&lt;/span&gt;DEVTYPE&lt;span class="o"&gt;}==&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;usb_device&amp;#34;&lt;/span&gt;, ENV&lt;span class="o"&gt;{&lt;/span&gt;PRODUCT&lt;span class="o"&gt;}==&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;5e3/610/663&amp;#34;&lt;/span&gt;, &lt;span class="nv"&gt;RUN&lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;/bin/su -c &amp;#39;/home/jfx/.local/bin/monitorcontrol --monitor=2 --set-input-source HDMI1&amp;#39; jfx&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is what works for me as I have &lt;code&gt;monitorcontrol&lt;/code&gt; installed in my local users Python installation.&lt;/p&gt;
&lt;p&gt;This setup requires you to press the button on the KVM, but saves you from needing to switch your monitor input yourself. It also saves you from soldering!&lt;/p&gt;
&lt;p&gt;This will also only work on Linux due to &lt;code&gt;udev&lt;/code&gt;, but I&amp;rsquo;m sure there&amp;rsquo;s a similar implementation for other operating systems.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;A big factor of working from home is being comfortable, so it pays to have a setup to switch between your devices. You don&amp;rsquo;t need to perform brain surgery on your USB hub to be happy. If your company gives you an allowance for equipment, definitely try out a switchable USB hub with a button. I highly recommend the UGREEN one.&lt;/p&gt;
&lt;p&gt;There is no financial cost of using free software like &lt;code&gt;monitorcontrol&lt;/code&gt; - the only cost is your time. Try it out on your display and perhaps create a key-binding or two to switch between your display inputs.&lt;/p&gt;
&lt;p&gt;I also enjoyed the opportunity to learn more about electronics and use a Pico to solve a problem. I&amp;rsquo;m not the strongest electronics person so this was a great learning project. Thank you to all the friends who helped out.&lt;/p&gt;
&lt;h2 id="the-code"&gt;The code&lt;/h2&gt;
&lt;p&gt;The code for my Stream Deck is available here: &lt;a href="https://github.com/itsjfx/dotfiles/blob/24a6e418244909d059a4d057645af0bb1f6996bc/lib/streamdeck.py#L144-L156"&gt;https://github.com/itsjfx/dotfiles/blob/24a6e418244909d059a4d057645af0bb1f6996bc/lib/streamdeck.py#L144-L156&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The code for my Pico API is available in this Gist: &lt;a href="https://gist.github.com/itsjfx/647cb9cb142cb4eb3233214ec6a50229"&gt;https://gist.github.com/itsjfx/647cb9cb142cb4eb3233214ec6a50229&lt;/a&gt;&lt;/p&gt;
&lt;script src="https://gist.github.com/itsjfx/647cb9cb142cb4eb3233214ec6a50229.js"&gt;&lt;/script&gt;</description></item><item><title>Engineering project management</title><link>https://jfx.ac/blog/engineering-project-management/</link><pubDate>Fri, 21 Feb 2025 00:00:00 +0000</pubDate><guid>https://jfx.ac/blog/engineering-project-management/</guid><description>&lt;h2 id="introduction"&gt;Introduction&lt;/h2&gt;
&lt;p&gt;As a Senior Engineer, one of my duties is breaking down large pieces of work into ordered and prioritised set of tasks, each scoped appropriately, ideally sized no more than 1 - 3 days.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s unlikely each task initially discussed during a planning session will be part of the final set, nor is it likely they appear in the initial ordering. As sessions progress, tasks may need to be re-prioritised, dependencies between tasks may get raised, and the scope of existing tasks may grow or shrink as newer tasks are discussed. It&amp;rsquo;s great to have a quick workflow which deals with these inevitable changes as it will result in more productive sessions.&lt;/p&gt;
&lt;p&gt;I found working in a text box driven GUI slow as I had to repetitively open dialogs, move my mouse between text boxes, type words, and submit.&lt;/p&gt;
&lt;p&gt;I was looking for a way to work smarter, do less tedious project management manual processes, and focus on solving real engineering problems.&lt;/p&gt;
&lt;p&gt;For these reasons I made &lt;a href="https://github.com/itsjfx/scrumfaster"&gt;scrumfaster&lt;/a&gt;, which allows you to create GitHub issues and project items with Markdown.&lt;/p&gt;
&lt;p&gt;The initial inspiration of using Markdown (other than it being great) came from an initial planning session of a project where my client had no project board set up, so we wrote a high level plan of the next few months in markdown. The client did not want to pay for or set up a Jira instance, so we decided to trial GitHub projects.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/itsjfx/scrumfaster"&gt;scrumfaster&lt;/a&gt; was initially a disgusting 50-line Bash script I wrote on a Friday night to get the tasks from that markdown doc into GitHub. From there we continued using it to create new sprints and bootstrap future projects. I decided it needed a rewrite in Python, which would allow more advanced features and less bugs, so I did that and published it online.&lt;/p&gt;
&lt;h2 id="a-showcase"&gt;A showcase&lt;/h2&gt;
&lt;p&gt;Below is an example of some markdown which &lt;code&gt;scrumfaster&lt;/code&gt; will accept (also referenced in the repo):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-markdown" data-lang="markdown"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;# my cool board
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gh"&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## Sprint 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;* [ ]&lt;/span&gt; Profile avatars
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;* [ ]&lt;/span&gt; Create database migration for avatar field [&lt;span class="ni"&gt;@itsjfx&lt;/span&gt;] [status=Done] [labels=database] [1]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;*&lt;/span&gt; Name the field &lt;span class="sb"&gt;`avatar`&lt;/span&gt; in the &lt;span class="sb"&gt;`users`&lt;/span&gt; table
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;*&lt;/span&gt; Set value for existing users to https://...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;* [ ]&lt;/span&gt; Accept avatar parameter in &lt;span class="sb"&gt;`update_user`&lt;/span&gt; API call [&lt;span class="ni"&gt;@itsjfx&lt;/span&gt;] [labels=api] [1]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;*&lt;/span&gt; Use existing image upload mechanisms
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;*&lt;/span&gt; Limit image size to 10mb
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;* [ ]&lt;/span&gt; Display and allow updating avatars on frontend [labels=frontend] [2]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;*&lt;/span&gt; Only display avatars on the users public profile page
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;*&lt;/span&gt; Thumbnails aside comments to be implemented in later card
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;* [ ]&lt;/span&gt; Dark mode
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;* [ ]&lt;/span&gt; Add ui toggle for dark mode [labels=frontend] [1]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;*&lt;/span&gt; Store preference in local storage
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;* [ ]&lt;/span&gt; Implement styles [labels=frontend] [2]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;*&lt;/span&gt; Apply styles dynamically based on user preference
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;* [ ]&lt;/span&gt; Delete jeff from database [labels=database] [1]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Some key notes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the nested tasks are syntactic sugar to create a task for each of the lowest children which the parent tasks as prefixes
&lt;ul&gt;
&lt;li&gt;e.g. &lt;code&gt;Profile avatars: Create database migration for avatar field&lt;/code&gt; would be the first task created&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;you can specify assignees, labels, points, and status in-line of the card&lt;/li&gt;
&lt;li&gt;milestones can be specified as markdown headings&lt;/li&gt;
&lt;li&gt;you can create GitHub issues, or GitHub project draft items&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The result is a natural and readable markdown representation of a project.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s much more information on the repository, so if this got your interest please check it out: &lt;a href="https://github.com/itsjfx/scrumfaster"&gt;https://github.com/itsjfx/scrumfaster&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="always-be-an-engineer"&gt;Always be an Engineer&lt;/h2&gt;
&lt;p&gt;The main takeaway of this post is not the cool project I wrote, but that engineering should span across everything you do. Sadly, as you get promoted, you will find yourself getting more duties, and doing less engineering work. You&amp;rsquo;ll be responsible for people, be more involved in your companies politics, and spend a lot more time in meetings.&lt;/p&gt;
&lt;p&gt;I believe a good engineer will be able to find ways to make their non-technical or tedious work exciting by identifying clear gaps and iteratively creating a solution which makes them more productive. You may be able to complete additional technical work or other important non-technical tasks as a result. More importantly, you will continue practising your engineering skills and demonstrate your competency to your peers. Never lose the engineer within you as you become more senior, but you must not neglect your key duties.&lt;/p&gt;
&lt;h2 id="see-also"&gt;See also&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/mheap/markdown-to-jira"&gt;https://github.com/mheap/markdown-to-jira&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=_E0IBNvAXX8"&gt;Are we losing the engineering part of software engineering?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Debugging my stupidity with mitmproxy</title><link>https://jfx.ac/blog/debugging-my-stupidity-with-mitmproxy/</link><pubDate>Tue, 28 Jan 2025 00:00:00 +0000</pubDate><guid>https://jfx.ac/blog/debugging-my-stupidity-with-mitmproxy/</guid><description>&lt;h2 id="introduction"&gt;Introduction&lt;/h2&gt;
&lt;p&gt;A colleague showed me a Neovim plugin that displayed errors from their LSP on virtual lines below their code, instead of adjacent to the code which is the default in Neovim. This interested me as I&amp;rsquo;d never thought about customising how errors are displayed in my Neovim.&lt;/p&gt;
&lt;p&gt;I opened up ChatGPT and had a quick conversation on which plugins were available for doing customising error messages. To my surprise, ChatGPT suggested the same plugin that my colleague had just shown me. I then asked ChatGPT to tell me how to install the plugin with my plugin manager, Lazy.vim. What followed was a waste of 15 minutes of my day.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The problem&lt;/h2&gt;
&lt;p&gt;The plugin my colleague showed me was &lt;a href="https://git.sr.ht/~whynothugo/lsp_lines.nvim"&gt;lsp_lines.nvim&lt;/a&gt;. It looks like this: &lt;a href="https://jfx.ac/assets/blog/debugging-my-stupidity-with-mitmproxy/lsp_lines.png" target="_blank"&gt;
&lt;img src="https://jfx.ac/assets/blog/debugging-my-stupidity-with-mitmproxy/lsp_lines.png" alt="" style="max-width: min(95vw, 120%)"&gt;
&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;I looked at the README and was convinced to try it out, so I asked ChatGPT to &amp;ldquo;help me install &lt;code&gt;lsp-lines.nvim&lt;/code&gt;&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;ChatGPT then suggested I add the following to my Lua config for &lt;code&gt;lazy.nvim&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-lua" data-lang="lua"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;https://git.sr.ht/~whynothugo/lsp-lines.nvim&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;lsp_lines&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I eyeballed the README for the project and it looked OK, so I slapped it in my Neovim config and tried it out.&lt;/p&gt;
&lt;p&gt;After restarting Neovim, I got a HTTP 403 error during cloning the repository from Sourcehut.&lt;/p&gt;
&lt;a href="https://jfx.ac/assets/blog/debugging-my-stupidity-with-mitmproxy/bad-clone.png" target="_blank"&gt;
&lt;img src="https://jfx.ac/assets/blog/debugging-my-stupidity-with-mitmproxy/bad-clone.png" alt="" style="max-width: min(95vw, 120%)"&gt;
&lt;/a&gt;
&lt;p&gt;Not what I was expecting. I had no clue why Sourcehut would give me a 403 for a repository that clearly existed! It couldn&amp;rsquo;t be SSH keys as I&amp;rsquo;d specified a HTTP URL. My first thought was Lazy was appending &lt;code&gt;.git&lt;/code&gt; to the URL which works on GitHub but not on Sourcehut.&lt;/p&gt;
&lt;p&gt;I looked at my colleagues config and his looked the same, and we also had the same version of Lazy installed.&lt;/p&gt;
&lt;p&gt;Next, I went to the directory where Lazy clones plugins, copied the URL for the repository from my browser, and ran &lt;code&gt;git clone&lt;/code&gt; &amp;hellip; and that worked! But when I opened Neovim, it tried cloning the repository and it failed again.&lt;/p&gt;
&lt;p&gt;I checked the source code for Lazy and saw it ran &lt;code&gt;git clone&lt;/code&gt;, but didn&amp;rsquo;t seem to modify the URL, so my &lt;code&gt;.git&lt;/code&gt; hypothesis was unlikely. I found it odd that Lazy would attempt cloning the repository even though the repository was on my machine, maybe cause I&amp;rsquo;d cloned it manually?&lt;/p&gt;
&lt;p&gt;I needed to get more information on what was happening when it reached out to Sourcehut.&lt;/p&gt;
&lt;h2 id="enter-mitmproxy"&gt;Enter mitmproxy&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve wrote about &lt;a href="https://mitmproxy.org/"&gt;mitmproxy&lt;/a&gt; before &lt;a href="https://jfx.ac/blog/homelab-mikrotik-inspect-network/"&gt;when I was hacking IoT devices&lt;/a&gt;. It can be used to look at the HTTP requests Lazy makes when pulling the plugin.&lt;/p&gt;
&lt;p&gt;First we need to start capturing traffic. I have an alias that runs &lt;code&gt;mitmdump --flow-detail=4 --showhost --set hardump=/tmp/dump.har&lt;/code&gt;, which outputs traffic to the terminal in real time and will write a &lt;a href="https://en.wikipedia.org/wiki/HAR_(file_format)"&gt;HAR file&lt;/a&gt; (essentially a &lt;code&gt;HTTP&lt;/code&gt; traffic dump in &lt;code&gt;JSON&lt;/code&gt; format) on exit.&lt;/p&gt;
&lt;h2 id="enter-mitmwrap"&gt;Enter mitmwrap&lt;/h2&gt;
&lt;p&gt;Next, we need Neovim to send its traffic through the proxy, and also make sure Neovim trusts the SSL certificate for &lt;code&gt;mitmproxy&lt;/code&gt;. I have a script &lt;a href="https://github.com/itsjfx/dotfiles/blob/master/bin/mitmwrap"&gt;in my dotfiles called mitmwrap&lt;/a&gt; for this.&lt;/p&gt;
&lt;p&gt;The usage is &lt;code&gt;mitmwrap [MITM_ARGS] PROGRAM [ARGS]&lt;/code&gt;. It currently has two modes: setting the &lt;code&gt;HTTP_PROXY&lt;/code&gt; variables to point to my &lt;code&gt;mitmproxy&lt;/code&gt;, or using &lt;code&gt;proxychains&lt;/code&gt; to force an application to use &lt;code&gt;mitmproxy&lt;/code&gt;. I plan on adding &lt;a href="https://github.com/hmgle/graftcp"&gt;graftcp&lt;/a&gt; support later.&lt;/p&gt;
&lt;p&gt;The script attempts to &lt;a href="https://github.com/itsjfx/dotfiles/blob/2e76936d6f4f01cc7d3ab840dac9c2a438d6d03f/bin/mitmwrap#L38"&gt;set common environment variables&lt;/a&gt; used by programs to look up the systems SSL store. I point these to the &lt;code&gt;mitmproxy&lt;/code&gt; SSL certificate. Lastly, it runs the program in a lightweight container using &lt;a href="https://github.com/containers/bubblewrap"&gt;bubblewrap&lt;/a&gt; , with &lt;code&gt;/etc/ssl/certs/ca-certificates.crt&lt;/code&gt; mounted to our &lt;code&gt;mitmproxy&lt;/code&gt; certificate in case the program does not use any of those earlier environment variables.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;bubblewrap&lt;/code&gt; not only has a cool name, but is also a cool program, and is used internally by Flatpak to sandbox applications.&lt;/p&gt;
&lt;p&gt;Why are we using bubblewrap? We aren&amp;rsquo;t using it to sandbox our program for security reasons, but cause &lt;code&gt;/etc/ssl/certs/ca-certificates.crt&lt;/code&gt; is owned by &lt;code&gt;root&lt;/code&gt;. I don&amp;rsquo;t want to run commands as &lt;code&gt;root&lt;/code&gt; to change the file when I should be able to do everything in user-land.&lt;/p&gt;
&lt;p&gt;I also don&amp;rsquo;t want &lt;code&gt;mitmproxy&lt;/code&gt;&amp;rsquo;s certificate in my systems cert store as it affects every program on my system, and is a potential security risk.&lt;/p&gt;
&lt;p&gt;Instead &lt;code&gt;bubblewrap&lt;/code&gt; can be run with &lt;code&gt;--dev-bind / /&lt;/code&gt; which is &lt;a href="https://wiki.archlinux.org/title/Bubblewrap#No-op"&gt;supposedly a no-op&lt;/a&gt;, before giving &lt;code&gt;--ro-bind MITM_CERT_FILE /etc/ssl/certs/ca-certificates.crt&lt;/code&gt; to bind the SSL file in read-only mode. There&amp;rsquo;s a lot of options that could be set, but I set as little as possible to not break any programs.&lt;/p&gt;
&lt;p&gt;The time to launch &lt;code&gt;bubblewrap&lt;/code&gt; is a few milliseconds which is great for this use case. There&amp;rsquo;s a &lt;a href="https://jvns.ca/blog/2022/06/28/some-notes-on-bubblewrap/"&gt;detailed blog on bubblewrap performance&lt;/a&gt; and using it as a Docker replacement if you&amp;rsquo;re curious.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve ran my &lt;code&gt;mitmwrap&lt;/code&gt; script a lot to write CLI wrappers for programs that make API calls. Most recently I used it to craft an API call similar to what the GitHub CLI (&lt;code&gt;gh&lt;/code&gt;) made to its GraphQL endpoint. It was quicker than reading the docs and figuring out how to craft the query myself.&lt;/p&gt;
&lt;p&gt;Bonus from today: I used the script to troubleshoot how &lt;a href="https://github.com/Aider-AI/aider"&gt;aider&lt;/a&gt; was communicating with AWS Bedrock as it was failing and there were no helpful logs. I noticed &lt;code&gt;aider&lt;/code&gt; was hitting &lt;code&gt;us-west-2&lt;/code&gt; instead of &lt;code&gt;ap-southeast-2&lt;/code&gt;, and I quickly realised my &lt;code&gt;AWS_REGION&lt;/code&gt; environment variable was not set, while &lt;code&gt;AWS_DEFAULT_REGION&lt;/code&gt; was&amp;hellip; which Aider does not read. Whoops!&lt;/p&gt;
&lt;h2 id="getting-to-the-bottom-of-my-cloning-issue"&gt;Getting to the bottom of my cloning issue&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;mitmdump&lt;/code&gt; gave pretty output in the console, but I still couldn&amp;rsquo;t figure it out. It looked like the URL was formed well.&lt;/p&gt;
&lt;a href="https://jfx.ac/assets/blog/debugging-my-stupidity-with-mitmproxy/bad.png" target="_blank"&gt;
&lt;img src="https://jfx.ac/assets/blog/debugging-my-stupidity-with-mitmproxy/bad.png" alt="" style="max-width: min(95vw, 120%)"&gt;
&lt;/a&gt;
&lt;p&gt;I also ran the &lt;code&gt;git clone&lt;/code&gt; that worked earlier via &lt;code&gt;mitmwrap&lt;/code&gt; and captured the traffic, and the request looked identical, except it had a 200 response code.&lt;/p&gt;
&lt;a href="https://jfx.ac/assets/blog/debugging-my-stupidity-with-mitmproxy/good.png" target="_blank"&gt;
&lt;img src="https://jfx.ac/assets/blog/debugging-my-stupidity-with-mitmproxy/good.png" alt="" style="max-width: min(95vw, 120%)"&gt;
&lt;/a&gt;
&lt;p&gt;Generally the console output is pretty helpful, but I knew something &lt;em&gt;had&lt;/em&gt; to be different, so I decided to look at the HAR file. I finally got to the bottom of it after running this command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;diff &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;cat /tmp/dump.har &lt;span class="p"&gt;|&lt;/span&gt; jq -rc &lt;span class="s1"&gt;&amp;#39;.log.entries[0].request | select(.method == &amp;#34;GET&amp;#34;)&amp;#39;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; gron&lt;span class="o"&gt;)&lt;/span&gt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;cat /tmp/dump.har &lt;span class="p"&gt;|&lt;/span&gt; jq -rc &lt;span class="s1"&gt;&amp;#39;.log.entries[1].request | select(.method == &amp;#34;GET&amp;#34;)&amp;#39;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; gron&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This command reads the first HTTP GET requests JSON with &lt;a href="https://jqlang.github.io/jq/"&gt;jq&lt;/a&gt;, and pipes it into &lt;a href="https://github.com/tomnomnom/gron"&gt;gron&lt;/a&gt;, and does the same for the second HTTP GET request. It then diffs the two outputs.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;jq&lt;/code&gt; is a very powerful CLI JSON processor, and &lt;code&gt;gron&lt;/code&gt; flattens JSON to make them easier to &lt;code&gt;grep&lt;/code&gt; or parse. e.g.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;{&amp;#34;hello&amp;#34;: &amp;#34;blog&amp;#34;, &amp;#34;x&amp;#34;: [1, 2]}&amp;#39;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; gron
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;json.hello &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;blog&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;json.x &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;json.x&lt;span class="o"&gt;[&lt;/span&gt;0&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 1&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;json.x&lt;span class="o"&gt;[&lt;/span&gt;1&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 2&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I find &lt;code&gt;gron&lt;/code&gt; nice for feeding JSON into &lt;code&gt;diff&lt;/code&gt; as it flattens it onto new lines.&lt;/p&gt;
&lt;p&gt;What was the cause?? Well here is the output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-diff" data-lang="diff"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;30c30
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;&lt;/span&gt;&lt;span class="gd"&gt;&amp;lt; json.url = &amp;#34;https://git.sr.ht/~whynothugo/lsp-lines.nvim/info/refs?service=git-upload-pack&amp;#34;;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;&lt;/span&gt;&lt;span class="gs"&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gs"&gt;&lt;/span&gt;&lt;span class="gi"&gt;&amp;gt; json.url = &amp;#34;https://git.sr.ht/~whynothugo/lsp_lines.nvim/info/refs?service=git-upload-pack&amp;#34;;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The difference is clear as day on my colourised &lt;code&gt;diff&lt;/code&gt; output. I had been giving my Neovim &lt;code&gt;lsp-lines.nvim&lt;/code&gt;, when the real URL is &lt;code&gt;lsp_lines.nvim&lt;/code&gt; with an underscore.&lt;/p&gt;
&lt;h2 id="how-did-this-happen"&gt;How did this happen&lt;/h2&gt;
&lt;p&gt;I never typed the complete URL out manually in my Neovim config, so how did this happen? Well earlier in &lt;a href="#the-problem"&gt;#the-problem&lt;/a&gt;, I had asked ChatGPT to give me the config for &lt;code&gt;lsp-lines.nvim&lt;/code&gt;. I had not typed the name of the repository properly, so ChatGPT ran with it and gave me the URL in the same form with a hyphen instead of an underscore.&lt;/p&gt;
&lt;p&gt;I also had no coffee for the day.&lt;/p&gt;
&lt;h2 id="reflecting"&gt;Reflecting&lt;/h2&gt;
&lt;p&gt;The URL in the example given by ChatGPT was incorrect, so I was doomed from the start. I reflected afterward and realised there were red flags and clear signs of why this was happening:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The 403 error was likely due to a URL typo&lt;/li&gt;
&lt;li&gt;Cloning the repository locally&lt;/li&gt;
&lt;li&gt;Neovim still trying to pull the plugin with the folder existing&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;At each of these points I should&amp;rsquo;ve figured out what was going on. Using &lt;code&gt;mitmproxy&lt;/code&gt; and all the other tools felt like using a chainsaw to cut butter, but I&amp;rsquo;d already had these things pre-baked, so consuming them was not costly.&lt;/p&gt;
&lt;p&gt;I was very grateful to have my own tooling to help me out here, as the next step was probably to get someone to pair with me and waste their time :) or drop the whole thing :)&lt;/p&gt;
&lt;p&gt;I hope this story encourages you to brush up your scripts and keep them around in your dotfiles. You never know when you may need them again.&lt;/p&gt;
&lt;h2 id="the-plugin"&gt;The plugin&lt;/h2&gt;
&lt;p&gt;At the end of the day I decided not to continue using &lt;code&gt;lsp_lines.nvim&lt;/code&gt;, as in certain repositories it was moving lines around a lot and got annoying. I may use it again in the future with a toggle, but the default diagnostic view from Neovim is fine with me for now. I really wasted my time.&lt;/p&gt;
&lt;a href="https://jfx.ac/assets/blog/debugging-my-stupidity-with-mitmproxy/default.png" target="_blank"&gt;
&lt;img src="https://jfx.ac/assets/blog/debugging-my-stupidity-with-mitmproxy/default.png" alt="" style="max-width: min(95vw, 120%)"&gt;
&lt;/a&gt;
&lt;h2 id="see-also"&gt;See also&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://jvns.ca/blog/2022/06/28/some-notes-on-bubblewrap"&gt;https://jvns.ca/blog/2022/06/28/some-notes-on-bubblewrap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://wiki.archlinux.org/title/Bubblewrap"&gt;https://wiki.archlinux.org/title/Bubblewrap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://mitmproxy.org/"&gt;https://mitmproxy.org/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/itsjfx/dotfiles/blob/master/bin/mitmwrap"&gt;https://github.com/itsjfx/dotfiles/blob/master/bin/mitmwrap&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>WebAuthn in the CLI</title><link>https://jfx.ac/blog/webauthn-in-the-cli/</link><pubDate>Fri, 20 Dec 2024 00:00:00 +0000</pubDate><guid>https://jfx.ac/blog/webauthn-in-the-cli/</guid><description>&lt;h2 id="introduction"&gt;Introduction&lt;/h2&gt;
&lt;p&gt;The first thing that often comes to mind about FIDO2 WebAuthn clients is their integration with modern web browsers. Typically, users are prompted on a website to verify their identity by entering a PIN and interacting with a physical security key. This user-friendly flow is designed to to be fast, secure, and reduce reliance on passwords.&lt;/p&gt;
&lt;p&gt;But what if you need to implement this functionality outside of a browser? What if your application runs in a command-line interface (CLI)? The good news is this is possible, but it&amp;rsquo;s not well documented or commonly implemented. I was motivated to implement this as leaving a terminal and waiting for a page to load to tap a device seemed wasteful.&lt;/p&gt;
&lt;p&gt;This post will guide you through the process of implementing a FIDO2 WebAuthn client in Python. In our example, we wrap &lt;a href="https://webauthn.io"&gt;https://webauthn.io&lt;/a&gt; and cover some of the main concepts along the way. With enough focus this approach could be used to wrap any IDP or service.&lt;/p&gt;
&lt;h2 id="deep-dive-into-webauthn"&gt;Deep dive into WebAuthn&lt;/h2&gt;
&lt;h3 id="summary"&gt;Summary&lt;/h3&gt;
&lt;p&gt;Mozilla has a detailed &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Credential_Management_API/Credential_types#web_authentication_assertions"&gt;explanation here&lt;/a&gt; . I will try summarise below.&lt;/p&gt;
&lt;p&gt;A typical WebAuthn authentication process in a browser looks like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The &amp;ldquo;relying party&amp;rdquo; (the website) sends user and server information to the web app handling the registration process, along with a &amp;ldquo;challenge&amp;rdquo;&lt;/li&gt;
&lt;li&gt;The browser asks the authenticator device (e.g. YubiKey) to sign the challenge via a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/get" title="navigator.credentials.get()"&gt;&lt;code&gt;navigator.credentials.get()&lt;/code&gt;&lt;/a&gt; call. This process is called getting an assertion.&lt;/li&gt;
&lt;li&gt;If the authenticator contains one of the given credentials and is able to successfully sign the challenge, it returns a signed assertion to the web app after receiving user consent&lt;/li&gt;
&lt;li&gt;The web app forwards the signed assertion to the relying party server to validate&lt;/li&gt;
&lt;li&gt;If everything checks out, the relying party will return a successful response to the web app&lt;/li&gt;
&lt;/ol&gt;
&lt;a href="https://jfx.ac/assets/blog/webauthn-in-the-cli/Passwordless_Web_Authentication.svg.png" target="_blank"&gt;
&lt;img src="https://jfx.ac/assets/blog/webauthn-in-the-cli/Passwordless_Web_Authentication.svg.png" alt="" style="max-width: min(95vw, 120%)"&gt;
&lt;/a&gt;
&lt;p&gt;&lt;em&gt;(from &lt;a href="https://en.wikipedia.org/wiki/User:Trscavo"&gt;Trscavo&lt;/a&gt; at English Wikipedia &lt;a href="https://commons.wikimedia.org/wiki/File:Passwordless_Web_Authentication.svg"&gt;available here&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="input"&gt;Input&lt;/h3&gt;
&lt;p&gt;Drilling further into &lt;code&gt;navigator.credentials.get()&lt;/code&gt;, it expects the following fields as parameters/inputs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialRequestOptions#challenge"&gt;&lt;code&gt;challenge&lt;/code&gt;&lt;/a&gt; - a value originating from the relying party&amp;rsquo;s server and used as a &lt;a href="https://en.wikipedia.org/wiki/Challenge%E2%80%93response_authentication"&gt;cryptographic challenge&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialRequestOptions#rpid"&gt;&lt;code&gt;rpId&lt;/code&gt;&lt;/a&gt; (optional) - a string that specifies the relying party&amp;rsquo;s identifier
&lt;ul&gt;
&lt;li&gt;This is used for identifying which keys to use and for preventing cross-origin attacks&lt;/li&gt;
&lt;li&gt;an example value may be &lt;code&gt;login.microsoft.com&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialRequestOptions#allowcredentials"&gt;&lt;code&gt;allowCredentials&lt;/code&gt;&lt;/a&gt; (optional) - A restricted the list of acceptable credentials. An empty array indicates that any credential is acceptable.
&lt;ul&gt;
&lt;li&gt;An empty list is typically used when the relying party is unaware of your username&lt;/li&gt;
&lt;li&gt;For example, &lt;a href="https://notes.jfx.ac/fido2/github/#multiple-accounts-on-a-single-fido2-device"&gt;GitHub sends an empty allowCredentials list&lt;/a&gt; when you press &amp;ldquo;Sign in with a passkey&amp;rdquo;.&lt;/li&gt;
&lt;li&gt;If you have multiple credentials registered under the relying party, your browser will ask you to select a credential to authenticate with&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialRequestOptions#timeout"&gt;&lt;code&gt;timeout&lt;/code&gt;&lt;/a&gt; (optional) - a hint indicating the time the relying party is willing to wait for the retrieval operation to complete&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialRequestOptions#userverification"&gt;&lt;code&gt;userVerification&lt;/code&gt;&lt;/a&gt; (optional) - A enumerated string specifying the relying party&amp;rsquo;s requirements for user verification of the authentication process
&lt;ul&gt;
&lt;li&gt;e.g. whether or not to do PIN verification&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;More detail on each option is available:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialRequestOptions"&gt;Mozilla developer documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;and in the &lt;a href="https://www.w3.org/TR/webauthn-2/#dictionary-assertion-options"&gt;WebAuthn specification&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most fields are optional, however if specified by the relying party they are important and should not be omitted.&lt;/p&gt;
&lt;h3 id="output"&gt;Output&lt;/h3&gt;
&lt;p&gt;The function then returns &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential"&gt;an object with type PublicKeyCredential&lt;/a&gt;. Typically an identity provider (IDP) will expect this entire object to be sent back, or a subset of the fields under the &lt;code&gt;response&lt;/code&gt; key which &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse"&gt;has type AuthenticatorAssertionResponse&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The important fields are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData"&gt;&lt;code&gt;authenticatorData&lt;/code&gt;&lt;/a&gt; - contains information about which relying party and credential was used
&lt;ul&gt;
&lt;li&gt;your device may set an interesting field, &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API/Authenticator_data#signcount"&gt;&lt;code&gt;signCount&lt;/code&gt;&lt;/a&gt;, a &lt;a href="https://www.imperialviolet.org/2023/08/05/signature-counters.html"&gt;signature counter&lt;/a&gt; used to prevent device cloning&lt;/li&gt;
&lt;li&gt;if &lt;code&gt;signCount&lt;/code&gt; is set, then repeated assertions will not return an identical &lt;code&gt;authenticatorData&lt;/code&gt; value, thus each &lt;code&gt;signature&lt;/code&gt; will be a unique value&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorResponse/clientDataJSON"&gt;&lt;code&gt;clientDataJSON&lt;/code&gt;&lt;/a&gt; - contains the &lt;code&gt;challenge&lt;/code&gt; and the origin used to prevent cross-origin attacks&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/signature"&gt;&lt;code&gt;signature&lt;/code&gt;&lt;/a&gt; - the combined signature of &lt;code&gt;authenticatorData&lt;/code&gt; and &lt;code&gt;clientDataJSON&lt;/code&gt;, signed with the private key of the selected credential associated with the relying party&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once a server receives these values, it can verify the signature was created by the correct credential, and can validate the contents of &lt;code&gt;authenticatorData&lt;/code&gt; and &lt;code&gt;clientDataJSON&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="inspecting-webauthnio"&gt;Inspecting webauthn.io&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://webauthn.io"&gt;https://webauthn.io&lt;/a&gt; is a public service for testing passkeys. This makes it the perfect test bed for understanding the flow for a WebAuthn client and testing our CLI client. We may create a bunch of malformed requests, so it&amp;rsquo;s better to do this to a test service and not a real IDP as we may get locked out of our real account.&lt;/p&gt;
&lt;p&gt;I won&amp;rsquo;t be covering registering passkeys in a CLI cause this is only completed once per security key. For the curious, this done in the browser via &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/create"&gt;navigator.credentials.create()&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Assuming we have already registered our passkey to a username, we can open the network explorer and observe a dual request log in flow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A POST call to &lt;code&gt;https://webauthn.io/authentication/options&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;With a JSON request body: &lt;code&gt;{&amp;quot;username&amp;quot;:&amp;quot;jfxac&amp;quot;,&amp;quot;user_verification&amp;quot;:&amp;quot;preferred&amp;quot;}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;And a JSON response body: &lt;code&gt;{&amp;quot;challenge&amp;quot;: &amp;quot;BASE64URL_CHALLENGE&amp;quot;, &amp;quot;timeout&amp;quot;: 60000, &amp;quot;rpId&amp;quot;: &amp;quot;webauthn.io&amp;quot;, &amp;quot;allowCredentials&amp;quot;: [{&amp;quot;id&amp;quot;: &amp;quot;BASE64URL_ID&amp;quot;, &amp;quot;type&amp;quot;: &amp;quot;public-key&amp;quot;, &amp;quot;transports&amp;quot;: [&amp;quot;usb&amp;quot;]}], &amp;quot;userVerification&amp;quot;: &amp;quot;preferred&amp;quot;}&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;Luckily these are the exact key value pairs &lt;a href="#input"&gt;used by &lt;code&gt;navigator.credentials.get()&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;A POST call to &lt;code&gt;https://webauthn.io/authentication/verification&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;With a JSON request body: containing the &lt;a href="#output"&gt;full response of &lt;code&gt;navigator.credentials.get()&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;And a JSON response body: &lt;code&gt;{&amp;quot;verified&amp;quot;: true}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;With all the information required, we can write some code to call the APIs and feed the values into a WebAuthn client.&lt;/p&gt;
&lt;h2 id="writing-some-code"&gt;Writing some code&lt;/h2&gt;
&lt;p&gt;Yubico have a created a &lt;a href="https://github.com/Yubico/python-fido2"&gt;Python package called fido2&lt;/a&gt; which has a &lt;a href="https://developers.yubico.com/python-fido2/API_Documentation/autoapi/fido2/client/index.html"&gt;WebAuthn client&lt;/a&gt;. They also provided &lt;a href="https://github.com/Yubico/python-fido2/blob/main/examples/credential.py"&gt;an example&lt;/a&gt; which acts as both a server &amp;amp; client and allows for registering a credential and getting an assertion. We will use this code as a base for our webauthn.io wrapper.&lt;/p&gt;
&lt;p&gt;The psuedocode is roughly:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Initialise a FIDO2 WebAuthn client&lt;/li&gt;
&lt;li&gt;Call &lt;a href="https://webauthn.io/authentication/options"&gt;https://webauthn.io/authentication/options&lt;/a&gt; with username provided by command-line arguments&lt;/li&gt;
&lt;li&gt;Pass the assertion options to the client, and wait for the user to complete the perform the actions&lt;/li&gt;
&lt;li&gt;Once complete, call &lt;a href="https://webauthn.io/authentication/verification"&gt;https://webauthn.io/authentication/verification&lt;/a&gt; with the values returned by the WebAuthn client&lt;/li&gt;
&lt;li&gt;If successful, write something to the terminal and exit successfully - - otherwise write any errors to the terminal and exit unsuccessfully&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A POST request to webauthn.io to kick off a log in:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;requests&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;COOKIES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;csrftoken&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;generate_random_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;sessionid&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;generate_random_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_options&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;username&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;user_verification&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;required&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;https://webauthn.io/authentication/options&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cookies&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;COOKIES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_options&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This will give our &lt;code&gt;challenge&lt;/code&gt;, &lt;code&gt;rpId&lt;/code&gt;, &lt;code&gt;allowCredentials&lt;/code&gt;, and &lt;code&gt;userVerification&lt;/code&gt; values.&lt;/p&gt;
&lt;br&gt;
We can then call our WebAuthn client, passing in the above key value pairs:
&lt;p&gt;I&amp;rsquo;ve removed the client initialisation code for this snippet but it&amp;rsquo;s available here: &lt;a href="https://github.com/Yubico/python-fido2/blob/main/examples/exampleutils.py#L70"&gt;https://github.com/Yubico/python-fido2/blob/main/examples/exampleutils.py#L70&lt;/a&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;fido2.utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;websafe_decode&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_options&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;request_options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;rpId&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;rpId&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;challenge&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;websafe_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;challenge&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;timeout&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;allowCredentials&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PublicKeyCredentialDescriptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cred&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;type&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;websafe_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cred&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;id&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="n"&gt;transports&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cred&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;transports&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;cred&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;allowCredentials&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;userVerification&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;userVerification&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_assertion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request_options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;result&lt;/code&gt; will contain our signed assertion, which can be sent to webauthn.io&lt;/p&gt;
&lt;p&gt;Note the &lt;code&gt;websafe_decode&lt;/code&gt; method used to give the FIDO2 library the raw challenge. A typical mistake is to give the WebAuthn client the challenge incorrectly encoded. In that case, the client will still sign an assertion, but the IDP will return a failure as the challenge is incorrect.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;With the assertion from our device, we can make a POST request to webauthn.io with the fields populated:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;fido2.utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;websafe_decode&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;requests&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;respond&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;username&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;response&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;authenticatorAttachment&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;cross-platform&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;clientExtensionResults&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;id&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;websafe_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;credentialId&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;rawId&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;websafe_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;credentialId&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;type&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;public-key&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;response&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;clientDataJSON&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;websafe_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;clientDataJSON&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;authenticatorData&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;websafe_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;authenticatorData&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;signature&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;websafe_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;signature&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;userHandle&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;websafe_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;userHandle&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;https://webauthn.io/authentication/verification&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cookies&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;COOKIES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Note using &lt;code&gt;websafe_encode&lt;/code&gt; to encode many of the fields. The data needs to be encoded as it&amp;rsquo;s in raw bytes. Typically Base64 URL encoded strings are used, but some IDPs may encode values differently. Make sure to identify the correct encoding.&lt;/p&gt;
&lt;h2 id="piecing-it-all-together"&gt;Piecing it all together&lt;/h2&gt;
&lt;p&gt;With all the pieces found we can write a complete CLI tool. Here is a demo:&lt;/p&gt;
&lt;a href="https://jfx.ac/assets/blog/webauthn-in-the-cli/cli.gif" target="_blank"&gt;
&lt;img src="https://jfx.ac/assets/blog/webauthn-in-the-cli/cli.gif" alt="" style="max-width: min(95vw, 120%)"&gt;
&lt;/a&gt;
&lt;p&gt;And here is the complete code that &lt;a href="https://gist.github.com/itsjfx/b4612d90c210d0a1c77de606a87311ad"&gt;I published as a gist&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Most of the code is initialising the FIDO2 WebAuthn client based on the &lt;a href="https://github.com/Yubico/python-fido2/blob/main/examples/credential.py"&gt;example code mentioned earlier&lt;/a&gt;. The complexity lies in signing the assertion correctly and creating web requests exactly the way webauthn.io expects. This includes cookies and headers. It may take a bit of trial and error to get everything perfect.&lt;/p&gt;
&lt;script src="https://gist.github.com/itsjfx/b4612d90c210d0a1c77de606a87311ad.js"&gt;&lt;/script&gt;
&lt;h2 id="handling-wsl"&gt;Handling WSL&lt;/h2&gt;
&lt;p&gt;The following works great on Linux, Mac, and on native Windows, but does not work on WSL. Windows does not pass-through USB devices to WSL natively.&lt;/p&gt;
&lt;p&gt;There are a few solutions however:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Install Python + &lt;code&gt;fido2&lt;/code&gt; package on Windows, call &lt;code&gt;python.exe&lt;/code&gt; and capture the assertion (e.g. via &lt;code&gt;stdout&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Follow &lt;a href="https://learn.microsoft.com/en-us/windows/wsl/connect-usb"&gt;this guide by Microsoft&lt;/a&gt; to expose the USB device to WSL
&lt;ul&gt;
&lt;li&gt;With enough elbow grease you could automate this process&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I enjoy working with hardware keys and have a keen interest in how FIDO2 works, so this was a really fun post to write. If you don&amp;rsquo;t write a CLI application with WebAuthn, I hope you still learnt something about how it works.&lt;/p&gt;
&lt;h2 id="see-also"&gt;See also&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.imperialviolet.org/tourofwebauthn/tourofwebauthn.html"&gt;Adam Langley&amp;rsquo;s book &amp;ldquo;A Tour of WebAuthn&amp;rdquo;&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;I wish existed when I first learnt about WebAuthn&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/TR/webauthn-2"&gt;The WebAuthn spec&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/WebAuthn"&gt;https://en.wikipedia.org/wiki/WebAuthn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API"&gt;https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>VS Code to Neovim</title><link>https://jfx.ac/blog/vscode-to-neovim/</link><pubDate>Sun, 15 Sep 2024 00:00:00 +0000</pubDate><guid>https://jfx.ac/blog/vscode-to-neovim/</guid><description>&lt;p&gt;I decided to stop using VS Code completely and see how long I could use Neovim. It&amp;rsquo;s been 5 weeks and so far it&amp;rsquo;s been great. I wish I&amp;rsquo;d done it earlier.&lt;/p&gt;
&lt;h2 id="what-is-vim-and-neovim"&gt;What is Vim and Neovim&lt;/h2&gt;
&lt;h3 id="vim"&gt;Vim&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Vim_(text_editor)"&gt;Vim&lt;/a&gt; is a free and open-source text editor that evolved from the original &lt;a href="https://en.wikipedia.org/wiki/Vi_(text_editor)"&gt;vi&lt;/a&gt; editor which dates back to 1976. It allows users to perform complex editing tasks through keyboard shortcuts without relying on a mouse. Vim has a &lt;a href="https://en.wikipedia.org/wiki/Text_user_interface"&gt;text user interface (TUI)&lt;/a&gt; instead of a &lt;a href="https://en.wikipedia.org/wiki/Graphical_user_interface"&gt;Graphical user interface (GUI)&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Vim is a mode-based editor, with the popular modes being &amp;ldquo;Normal&amp;rdquo;, &amp;ldquo;Insert&amp;rdquo;, and &amp;ldquo;Command&amp;rdquo; mode. Vim users will navigate and issue editor commands in Normal mode, type text in Insert mode, and issue commands (e.g. saving, quitting, or find and replace) in Command mode.&lt;/p&gt;
&lt;p&gt;Vim&amp;rsquo;s modes simplify tasks as they separate typing from actions and eliminate complex multi-key operations. Complex operations are issued via keystrokes with a clear purpose, resulting in a smoother process with minimal keystrokes and minimal movement from the keyboard&amp;rsquo;s home row. The caveat is switching modes and learning the keystrokes is complex and takes a while before it feels right.&lt;/p&gt;
&lt;p&gt;Despite the complexity, in a StackOverflow 2023 survey, Vim was voted the &lt;a href="https://survey.stackoverflow.co/2023/#most-popular-technologies-new-collab-tools"&gt;5th most popular text editor&lt;/a&gt;. 22.29% respondents said they regularly used Vim and wanted to continue to using it.&lt;/p&gt;
&lt;h3 id="neovim"&gt;Neovim&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Vim_(text_editor)#Neovim"&gt;Neovim&lt;/a&gt; is a fork of Vim that strives to improve the extensibility and maintainability of Vim. Neovim supports Lua scripting in addition to VimScript, making it easier for the community to extend the editor. You can &lt;a href="https://neovim.io/charter"&gt;learn more here&lt;/a&gt; about the vision of Neovim.&lt;/p&gt;
&lt;p&gt;Compared to Vim, it has new powerful features such as: a well documented API, job-control, LSP support, built-in parser support, &lt;a href="https://neovim.io/doc/user/vim_diff.html"&gt;and more&lt;/a&gt;. Overall it feels more community-focused than Vim.&lt;/p&gt;
&lt;p&gt;In the StackOverflow survey mentioned earlier, it was voted the &lt;a href="https://survey.stackoverflow.co/2023/#most-popular-technologies-new-collab-tools"&gt;10th most popular text editor&lt;/a&gt;, however it was &lt;a href="https://survey.stackoverflow.co/2023/#integrated-development-environment"&gt;the most admired editor&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="learning-vim"&gt;Learning Vim&lt;/h2&gt;
&lt;p&gt;During my annual leave January last year (20 months ago) I decided to learn Vim. I got curious after watching and talking to Vim users at work. Before Vim I used &lt;code&gt;nano&lt;/code&gt; in the terminal for many years and VS Code as my GUI text editor and IDE of choice.&lt;/p&gt;
&lt;p&gt;I chose to try Neovim on my workstation and Vim on remote servers.&lt;/p&gt;
&lt;p&gt;I learnt the basics from the Tutor built into Vim via &lt;code&gt;:Tutor&lt;/code&gt;. I also read sections of the &lt;a href="https://ofirgall.github.io/learn-nvim"&gt;Learn Neovim book&lt;/a&gt; by &lt;a href="https://github.com/ofirgall"&gt;Ofir Gal&lt;/a&gt; which was helpful for understanding key concepts.&lt;/p&gt;
&lt;p&gt;I found Vim and Neovim perfect for working on remote servers, changing configuration files, and making basic changes. However, I found it difficult to use for app development as it was too limiting.&lt;/p&gt;
&lt;p&gt;I tried building out a basic Neovim configuration and installed plugins to make it more feature rich, but combined with learning Vim it was too much for me to handle. Instead I continued using VS Code for app development.&lt;/p&gt;
&lt;p&gt;The Vim concepts and keybindings still intrigued me, so I installed a &lt;a href="https://marketplace.visualstudio.com/items?itemName=vscodevim.vim"&gt;Vim extension for VSCode&lt;/a&gt;. The extension provides the productivity boost and behaviours of Vim alongside the powerful feature-set and familiarity of VS Code. I recommend it to anyone interested.&lt;/p&gt;
&lt;p&gt;I also enabled &lt;a href="https://publish.obsidian.md/hub/04+-+Guides%2C+Workflows%2C+%26+Courses/for+Vim+users"&gt;Vim keybindings in Obsidian&lt;/a&gt;. I was definitely working faster after using Vim bindings in my editors for a few months as I was relying on the keyboard and less on the mouse. Cause I&amp;rsquo;d adjusted to the bindings from using the GUI programs, I was able to edit files a lot faster in the terminal. I no longer found editing files remotely over SSH a hindrance and was willing to do more changes over shell sessions. This helped me out a lot at work.&lt;/p&gt;
&lt;h2 id="switching-over"&gt;Switching over&lt;/h2&gt;
&lt;p&gt;The itch to stop emulating Vim and use the real thing was too big so I decided to try give switching another chance. I wanted more control over my text editor as I was doing JavaScript &amp;amp; CSS injection in VS Code&amp;rsquo;s Electron runtime to customise it which would break on most updates. I also felt like a heavy opinionated Electron editor was no longer for me and a terminal based workflow would be nice.&lt;/p&gt;
&lt;p&gt;Before switching I thought about all the VS Code features I wanted in my new environment:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Buffer manager
&lt;ul&gt;
&lt;li&gt;Go to bindings&lt;/li&gt;
&lt;li&gt;Re-ordering bindings&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Indentation detection&lt;/li&gt;
&lt;li&gt;Indentation guides&lt;/li&gt;
&lt;li&gt;Git integration&lt;/li&gt;
&lt;li&gt;File finder (Ctrl + P)&lt;/li&gt;
&lt;li&gt;Text finder (Ctrl + Shift +F)&lt;/li&gt;
&lt;li&gt;Syntax highlighting
&lt;ul&gt;
&lt;li&gt;Treesitter&lt;/li&gt;
&lt;li&gt;LSP&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Language_Server_Protocol"&gt;Language Server Protocol&lt;/a&gt; (LSP)
&lt;ul&gt;
&lt;li&gt;Manager&lt;/li&gt;
&lt;li&gt;Go to definition&lt;/li&gt;
&lt;li&gt;Show definition&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Completion
&lt;ul&gt;
&lt;li&gt;LSP&lt;/li&gt;
&lt;li&gt;Snippets&lt;/li&gt;
&lt;li&gt;Command line&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Styling
&lt;ul&gt;
&lt;li&gt;A nice theme&lt;/li&gt;
&lt;li&gt;Highlighting on TODOs&lt;/li&gt;
&lt;li&gt;Colouriser for hex codes&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Status line&lt;/li&gt;
&lt;li&gt;Directory manager&lt;/li&gt;
&lt;li&gt;Session management&lt;/li&gt;
&lt;li&gt;Rainbow parentheses&lt;/li&gt;
&lt;li&gt;Indentation guides&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I suggest doing the same exercise.&lt;/p&gt;
&lt;p&gt;As mentioned earlier, I previously attempted setting up a Neovim environment and it was incredibly time-consuming. Most of the features I wanted are not available out of the box and require third-party plugins. After asking people for plugin suggestions the list quickly piled up. I just wanted something that would work that I could change later when I felt like customising/ricing it to suit me.&lt;/p&gt;
&lt;h2 id="trying-out-presets"&gt;Trying out presets&lt;/h2&gt;
&lt;h3 id="struggling-with-distributions"&gt;Struggling with distributions&lt;/h3&gt;
&lt;p&gt;Luckily people online have tried solving my problems by having Neovim configurations available.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s generally two ways of getting started:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;using a Neovim distribution
&lt;ul&gt;
&lt;li&gt;fully-fledged, opinionated, and feature-rich IDE-like Neovim environments&lt;/li&gt;
&lt;li&gt;probably has more things than you need&lt;/li&gt;
&lt;li&gt;feels like its own editor, not like Neovim&lt;/li&gt;
&lt;li&gt;e.g. &lt;a href="https://www.lunarvim.org/"&gt;LunarVim&lt;/a&gt;, &lt;a href="https://astronvim.com/"&gt;AstroNvim&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;using and modifying a basic configuration
&lt;ul&gt;
&lt;li&gt;less feature-rich&lt;/li&gt;
&lt;li&gt;likely missing a couple of things you want&lt;/li&gt;
&lt;li&gt;generally people&amp;rsquo;s dot-files, Neovim &amp;ldquo;templates&amp;rdquo;, or &amp;ldquo;basic configurations&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I tried two popular Neovim distributions (based on GitHub stars) and both of them gave me a giant wall of errors each time I opened a buffer. I&amp;rsquo;m not going to name and shame them as it&amp;rsquo;s likely my fault. I was frustrated after waiting for an absurd number of plugins to download for it to not to function.&lt;/p&gt;
&lt;p&gt;I immediately kicked into troubleshoot mode and started fixing problems. Eventually I sat back and came to the realisation that it shouldn&amp;rsquo;t be this hard.&lt;/p&gt;
&lt;h3 id="kickstart"&gt;kickstart&lt;/h3&gt;
&lt;p&gt;Before I gave up again, a colleague suggested I try &lt;a href="https://github.com/nvim-lua/kickstart.nvim"&gt;kickstart.nvim&lt;/a&gt;. It&amp;rsquo;s absolutely amazing.&lt;/p&gt;
&lt;p&gt;kickstart is a highly documented less opinionated starting point / basic configuration for Neovim. It has &lt;a href="https://neovim.io/doc/user/lsp.html"&gt;LSP&lt;/a&gt;, &lt;a href="https://tree-sitter.github.io/tree-sitter/"&gt;treesitter&lt;/a&gt;, &lt;a href="https://github.com/nvim-telescope/telescope.nvim"&gt;telescope&lt;/a&gt; (fuzzy finder), some sane default options, and that&amp;rsquo;s it.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;d like additional LSP servers or &lt;code&gt;treesitter&lt;/code&gt; parsers, it&amp;rsquo;s written in the config how to add them. The result is a reasonably light Neovim environment, most of the config is just documentation in the comments.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s how kickstart looks:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Kickstart out of the box&lt;/strong&gt;
&lt;a href="https://jfx.ac/assets/blog/vscode-to-neovim/kickstart-vanilla.png" target="_blank"&gt;
&lt;img src="https://jfx.ac/assets/blog/vscode-to-neovim/kickstart-vanilla.png" alt="" style="max-width: min(95vw, 120%)"&gt;
&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Telescopes file finder&lt;/strong&gt;
&lt;a href="https://jfx.ac/assets/blog/vscode-to-neovim/telescope-find.png" target="_blank"&gt;
&lt;img src="https://jfx.ac/assets/blog/vscode-to-neovim/telescope-find.png" alt="" style="max-width: min(95vw, 120%)"&gt;
&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Telescopes live grep&lt;/strong&gt;
&lt;a href="https://jfx.ac/assets/blog/vscode-to-neovim/telescope-grep.png" target="_blank"&gt;
&lt;img src="https://jfx.ac/assets/blog/vscode-to-neovim/telescope-grep.png" alt="" style="max-width: min(95vw, 120%)"&gt;
&lt;/a&gt;
&lt;/p&gt;
&lt;br&gt;
Kickstart clocks at only 26 plugins, most of which enhance language features or add Telescope functionality, so it's fairly lightweight but lacking a couple things from the list mentioned earlier.
&lt;h2 id="my-setup"&gt;My setup&lt;/h2&gt;
&lt;h3 id="configuration"&gt;Configuration&lt;/h3&gt;
&lt;p&gt;Which brings us to me. I run a modified version of kickstart. Typically I break my dot-files up into multiple files (see my &lt;a href="https://github.com/itsjfx/dotfiles/tree/master/.config/zsh.d"&gt;zsh&lt;/a&gt; and &lt;a href="https://github.com/itsjfx/dotfiles/tree/master/.config/tmux/conf.d"&gt;tmux&lt;/a&gt;), but I&amp;rsquo;ve not done that yet and just extended the single file kickstart.&lt;/p&gt;
&lt;p&gt;To help extend my config I used a bunch of plugins from &lt;a href="https://github.com/rockerBOO/awesome-neovim"&gt;rockerBOO&amp;rsquo;s awesome-neovim list&lt;/a&gt;, &lt;a href="https://github.com/tpope"&gt;Tim Pope (tpope)&lt;/a&gt;, and &lt;a href="https://reddit.com/r/neovim"&gt;/r/neovim&lt;/a&gt;. So far I&amp;rsquo;ve not run into issues with plugins fighting each other or being incompatible.&lt;/p&gt;
&lt;p&gt;In addition to kickstart&amp;rsquo;s configuration, I installed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;buffer manager
&lt;ul&gt;
&lt;li&gt;using &lt;a href="https://github.com/romgrk/barbar.nvim"&gt;barbar.nvim&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;directory manager
&lt;ul&gt;
&lt;li&gt;using the wonderful &lt;a href="https://github.com/stevearc/oil.nvim"&gt;oil.nvim&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;and &lt;a href="https://github.com/nvim-neo-tree/neo-tree.nvim"&gt;neo-tree.nvim&lt;/a&gt; for my sidebar&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;colorizer
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/norcalli/nvim-colorizer.lua"&gt;nvim-colorizer&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;session manager
&lt;ul&gt;
&lt;li&gt;using &lt;a href="https://github.com/tpope/vim-obsession"&gt;vim-obsession&lt;/a&gt; to get a similar experience to VS Code workspaces&lt;/li&gt;
&lt;li&gt;(more below)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;rainbow parentheses
&lt;ul&gt;
&lt;li&gt;using &lt;a href="https://github.com/lincheney/nvim-ts-rainbow"&gt;nvim-ts-rainbow&lt;/a&gt; forked by the wonderful &lt;a href="https://github.com/lincheney"&gt;lincheney&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;indentation guides
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/lukas-reineke/indent-blankline.nvim"&gt;indent-blankline.nvim&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/junegunn/fzf.vim"&gt;fzf.vim&lt;/a&gt; instead of telescope
&lt;ul&gt;
&lt;li&gt;(more below)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;movement plugins
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/ggandor/leap.nvim"&gt;leap.nvim&lt;/a&gt; crazy plugin to navigate to the exact word you want&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/unblevable/quick-scope"&gt;quick-scope&lt;/a&gt; enhances the &lt;code&gt;f&lt;/code&gt; mode to highlight the first character of each word&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mini.move&lt;/code&gt; to quickly move things around instead of using &lt;code&gt;d&lt;/code&gt; &amp;amp; &lt;code&gt;p&lt;/code&gt; (highly recommend this)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;more git plugins
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/tpope/vim-fugitive"&gt;vim-fugitive&lt;/a&gt; for some extra Git bits&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rbong/vim-flog"&gt;vim-flog&lt;/a&gt; for browsing Git logs&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ruanyl/vim-gh-line"&gt;vim-gh-line&lt;/a&gt; for opening stuff in the browser&lt;/li&gt;
&lt;li&gt;kickstart has &lt;a href="https://github.com/lewis6991/gitsigns.nvim"&gt;gitsigns.nvim&lt;/a&gt; out of the box which is a great plugin for seeing changed lines/hunks&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;a bunch of small quality of life plugins
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/bullets-vim/bullets.vim"&gt;bullets.vim&lt;/a&gt; for editing markdown lists&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mini.cursorword&lt;/code&gt; to show other matches on highlighted words&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rhysd/committia.vim"&gt;committia.vim&lt;/a&gt; nicer git commit editor&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/junegunn/vim-easy-align"&gt;vim-easy-align&lt;/a&gt; for aligning things, e.g. by whitespace or colon&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/nvim-treesitter/nvim-treesitter-context"&gt;nvim-treesitter-context&lt;/a&gt; for getting function scoping when scrolling&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com.mechatroner/rainbow_csv"&gt;rainbow_csv&lt;/a&gt; for clarity when editing tsv-like files&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/tpope/vim-repeat"&gt;vim-repeat&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&amp;rsquo;m not likely to keep this post updated. Source of truth is &lt;a href="https://github.com/itsjfx/dotfiles/blob/master/.config/nvim/init.lua"&gt;available in my dotfiles&lt;/a&gt;, but I hope the above list and three links (especially &lt;a href="https://github.com/rockerBOO/awesome-neovim"&gt;awesome-neovim&lt;/a&gt;) will help people get started.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve got 52 plugins installed in total (including some not mentioned above), so I&amp;rsquo;ve doubled the amount from kickstart.&lt;/p&gt;
&lt;p&gt;Kickstart comes out of the box with &lt;a href="https://github.com/nvim-telescope/telescope.nvim"&gt;telescope.nvim&lt;/a&gt; which is a fuzzy finder for Neovim. I try use &lt;a href="https://github.com/junegunn/fzf"&gt;fzf&lt;/a&gt; (another fuzzy finder) everywhere I can, so while telescope was installed and working well I use fzf for Ripgrep and file finding. The telescope setup in kickstart works really well though so I suggest people to try it out first.&lt;/p&gt;
&lt;h3 id="screengrabs"&gt;Screengrabs&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;My Neovim with the file manager shown&lt;/strong&gt;
&lt;a href="https://jfx.ac/assets/blog/vscode-to-neovim/my-neovim-file-manager.png" target="_blank"&gt;
&lt;img src="https://jfx.ac/assets/blog/vscode-to-neovim/my-neovim-file-manager.png" alt="" style="max-width: min(95vw, 120%)"&gt;
&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A demo of me using Neovim. leap + fzf + LSP (recorded with &lt;a href="https://github.com/asciinema/asciinema"&gt;asciinema&lt;/a&gt;, click to enlarge)&lt;/strong&gt;
&lt;a href="https://jfx.ac/assets/blog/vscode-to-neovim/demo.gif" target="_blank"&gt;
&lt;img src="https://jfx.ac/assets/blog/vscode-to-neovim/demo.gif" alt="" style="max-width: min(95vw, 120%)"&gt;
&lt;/a&gt;
&lt;/p&gt;
&lt;h3 id="working-remotely"&gt;Working remotely&lt;/h3&gt;
&lt;p&gt;The nice thing about Neovim running in a terminal is I can resume my sessions remotely as all my shell sessions run within a &lt;code&gt;tmux&lt;/code&gt; session. This is a reference to &lt;a href="../my-workflow-philosophy#6-resumable"&gt;my-workflow-philosophy#resumable&lt;/a&gt;. Check out my other post if you have not :)&lt;/p&gt;
&lt;p&gt;Sometimes I work on my desktop, and other times on my laptop on the couch. When I move to the couch I SSH to my desktop, press &lt;code&gt;CTRL + b&lt;/code&gt; (my prefix key for tmux), then press &lt;code&gt;s&lt;/code&gt; to interactively see my tmux sessions, select my Neovim session and continue working. When I go back to my desktop everything is the same as when I finished on my laptop. Super seamless.&lt;/p&gt;
&lt;p&gt;To match the &lt;a href="https://code.visualstudio.com/docs/remote/ssh"&gt;Remote SSH VS Code extension&lt;/a&gt; I either run Vim directly on a server, or mount the remote file system with &lt;a href="https://github.com/libfuse/sshfs"&gt;SSHFS&lt;/a&gt; or &lt;a href="https://rclone.org/sftp"&gt;SFTP over rclone&lt;/a&gt; and edit locally via Neovim.&lt;/p&gt;
&lt;h3 id="vs-code-key-bindings"&gt;VS Code key bindings&lt;/h3&gt;
&lt;p&gt;I was used to VS code&amp;rsquo;s keybindings for commenting (&lt;code&gt;CTRL + /&lt;/code&gt;) and searching for files (&lt;code&gt;CTRL + p&lt;/code&gt;), so I bound those in Neovim too. Eventually I want to remove these hot-keys and rely more on Vim modes instead, however keeping some bindings has greatly aided transitioning.&lt;/p&gt;
&lt;h3 id="session-management"&gt;Session Management&lt;/h3&gt;
&lt;p&gt;When using VS Code I create &lt;a href="https://code.visualstudio.com/docs/editor/workspaces"&gt;VS Code workspaces&lt;/a&gt; for each project and save them in &lt;code&gt;~/.code-workspaces/&lt;/code&gt;. I had a Rofi selector which let me select and open my workspaces. It looked like this:&lt;/p&gt;
&lt;a href="https://jfx.ac/assets/blog/vscode-to-neovim/vscode-rofi.png" target="_blank"&gt;
&lt;img src="https://jfx.ac/assets/blog/vscode-to-neovim/vscode-rofi.png" alt="" style="max-width: min(95vw, 120%)"&gt;
&lt;/a&gt;
&lt;p&gt;For Neovim I use &lt;a href="https;//github.com/tpope/vim-obsession"&gt;vim-obsession&lt;/a&gt; by Tim Pope which extends the in-built Vim &lt;code&gt;mksession&lt;/code&gt; command to save and resume sessions more aggressively, effectively behaving like VS Code workspaces. Starting a session is done with &lt;code&gt;:Obsession ~/.nvim-sessions/x&lt;/code&gt; and resuming sessions is by &lt;code&gt;nvim -S ~/.nvim-sessions/x&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To make resuming sessions easier I use a shell command &lt;code&gt;vw&lt;/code&gt; with custom tab completion. It looks like this:
&lt;a href="https://jfx.ac/assets/blog/vscode-to-neovim/vw.png" target="_blank"&gt;
&lt;img src="https://jfx.ac/assets/blog/vscode-to-neovim/vw.png" alt="" style="max-width: min(95vw, 120%)"&gt;
&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;The source code is located here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/itsjfx/dotfiles/blob/master/bin/vw"&gt;https://github.com/itsjfx/dotfiles/blob/master/bin/vw&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/itsjfx/dotfiles/blob/master/.completions/_vw"&gt;https://github.com/itsjfx/dotfiles/blob/master/.completions/_vw&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="things-to-improve"&gt;Things to improve&lt;/h2&gt;
&lt;p&gt;Right now I&amp;rsquo;m very happy overall but there&amp;rsquo;s a few things I want to improve on.&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t have a debugger nor dev containers working in Neovim at the moment. A project I work on utilises these two things heavily so I may need to continue using VS Code until I figure out how to get it working nicely in Neovim.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s also a ton of plugins I&amp;rsquo;ve installed but have not understood much about. For example, Gitsigns, Fugitive, and oil have a bunch of powerful features and bindings I&amp;rsquo;m not utilising, so I hope I figure these out eventually.&lt;/p&gt;
&lt;p&gt;I also miss having a multi line buffer manager in VS Code, and while barbar is nice it continues overflowing horizontally if I open too many buffers, so I hope to find a buffer manager that works like VS Code did.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s also a ton of Vim stuff I still need to learn :)&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I hope this helps someone else switch to Neovim and gives some ideas on how to do the move. I highly recommend learning how to Vim in the shell, then trying out Vim bindings in your current editor (if possible), and then trying out Neovim as your main editor if you feel like moving.&lt;/p&gt;
&lt;p&gt;Learning Vim in the shell as a minimum will be extremely fulfilling and help you doing basic operations in the terminal.&lt;/p&gt;
&lt;p&gt;Remember nobody is forcing you to use Vim everyday and you can always go back to VS Code or your original editor :)&lt;/p&gt;
&lt;h2 id="see-also"&gt;See also&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;:Tutor&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ofirgall/learn-nvim/blob/master/media/EverythingEverywhereAllAtOnce.md"&gt;https://github.com/ofirgall/learn-nvim/blob/master/media/EverythingEverywhereAllAtOnce.md&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;this helped inspired my Neovim move&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ofirgall.github.io/learn-nvim"&gt;https://ofirgall.github.io/learn-nvim&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;helpful book&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/nvim-lua/kickstart.nvim"&gt;https://github.com/nvim-lua/kickstart.nvim&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://didoesdigital.com/blog/neovim-to-vs-code"&gt;https://didoesdigital.com/blog/neovim-to-vs-code&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;the opposite of what I did&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rockerBOO/awesome-neovim"&gt;awesome-neovim&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;the list of plugins from &lt;a href="#configuration"&gt;#configuration&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Homelab &amp; Mikrotik inspection network</title><link>https://jfx.ac/blog/homelab-mikrotik-inspect-network/</link><pubDate>Wed, 31 Jul 2024 00:00:00 +0000</pubDate><guid>https://jfx.ac/blog/homelab-mikrotik-inspect-network/</guid><description>&lt;h2 id="introduction"&gt;Introduction&lt;/h2&gt;
&lt;p&gt;I wanted to reverse engineer the protocols of IoT devices, but it had been a while since I&amp;rsquo;d last traffic inspected a device and I needed to set up something fresh.&lt;/p&gt;
&lt;p&gt;In the past I would&amp;rsquo;ve run Linux on a laptop, spawn an AP, run &lt;code&gt;tcpdump&lt;/code&gt; and call it a day. But I put &lt;em&gt;so much effort&lt;/em&gt; into building a home network with my Mikrotik router and Ubiquiti APs, and have the ability to use VLANs, so I wanted to do something better.&lt;/p&gt;
&lt;p&gt;My goals:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;no reliance on a desktop/laptop for the capturing to function. my home lab server and wireless APs are always on so I want to use these&lt;/li&gt;
&lt;li&gt;TLS termination as I&amp;rsquo;d need to capture HTTPS &amp;amp; MQTTS traffic&lt;/li&gt;
&lt;li&gt;strong WiFi coverage and speed
&lt;ul&gt;
&lt;li&gt;Coverage: I&amp;rsquo;ll be inspecting robot vacuums which will physically traverse my whole house&lt;/li&gt;
&lt;li&gt;Speed: I&amp;rsquo;ll be dumping and transferring file systems via the network from IoT devices once I gain root. These may be large and I don&amp;rsquo;t want to spend too long waiting&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;easily capture and override DNS lookups&lt;/li&gt;
&lt;li&gt;easily toggle the inspection
&lt;ul&gt;
&lt;li&gt;it&amp;rsquo;s generally annoying to reset WiFi on an IoT device, so being able to toggle inspection on the Mikrotik is super convenient&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;harness the power of &lt;code&gt;mitmproxy&lt;/code&gt; and &lt;code&gt;*shark&lt;/code&gt; programs like &lt;code&gt;wireshark&lt;/code&gt; and &lt;code&gt;tshark&lt;/code&gt; to understand the protocols on these devices&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="building-it-out"&gt;Building it out&lt;/h2&gt;
&lt;h3 id="mikrotik"&gt;Mikrotik&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Create a new IP pool, assign a VLAN ID&lt;/li&gt;
&lt;li&gt;Create a DHCP server, point the gateway to Mikrotik router, and DNS to something like Google/Cloudflare temporarily&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="ubiquiti"&gt;Ubiquiti&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Create a new &amp;ldquo;Network&amp;rdquo;, assign it the VLAN ID of the inspection VLAN created above&lt;/li&gt;
&lt;li&gt;Create a new WiFi network, assign it to the network just created
&lt;ol&gt;
&lt;li&gt;Suggest using an alphanumeric SSID and password as some IoT devices play funny with symbols&lt;/li&gt;
&lt;li&gt;Suggest forcing 2.4Ghz + WPA2 for compatibility, however if you know the device can support 5Ghz use that&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="vm"&gt;VM&lt;/h3&gt;
&lt;p&gt;Fortunately I run Proxmox, so it&amp;rsquo;s very easy for me to spin up a VM for inspection:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ens18&lt;/code&gt; - main NIC on main network, can connect to it from my desktop&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ens19&lt;/code&gt; - secondary NIC assigned to VLAN ID earlier&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Make sure the route tables still have your main NIC as the default route. I&amp;rsquo;m running Debian and needed to edit &lt;code&gt;/etc/network/interfaces&lt;/code&gt;. If you&amp;rsquo;re using another distro or &lt;code&gt;NetworkManager&lt;/code&gt;, follow the appropriate steps.&lt;/p&gt;
&lt;p&gt;In the Mikrotik DHCP server, assign this server a static IP, e.g. &lt;code&gt;192.168.30.2&lt;/code&gt;&lt;/p&gt;
&lt;h4 id="software"&gt;Software&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;Install &lt;code&gt;mitmproxy&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Install &lt;code&gt;dnsmasq&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Install &lt;code&gt;tcpdump&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Install &lt;code&gt;iptables&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h5 id="mitmproxy"&gt;mitmproxy&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;mitmproxy&lt;/code&gt; will be used for TLS decryption and termination. The &lt;code&gt;mitmdump&lt;/code&gt; output can also be handy to see what&amp;rsquo;s going on, however we will mainly be using it to capture the pre-master secrets used in TLS handshakes and throw them into tshark/wireshark later.&lt;/p&gt;
&lt;p&gt;Run mitmproxy with &lt;code&gt;SSLKEYLOGFILE&lt;/code&gt; set to capture the TLS pre-shared keys. For information &lt;a href="https://docs.mitmproxy.org/stable/howto-wireshark-tls/"&gt;see here&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I run this script. The first argument is the name of the output capture file, and I generally run it out of the directory I want to store my captures. I don&amp;rsquo;t look at the &lt;code&gt;mitmproxy&lt;/code&gt; output often, but it&amp;rsquo;s nice to have it stored. I call this script &lt;code&gt;mitm&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;These commands are from:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.mitmproxy.org/stable/howto-transparent/#linux"&gt;https://docs.mitmproxy.org/stable/howto-transparent/#linux&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.mitmproxy.org/stable/howto-wireshark-tls/"&gt;https://docs.mitmproxy.org/stable/howto-wireshark-tls/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/usr/bin/env bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -eu -o pipefail
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ensure dependencies&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# https://docs.mitmproxy.org/stable/howto-transparent/#linux&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo sysctl -w net.ipv4.ip_forward&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo sysctl -w net.ipv6.conf.all.forwarding&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo sysctl -w net.ipv4.conf.all.send_redirects&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; cmd in iptables ip6tables&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; sudo &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$cmd&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -t nat -A PREROUTING -i ens19 -p tcp --dport &lt;span class="m"&gt;80&lt;/span&gt; -j REDIRECT --to-port &lt;span class="m"&gt;8080&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; sudo &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$cmd&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -t nat -A PREROUTING -i ens19 -p tcp --dport &lt;span class="m"&gt;443&lt;/span&gt; -j REDIRECT --to-port &lt;span class="m"&gt;8080&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; sudo &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$cmd&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -t nat -A PREROUTING -i ens19 -p tcp --dport &lt;span class="m"&gt;8883&lt;/span&gt; -j REDIRECT --to-port &lt;span class="m"&gt;8080&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# for wireshark&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;SSLKEYLOGFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;/.mitmproxy/sslkeylogfile.txt
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;outfile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;.mitmdump.txt
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;shift&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mitmdump -m transparent --save-stream-file&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$outfile&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; --tcp-hosts&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;192.168.30.0/24&amp;#39;&lt;/span&gt; --raw --ssl-insecure &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h5 id="dnsmasq"&gt;dnsmasq&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;dnsmasq&lt;/code&gt; is used for capturing and overriding DNS lookups. Some devices may bypass this of course, but I&amp;rsquo;ve found most of the time it works well.&lt;/p&gt;
&lt;p&gt;Setup &lt;code&gt;dnsmasq&lt;/code&gt; to log requests to a file, and point to an upstream DNS&lt;/p&gt;
&lt;p&gt;My config looks like this. Anything I want to override I set at the bottom&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;interface&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ens19&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1.1.1.1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;log-queries&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;log-facility&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/var/log/dnsmasq.log&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;listen-address&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1,192.168.30.2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#address=/jmq-ngiot-au.area.ww.ecouser.net/192.168.30.2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/jmq-ngiot-au.area.ww.ecouser.net/127.0.0.1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h5 id="tcpdump"&gt;tcpdump&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;tcpdump&lt;/code&gt; is used for capturing TCP/IP traffic on a network interface. It can later be parsed.&lt;/p&gt;
&lt;p&gt;Run &lt;code&gt;tcpdump&lt;/code&gt; to dump a &lt;code&gt;.pcap&lt;/code&gt; flie.&lt;/p&gt;
&lt;p&gt;A basic usage is something like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo tcpdump -i ens19 -U -w idle.pcap
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo tcpdump -i ens19 -U -w idle.pcap &lt;span class="s1"&gt;&amp;#39;host xxx.xxx.xxx.xx&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="setting-capturing-up"&gt;Setting capturing up&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Go back in the Mikrotik and set the DHCP server&amp;rsquo;s gateway and DNS to the VM&amp;rsquo;s IP&lt;/li&gt;
&lt;li&gt;Force a DHCP request by having the device reconnect to the AP&lt;/li&gt;
&lt;li&gt;All requests should be going through the VM, see output in the &lt;code&gt;mitmdump&lt;/code&gt; script above&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="tying-it-all-together"&gt;Tying it all together&lt;/h2&gt;
&lt;p&gt;Things aren&amp;rsquo;t as usable as they could be as we&amp;rsquo;re using a VM. I&amp;rsquo;ve solved a lot of friction by tunnelling things over SSH.&lt;/p&gt;
&lt;p&gt;I do the following:&lt;/p&gt;
&lt;h3 id="ssh-proxyjump"&gt;SSH ProxyJump&lt;/h3&gt;
&lt;p&gt;I use &lt;code&gt;ssh X -J VM&lt;/code&gt; to connect to my IoT devices on the inspect network, effectively using my inspect VM as a bastion or jump host.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;-J&lt;/code&gt; is the &lt;code&gt;ProxyJump&lt;/code&gt; flag in SSH. See &lt;code&gt;man ssh&lt;/code&gt; for more information&lt;/p&gt;
&lt;h3 id="network-capture-script"&gt;network capture script&lt;/h3&gt;
&lt;p&gt;I got a bunch of hacky scripts I put together in the heat of the moment to get everything captured in an efficient enough way.&lt;/p&gt;
&lt;p&gt;I have a &amp;ldquo;capture&amp;rdquo; script, which lives on the VM, and which I execute over SSH. It runs &lt;code&gt;tcpdump&lt;/code&gt; and writes output a file on the VM, as well as &lt;code&gt;stdout&lt;/code&gt;, and runs &lt;code&gt;mitmdump&lt;/code&gt; and outputs the logs to &lt;code&gt;stderr&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;capture&lt;/code&gt; script looks like this, where &lt;code&gt;./bin/mitm&lt;/code&gt; is the &lt;a href="#mitmproxy"&gt;mitmproxy script earlier&lt;/a&gt; :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/usr/bin/env bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -eu -o pipefail
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;capture_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;shift&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./bin/mitm &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$capture_name&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;pid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$!&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Run mitm, running tcpdump&amp;#39;&lt;/span&gt; &amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;trap&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;kill &amp;#34;$pid&amp;#34;; echo dead &amp;gt;&amp;amp;2&amp;#39;&lt;/span&gt; EXIT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo tcpdump -i ens19 -U -w - &lt;span class="s1"&gt;&amp;#39;host xxx.xxx.xxx.xxx&amp;#39;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; tee &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$capture_name&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;.pcap
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I also have a cleanup script. It makes sure everything is dead cause running these commands over SSH does not always gracefully exit.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/usr/bin/env bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;killall mitmdump
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo killall tcpdump
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I wrap this in a script on my local machine. I call it &lt;code&gt;capture-mitm&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/usr/bin/env bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -eu -o pipefail
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;capture&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;shift&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;trap&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;ssh inspect ./bin/cleanup&amp;#39;&lt;/span&gt; EXIT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh inspect ./bin/capture &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$capture&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In another shell on my machine, I run a script to stream &lt;code&gt;SSLKEYLOGFILE&lt;/code&gt; to a file, so &lt;code&gt;tshark&lt;/code&gt; and &lt;code&gt;wireshark&lt;/code&gt; work as expected.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh inspect &lt;span class="s1"&gt;&amp;#39;tail -F -n +1 &amp;#34;$HOME&amp;#34;/.mitmproxy/sslkeylogfile.txt&amp;#39;&lt;/span&gt; &amp;gt;/tmp/sslkeylogfile.txt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I can now run &lt;code&gt;capture-mitm CAPTURE_NAME | wireshark -k -i -&lt;/code&gt; and see in real-time the traffic inspection in &lt;code&gt;wireshark&lt;/code&gt; over SSH.&lt;/p&gt;
&lt;p&gt;If I look back at the command I can see &lt;code&gt;mitmdump&lt;/code&gt; output. If I exit out of the command it&amp;rsquo;ll stop capturing traffic on the VM. Any session information is stored on the VM in a &lt;code&gt;CAPTURE_NAME.pcap&lt;/code&gt; file, so I can revisit it later and parse it with &lt;code&gt;tshark&lt;/code&gt; or &lt;code&gt;wireshark&lt;/code&gt; again.&lt;/p&gt;
&lt;p&gt;I like this setup cause I get live data as well as the &amp;ldquo;raw&amp;rdquo; data saved, which is super helpful if you struck upon gold.&lt;/p&gt;
&lt;h3 id="socat--dnsmasq"&gt;socat &amp;amp; dnsmasq&lt;/h3&gt;
&lt;p&gt;I also use &lt;code&gt;socat&lt;/code&gt; &amp;amp; &lt;code&gt;dnsmasq&lt;/code&gt; on the VM to expose any services to the IoT device&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;similar premise to &lt;code&gt;ssh -J&lt;/code&gt;, I&amp;rsquo;m using the VM to forward and expose ports from my local machine to the IoT device&lt;/li&gt;
&lt;li&gt;e.g. &lt;code&gt;sudo socat tcp-listen:443,reuseaddr,fork tcp:192.168.88.x:1883&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;in this example I&amp;rsquo;m redirecting my IoT devices cloud server to my VM via DNS rewrite using &lt;code&gt;dnsmasq&lt;/code&gt;, then I&amp;rsquo;m exposing port 443 on my VM to port 1883 on my machine on my home network&lt;/li&gt;
&lt;li&gt;when the IoT device wants to connect to the cloud, it&amp;rsquo;ll resolve the VM via DNS, then connect to the VM, and the VM will connect to my machine which is on another subnet/VLAN and forward the traffic&lt;/li&gt;
&lt;li&gt;if I want the robot to connect to the real cloud, I can replace my machines IP in the &lt;code&gt;socat&lt;/code&gt; command with the IP of the real cloud. I could point the DNS record back, but then that&amp;rsquo;ll require restarting &lt;code&gt;dnsmasq&lt;/code&gt; and flushing DNS cache on the IoT device (likely doing a reboot)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-result"&gt;The result&lt;/h2&gt;
&lt;p&gt;I want to write a separate post on how I do crazy parsing on &lt;code&gt;MQTT&lt;/code&gt; traffic via &lt;code&gt;tshark&lt;/code&gt;, however here&amp;rsquo;s a sneak peek of what I can do with the setup above:&lt;/p&gt;
&lt;p&gt;Using Wireshark:
&lt;img src="https://jfx.ac/assets/blog/homelab-mikrotik-inspection-network/wireshark.png" alt="Wireshark"&gt;&lt;/p&gt;
&lt;p&gt;Using my fork of &lt;a href="https://github.com/itsjfx/mqttshark"&gt;mqttshark&lt;/a&gt;:
&lt;img src="https://jfx.ac/assets/blog/homelab-mikrotik-inspection-network/mqttshark.png" alt="mqttshark"&gt;&lt;/p&gt;
&lt;p&gt;Using a &lt;a href="https://github.com/itsjfx/ecovacs-hacking/blob/master/bin/parse.py"&gt;hastily written script&lt;/a&gt; which parses the output from &lt;code&gt;tshark&lt;/code&gt; , understands the cloud-specific protocol, and dumps an aggregated summarised version into &lt;code&gt;yaml&lt;/code&gt; (batches requests and responses), which is easier to read and also parse with tools like &lt;code&gt;jq&lt;/code&gt;, &lt;code&gt;gron&lt;/code&gt;, and &lt;code&gt;grep&lt;/code&gt;:
&lt;img src="https://jfx.ac/assets/blog/homelab-mikrotik-inspection-network/parse.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I got 3 different ways to get information about my IoT device and the messages, which makes reverse-engineering much simpler!&lt;/p&gt;
&lt;h2 id="see-also"&gt;See also&lt;/h2&gt;
&lt;p&gt;If you&amp;rsquo;re interested in why I did all this, see:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/itsjfx/ecovacs-hacking"&gt;https://github.com/itsjfx/ecovacs-hacking&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/itsjfx/ValetudoEV/blob/master/ValetudoEV.md"&gt;https://github.com/itsjfx/ValetudoEV/blob/master/ValetudoEV.md&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And thanks to the following resources that made this possible:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/bmartin5692/bumper/blob/master/docs/Sniffing.md"&gt;https://github.com/bmartin5692/bumper/blob/master/docs/Sniffing.md&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.mitmproxy.org/stable/howto-transparent/#linux"&gt;https://docs.mitmproxy.org/stable/howto-transparent/#linux&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.mitmproxy.org/stable/howto-wireshark-tls"&gt;https://docs.mitmproxy.org/stable/howto-wireshark-tls&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Robot vacuum hacking</title><link>https://jfx.ac/blog/robot-vacuum-hacking/</link><pubDate>Mon, 24 Jun 2024 00:00:00 +0000</pubDate><guid>https://jfx.ac/blog/robot-vacuum-hacking/</guid><description>&lt;h2 id="important-notice"&gt;Important Notice&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m writing this post to give a lens into robot hacking and the work being done by the community. This is not aimed to sell the idea of hacking your robot.&lt;/p&gt;
&lt;p&gt;Please do your research and understand the goals and risks of hacking your robot before you try.&lt;/p&gt;
&lt;h2 id="introduction"&gt;Introduction&lt;/h2&gt;
&lt;p&gt;This is a long post on things I wish I knew earlier about hacking robot vacuums. This is not a replacement for existing documentation or help guides. If you only care about rooting your robot and freeing it for the cloud and don&amp;rsquo;t care about how the robot works, chances are this post won&amp;rsquo;t be that helpful to you.&lt;/p&gt;
&lt;p&gt;Earlier this month, I installed custom firmware and a cloud replacement on a second &lt;a href="https://www.dreametech.com/products/dreamebot-l10s-ultra"&gt;Dreame L10s Ultra robot vacuum&lt;/a&gt;. Also released this month (2024-June) was root and cloud-replacement support for the new &lt;a href="https://www.dreametech.com/products/dreametech-x40-ultra-robot-vacuum"&gt;Dreame X40 Ultra&lt;/a&gt; and &lt;a href="https://robotinfo.dev/detail_dreame.vacuum.r2338a_0.html"&gt;Dreame L10s Pro Ultra Heat&lt;/a&gt;, so it&amp;rsquo;s certainly still an exciting time to get into robot hacking!&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll quickly cover about my views on robot vacuums, the robot hardware, the hacking community, the software, and write briefly about my experiences. Feel free to skip over my preamble into robot vacuums if you&amp;rsquo;re not interested.&lt;/p&gt;
&lt;h2 id="why-am-i-crazy-about-robot-vacuums"&gt;Why am I crazy about robot vacuums?&lt;/h2&gt;
&lt;p&gt;Robot vacuums are a great purchase as you can only vacuum and mop your home so much. Dust builds up very quickly and unless you&amp;rsquo;re regularly vacuuming your home daily, you&amp;rsquo;ll find that dust will travel across the house onto more surfaces.&lt;/p&gt;
&lt;p&gt;Modern robots have advanced mop heads which spin at fast speeds to get your floors clean. They can take in clean water from a water tank in the dock, use detergent, self-clean &amp;amp; dry the mop, and empty the dirty water into a secondary dirty water tank for you to empty into your drain.&lt;/p&gt;
&lt;p&gt;The mop means my hardwood floors are shiny which makes me happy. I find mopping tiring so it helps relieve some physical hardship.&lt;/p&gt;
&lt;p&gt;I also got a robot for my elderly parents which helps reduce physical effort needed by them to maintain a tidy house. It&amp;rsquo;s a great piece of &lt;strong&gt;luxury hardware&lt;/strong&gt; if you can justify spending the money on one.&lt;/p&gt;
&lt;h2 id="why-am-i-also-unhappy-with-robot-vacuums"&gt;Why am I also unhappy with robot vacuums?&lt;/h2&gt;
&lt;p&gt;One of the main factors is privacy. As a consumer you place a lot of trust on the vendor. You don&amp;rsquo;t have any control of the software running on the robot. It requires internet access, has cameras, sensors, and local AI models equipped.&lt;/p&gt;
&lt;p&gt;These aren&amp;rsquo;t crazy concerns to have. If you watch &lt;a href="https://www.youtube.com/watch?v=56N1dYfdVf4"&gt;Dennis Giese &amp;amp; braelynn&amp;rsquo;s talk at 37C3&lt;/a&gt;, they showcase that robots made by a specific vendor have user data which is publicly accessible, remains on cloud servers past account deletion, and remains on the robot past factory reset. The same vendors robots software avoids TLS checks as they have shell scripts with &lt;code&gt;wget --no-check-certificate&lt;/code&gt; in the firmware. Additionally they showcased an exploit where the live video feed for a robot is accessible under certain conditions as a pin check is enforced client side in the application.&lt;/p&gt;
&lt;p&gt;Other than privacy, you may not want a robot due to high cost, messy or uneven flooring, or carpet, which in recent years is less of an issue due to detachable mop vacuums.&lt;/p&gt;
&lt;p&gt;Update: Dennis Giese published a zero-touch RCE via BLE for the same vendors robot. An article is available here showcasing the exploit: &lt;a href="https://www.abc.net.au/news/2024-10-04/robot-vacuum-hacked-photos-camera-audio/104414020"&gt;https://www.abc.net.au/news/2024-10-04/robot-vacuum-hacked-photos-camera-audio/104414020&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="robot-hardware-breakdown"&gt;Robot hardware breakdown&lt;/h2&gt;
&lt;p&gt;Below is a hardware breakdown diagram from the same talk mentioned earlier.&lt;/p&gt;
&lt;a href="https://jfx.ac/assets/blog/robot-vacuum-hacking/hardware-architecture.png" target="_blank"&gt;
&lt;img src="https://jfx.ac/assets/blog/robot-vacuum-hacking/hardware-architecture.png" alt="" style="max-width: min(95vw, 120%)"&gt;
&lt;/a&gt;
&lt;h3 id="the-soc"&gt;The SoC&lt;/h3&gt;
&lt;p&gt;The system on a chip (SoC) is the application processor which handles navigation, mapping and networking. It typically runs Linux.&lt;/p&gt;
&lt;p&gt;Custom vendor software runs on the robot as a daemon and does most of the heavy lifting handling cloud communication, OTA, robot control, mapping, and other user facing functions. On Dreame this software is called &lt;a href="https://github.com/alufers/dreame_mcu_protocol/blob/master/dreame_z10_notes.md#ava-nodes"&gt;ava&lt;/a&gt;, and on Ecovacs it&amp;rsquo;s called &lt;a href="https://dontvacuum.me/talks/37c3-2023/37c3-vacuuming-and-mowing.pdf"&gt;medusa&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Some interesting things I found:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Some vendors have a lot of shell scripts with poor bash/shell scripting practices which get called on the SoC. These scripts may interact with the OS, handle WiFi (e.g. interact with &lt;code&gt;wpa_supplicant&lt;/code&gt;), or interact with the custom vendor software.&lt;/li&gt;
&lt;li&gt;As the camera is part of the SoC, on some devices it can be accessed as a video device on &lt;code&gt;/dev/videoX&lt;/code&gt; .&lt;/li&gt;
&lt;li&gt;Newer robots can be seen having 4 core SoCs, 1-2GB of RAM, 4-8GB of flash storage, and getting more powerful overtime.&lt;/li&gt;
&lt;li&gt;My robot, the Dreame L10s Ultra runs Athena Linux ARM64 on the SoC
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Linux r2228_release 4.9.191 #1 SMP PREEMPT Wed Sep 13 15:19:31 CST 2023 aarch64 GNU/Linux&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="the-mcu"&gt;The MCU&lt;/h3&gt;
&lt;p&gt;The micro-controller unit (MCU) handles real-time operations such as motors and sensors. This has it&amp;rsquo;s own firmware separate to the SoC&amp;rsquo;s firmware/OS. &lt;a href="https://github.com/alufers/dreame_mcu_protocol/blob/master/dreame_z10_notes.md"&gt;Information on the MCU firmware for the Dreame Z10 is available here&lt;/a&gt; if you&amp;rsquo;d like to learn about what lives in the MCU for a robot.&lt;/p&gt;
&lt;p&gt;MCU interest is not as popular. It requires a different set of skills. There&amp;rsquo;s a lot of secret sauce baked into the MCU, so vendors make it difficult for people to understand what&amp;rsquo;s happening. Most people are interested in the SoC and the cloud integration. I&amp;rsquo;d suggest only learning more if you&amp;rsquo;re interested in hardware or MCUs.&lt;/p&gt;
&lt;h3 id="finding-hardware-information"&gt;Finding hardware information&lt;/h3&gt;
&lt;p&gt;Dennis Giese has done research on a large number of robots and &lt;a href="https://robotinfo.dev"&gt;published information here&lt;/a&gt;. Information on the hardware used as well as software &amp;amp; firmware versions, links to rooting methods and custom firmware downloads are available for many robots.&lt;/p&gt;
&lt;p&gt;There are sometimes breakdowns of the robot boards themselves. It&amp;rsquo;s an extremely valuable resource to understand what builds up a robot.&lt;/p&gt;
&lt;h2 id="the-robot-hacking-community"&gt;The robot hacking community&lt;/h2&gt;
&lt;p&gt;The community seems to be broken down to the following groups of people:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;People interested in removing the cloud from their robot
&lt;ul&gt;
&lt;li&gt;This is the average hacker and makes a large portion of the community&lt;/li&gt;
&lt;li&gt;Mixed level of skills&lt;/li&gt;
&lt;li&gt;Many sadly incapable of reading documentation/FAQs or following instructions and complain on Telegram or GitHub&lt;/li&gt;
&lt;li&gt;It&amp;rsquo;s disappointing to see in the chats as it makes approaching the community as an outsider more difficult as such&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;People interested in the hardware&lt;/li&gt;
&lt;li&gt;People interested in the firmware/software on the SoC
&lt;ul&gt;
&lt;li&gt;Gaining root&lt;/li&gt;
&lt;li&gt;Building custom cloud implementations&lt;/li&gt;
&lt;li&gt;Research&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;People interested in the firmware on the MCU&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are many people who contribute greatly in the community, but it boils down to these two people (who are the most public):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dontvacuum.me/"&gt;Dennis Giese&lt;/a&gt;, an IoT researcher
&lt;ul&gt;
&lt;li&gt;Dennis maintains &lt;a href="https://dustbuilder.dontvacuum.me/"&gt;Dustbuilder&lt;/a&gt;, a website which allows you to create custom rooted firmware for robot vacuums&lt;/li&gt;
&lt;li&gt;Dennis also maintains the &lt;a href="https://robotinfo.dev/"&gt;Robot Info website&lt;/a&gt; (mentioned earlier)&lt;/li&gt;
&lt;li&gt;Dennis and others discover root methods, gain persistence, and verify vendor claims&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Hypfer"&gt;Sören Beye (known as Hypfer)&lt;/a&gt; who is the maintainer of &lt;a href="https://valetudo.cloud"&gt;Valetudo&lt;/a&gt;, a cloud replacement software for robot vacuums
&lt;ul&gt;
&lt;li&gt;Hypfer is very vocal about his views and vocalising the strug of maintaining a popular open-source project and providing support. See the &amp;ldquo;On not burning out&amp;rdquo; section in this release: &lt;a href="https://github.com/Hypfer/Valetudo/releases/tag/2024.06.2"&gt;https://github.com/Hypfer/Valetudo/releases/tag/2024.06.2&lt;/a&gt;. It&amp;rsquo;s a good read. Don&amp;rsquo;t be like these people if you visit the Telegram chat.&lt;/li&gt;
&lt;li&gt;FWIW: I found that the &lt;a href="https://valetudo.cloud/pages/general/getting-started.html"&gt;Valetudo/rooting how-to guides&lt;/a&gt; and surrounding documentation are simple enough to follow and answer enough questions that you &lt;em&gt;shouldn&amp;rsquo;t need&lt;/em&gt; additional support in Telegram.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="dustbuilder"&gt;Dustbuilder&lt;/h2&gt;
&lt;p&gt;Once a root method is discovered and published, a &lt;a href="https://dustbuilder.dontvacuum.me/"&gt;Dustbuilder&lt;/a&gt; page for a robot is generally created sometime later.&lt;/p&gt;
&lt;p&gt;Dustbuilder creates a patched firmware to run on your SoC, and gives you an MCU firmware image. It saves you from dumping and patching the firmware yourself (which may not be possible), and saves you from doing it incorrectly.&lt;/p&gt;
&lt;p&gt;It has several patches to allow you to gain persistence on the robot, and disable cloud connectivity, calling home features like telemetry, video recordings. It allows you to use Valetudo, and much more. You can see the diff from the original firmware image which is really helpful in understanding what it does.&lt;/p&gt;
&lt;p&gt;If a vendor updates the firmware, it&amp;rsquo;s likely the Dustbuilder firmware will also be updated (once determined stable). This means you can continue to receive vendor updates to the MCU/SoC even when rooting.&lt;/p&gt;
&lt;h2 id="valetudo"&gt;Valetudo&lt;/h2&gt;
&lt;h3 id="what-is-it-tech-wise"&gt;What is it? (tech wise)&lt;/h3&gt;
&lt;p&gt;As a preface, I&amp;rsquo;d suggest reading the &lt;a href="https://valetudo.cloud/pages/general/newcomer-guide.html"&gt;Valetudo Newcomer Guide&lt;/a&gt; and come back here. I&amp;rsquo;m going to write mostly about my observations about tech itself rather than what the software offers and it&amp;rsquo;s goals. Please read the official documentation to understand the project.&lt;/p&gt;
&lt;p&gt;As mentioned, Valetudo is a cloud replacement for robot vacuums. It has a frontend written in React + TypeScript, and a backend written in Node.js.&lt;/p&gt;
&lt;p&gt;Valetudo is an interesting piece of software as it will run an API, serve a frontend, act as an MQTT client, and implement the robots cloud all as a single binary on your robot.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not a firmware replacement unlike other articles on the internet mention. Instead it sits alongside existing firmware on the robot, typically a patched firmware from Dustbuilder. As it does not replace the firmware, &lt;em&gt;you shouldn&amp;rsquo;t&lt;/em&gt; lose functionality of the original robot.&lt;/p&gt;
&lt;p&gt;Before installing Valetudo I was expecting that I&amp;rsquo;d have to host it on another machine as it&amp;rsquo;s a cloud replacement, but this is not the case. It has no dependencies on other infrastructure like a server, except for NTP, and has no dependency on a mobile app interface like stock firmware robot vacuums.&lt;/p&gt;
&lt;p&gt;Cause of it&amp;rsquo;s architecture, Valetudo won&amp;rsquo;t face any issues due to infrastructure being down or having a conflicting version. It will simply operate as expected as long as it can continue to run on the robot. If you have multiple robots, then they will each run their own instance of Valetudo.&lt;/p&gt;
&lt;p&gt;This has the con that the robot has to be powerful enough to run Valetudo as well as its original firmware, however the maintainer has taken a lot of efforts to ensure Valetudo runs with a low hardware footprint.&lt;/p&gt;
&lt;p&gt;Valetudo &lt;a href="https://valetudo.cloud/pages/general/supported-robots.html"&gt;officially supports 35 different robots&lt;/a&gt; across different vendors. Officially supported means each robot has been tested by Hypfer, with the root method documented for each robot. It&amp;rsquo;s insane that a single piece of software not only works with so many robots, but that the developer has also tested it themselves and continue to hold the software to this standard.&lt;/p&gt;
&lt;p&gt;Valetudo has well documented releases every few months with breaking changes documented, and can also update itself in the UI. Newer versions may have UI updates, bug fixes, but more importantly support for newer robots.&lt;/p&gt;
&lt;h3 id="using-it"&gt;Using it&lt;/h3&gt;
&lt;p&gt;To use Valetudo, you simply go to it&amp;rsquo;s web UI, which is accessible via your robots private IP, or by port forwarding to the web server on the machine via SSH. The web UI offers you control of your robot like you would in a native app.&lt;/p&gt;
&lt;p&gt;Of course it can integrate with systems like Home Assistant via it&amp;rsquo;s MQTT client, which will then allow you to control it via Home Assistant with various integrations. At the moment, &lt;a href="https://github.com/PiotrMachowski/lovelace-xiaomi-vacuum-map-card"&gt;I use this card&lt;/a&gt; to control my robot in Home Assistant.&lt;/p&gt;
&lt;h3 id="whywhy-not"&gt;Why/why not&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;m not going to write about why or why not to use Valetudo. If this got your interest, read the below pages, and the surrounding documentation on the Valetudo website:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://valetudo.cloud/pages/general/why-valetudo.html"&gt;Why Valetudo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://valetudo.cloud/pages/general/why-not-valetudo.html"&gt;Why not Valetudo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="my-rooting-journey"&gt;My rooting journey&lt;/h2&gt;
&lt;p&gt;I followed the Dreame fastboot instructions for both my Dreame L10s Ultra&amp;rsquo;s. The robot is slowly approaching end of life so I was lucky to get it on sale. I couldn&amp;rsquo;t easily obtain the Valetudo rooting PCB board so I used some dupont cables and an old USB mouse with a USB 2.0 cable (rip mouse). It worked OK minus some USB 2.0 shenanigans. If I could do it again, I would put the effort in to obtain the PCB board as I probably wasted 2 hours getting the USB connection to work correctly.&lt;/p&gt;
&lt;p&gt;I previously had a Roborock S6 MaxV which I couldn&amp;rsquo;t root and faced many issues getting the Home Assistant integration working. Occasionally the vendor would rate limit my Home Assistant integration or change their API, meaning I&amp;rsquo;d have to wait for the maintainer of the integration to address the issue. I&amp;rsquo;ve not faced any issues with my L10s Ultra and Valetudo due to its native MQTT client and Home Assistant support. The robot has not skipped a beat and is far more responsive than my previous vendor cloud locked Roborock. I also have no internet calls coming out of my robot (using a NTP server on my LAN), so I&amp;rsquo;m super pleased with that.&lt;/p&gt;
&lt;h2 id="whats-upcoming-in-the-community"&gt;What&amp;rsquo;s upcoming in the community?&lt;/h2&gt;
&lt;p&gt;In Dennis Giese&amp;rsquo;s recent talk he covered Ecovacs vacuums. The root method for many models is now public, however there is no Valetudo support for them right now. Please do not ask the maintainers when they will supported. We can only hope there&amp;rsquo;ll be Valetudo support in the future or a cloud replacement for these robots.&lt;/p&gt;
&lt;h2 id="update"&gt;Update&lt;/h2&gt;
&lt;p&gt;I spent a month hacking my Ecovacs X1 Omni. I published some notes and a forked version of Valetudo which works on my robot.&lt;/p&gt;
&lt;p&gt;See the links below:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/itsjfx/ecovacs-hacking"&gt;https://github.com/itsjfx/ecovacs-hacking&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/itsjfx/ValetudoEV"&gt;https://github.com/itsjfx/ValetudoEV&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/itsjfx/ValetudoEV/blob/master/ValetudoEV.md"&gt;https://github.com/itsjfx/ValetudoEV/blob/master/ValetudoEV.md&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="see-also"&gt;See also&lt;/h2&gt;
&lt;p&gt;The resources below helped me understand the things I wrote about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dennis Giese&amp;rsquo;s talks (these are a must watch):
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=EWqFxQpRbv8"&gt;DEF CON 29- Dennis Giese - Robots with lasers and cameras but no security Liberating your vacuum&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=AfMfYOUYZvc"&gt;DEF CON 31 - Vacuum Robot Security &amp;amp; Privacy Prevent yr Robot from Sucking Your Data - Dennis Giese&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=56N1dYfdVf4"&gt;37C3 - Sucking dust and cutting grass: reversing robots and bypassing security&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://valetudo.cloud"&gt;Valetudo&amp;rsquo;s website and documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dontvacuum.me"&gt;Dennis Giese&amp;rsquo;s website&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://robotinfo.dev"&gt;robotinfo.dev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://builder.dontvacuum.me"&gt;Dustbuilder&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/alufers/dreame_mcu_protocol/tree/master"&gt;alufers/dreame_mcu_protocol&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>My workflow philosophy</title><link>https://jfx.ac/blog/my-workflow-philosophy/</link><pubDate>Sun, 12 May 2024 00:00:00 +0000</pubDate><guid>https://jfx.ac/blog/my-workflow-philosophy/</guid><description>&lt;h2 id="introduction"&gt;Introduction&lt;/h2&gt;
&lt;p&gt;This is a overview of my &amp;ldquo;workflow&amp;rdquo;, a really long rant on how I deal with
technology choices related to my workflow, and why I do things &amp;ldquo;my way&amp;rdquo;. I&amp;rsquo;m
hoping somebody will find this useful in making their choices.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m also writing this as a preface for my future blog posts where I discuss part
of my workflow. I&amp;rsquo;ll keep the below updated with links as they come out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Working anywhere with SSH, FIDO2, and Git (coming &lt;em&gt;soon&lt;/em&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="summary-of-my-workflow"&gt;Summary of my workflow&lt;/h2&gt;
&lt;p&gt;At the moment I use 3 hosts (almost) daily:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;my desktop (Linux)
&lt;ul&gt;
&lt;li&gt;Windows dual booted for gaming&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;my personal laptop (Linux)&lt;/li&gt;
&lt;li&gt;my work laptop (Linux)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On each hosts, some key technologies:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Arch Linux encrypted with LUKS + btrfs (see &lt;a href="https://notes.jfx.ac/linux/install"&gt;Linux
Install&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Encrypted self-hosted WireGuard VPN to access most of my hosts, and TailScale
for others&lt;/li&gt;
&lt;li&gt;I use SSH via FIDO2 to connect to my hosts, copy files
&lt;ul&gt;
&lt;li&gt;Alternative &amp;ldquo;personal&amp;rdquo; SSH Agent on my work machine to not leak keys from
work&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Git operations to GitHub, commit signing, all with my FIDO2 key. Works over an
SSH connection from another one of my hosts.&lt;/li&gt;
&lt;li&gt;File copying with &lt;code&gt;SCP&lt;/code&gt; &amp;amp; &lt;code&gt;rsync&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;VS Code with the &lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-ssh"&gt;Remote SSH
extension&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;Can do all the operations listed above in the built-in terminal&lt;/li&gt;
&lt;li&gt;and &lt;a href="https://github.com/itsjfx/dotfiles/blob/master/.config/extman/extensions.yaml"&gt;a ton of other
extensions&lt;/a&gt;
to help with my workflow&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Neovim&lt;/li&gt;
&lt;li&gt;tmux
&lt;ul&gt;
&lt;li&gt;Using my &lt;a href="https://github.com/itsjfx/zsh-tmux-smart-status-bar"&gt;custom tmux status
bar&lt;/a&gt; to give me
context of my shell without flooding my prompt&lt;/li&gt;
&lt;li&gt;if I&amp;rsquo;m working heavily in a shell, then I&amp;rsquo;ll work in a tmux session over
SSH, which means I can resume work from any one of my machines later&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Obsidian for note taking
&lt;ul&gt;
&lt;li&gt;Notes synced across devices&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Same base config and bootstrap across machines, through &lt;a href="https://github.com/itsjfx/dotfiles"&gt;my dot
files&lt;/a&gt;, all managed in Git
&lt;ul&gt;
&lt;li&gt;the same &lt;a href="https://i3wm.org/"&gt;i3&lt;/a&gt;/&lt;code&gt;x11&lt;/code&gt; setup, key bindings, etc&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;as well as a ton of other programs and utilities
&lt;ul&gt;
&lt;li&gt;more mentioned below&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I will try write blog posts on each thing above. In the meantime, most of it is
in &lt;a href="https://github.com/itsjfx/dotfiles"&gt;my dot files&lt;/a&gt;, or I scribbled about it
in &lt;a href="https://notes.jfx.ac"&gt;my public notes&lt;/a&gt; if you&amp;rsquo;re bold enough to fish them
out.&lt;/p&gt;
&lt;h2 id="principles"&gt;Principles&lt;/h2&gt;
&lt;p&gt;Generally I try to follow these principles in regards to my workflow. I also
follow some of these when I approach new technologies in system design, but not
all are mutually exclusive:&lt;/p&gt;
&lt;h3 id="1-secure"&gt;1) secure&lt;/h3&gt;
&lt;p&gt;This is a no-brainer.&lt;/p&gt;
&lt;p&gt;Keep in mind, this is rated number one in importance, but not in practice.
There&amp;rsquo;s a fine balance to strike as things must be secure, but still practical
and useable.&lt;/p&gt;
&lt;p&gt;If I &lt;em&gt;really&lt;/em&gt; wanted things to be truly secure, then I would have no computer or
smart phone, and only pay in cash. Of course this is not practical.&lt;/p&gt;
&lt;p&gt;Some relevant rules I try to follow are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;limited public facing services (e.g. limit exposure of SSH to the internet)&lt;/li&gt;
&lt;li&gt;adopt hardware keys where supported (and where not lazy)&lt;/li&gt;
&lt;li&gt;where hardware keys are not possible, use a password manager, and other forms
of 2 factor&lt;/li&gt;
&lt;li&gt;long passwords&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These rules are important cause they dictate how I&amp;rsquo;m able to work. Now in order
to work, I &lt;em&gt;should&lt;/em&gt; have easy access to hardware keys, and easy access to my
non-public facing services.&lt;/p&gt;
&lt;h3 id="2-simple-and-boring"&gt;2) simple and boring&lt;/h3&gt;
&lt;p&gt;I use a lot of boring technologies. If it&amp;rsquo;s not boring, then I&amp;rsquo;ve likely had a
good reason to choose it, and have a decent understanding on how it works. I
likely understand how the &amp;ldquo;boring&amp;rdquo; alternative works in case the &amp;ldquo;cool&amp;rdquo; thing
I&amp;rsquo;m using is no longer &amp;ldquo;cool&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s easy to bet on a bad technology that will no longer be supported or
maintained, but it&amp;rsquo;s even easier to pick a good one. Use proven software that
has already solved the problem and has stood the test of time. Software like
Linux and GNU.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m also generally a slow adopter of new &amp;ldquo;cool&amp;rdquo; technology due to being cynical
and lazy. Unless I try something for the first time and really think, &amp;ldquo;wow this
is insane, I can&amp;rsquo;t believe nobody else is using this!&amp;rdquo;, I&amp;rsquo;ll probably keep an
eye on it and see how it stands the test of time.&lt;/p&gt;
&lt;p&gt;If I&amp;rsquo;m also getting all I need out of something, and I don&amp;rsquo;t wish to put more
time into improving it, why would I risk my current workflow to move to
something &amp;ldquo;cooler&amp;rdquo;? I call this tactical laziness.&lt;/p&gt;
&lt;p&gt;Adopting new technologies takes time and effort. That said, it&amp;rsquo;s low risk and
effort to try something and see what life is like on the other side.&lt;/p&gt;
&lt;p&gt;An example is &lt;a href="https://obsidian.md/"&gt;Obsidian&lt;/a&gt;. A colleague showed it to me, I
was impressed, then it took me a year to switch from my existing workflow with
Joplin and VS Code. Why? Cause I had everything set up great already. Linting
rules, note sync working, spell checking, and settings I felt were sensible.
Everything was &lt;em&gt;just right&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;To switch to Obsidian, I needed to migrate my notes, get my sync working, get my
settings right, plus learn whatever default / new features are applied by
Obsidian and flick things on or off. And the final thing (which will never stop)
is to learn how to optimise my workflow with all the new things I can do in
Obsidian.&lt;/p&gt;
&lt;p&gt;The move to Obsidian has paid off though. It&amp;rsquo;s made me take more notes and feel
better doing it. However, in the year between my colleague showing me Obsidian
and when I finally switched to Obsidian, I probably dealt with &lt;em&gt;some&lt;/em&gt; more
important things short-term things, but I still wish I&amp;rsquo;d switched sooner.&lt;/p&gt;
&lt;p&gt;See also: &lt;a href="https://boringtechnology.club"&gt;https://boringtechnology.club&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="3-stable-but-also-have-workarounds"&gt;3) stable, but also have workarounds&lt;/h3&gt;
&lt;p&gt;Reality is things will catastrophically fail at some stage. If I cannot access
to my network via my VPN, do I have another point of access or some form of
break-glass?&lt;/p&gt;
&lt;p&gt;This cascades from the last point: &lt;em&gt;generally&lt;/em&gt; simple things are stable, or
simple to troubleshoot and resolve.&lt;/p&gt;
&lt;h3 id="4-use-my-preferred-technology-where-possible"&gt;4) use my preferred technology where possible&lt;/h3&gt;
&lt;p&gt;Some examples are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Linux as a desktop instead of Windows or Mac&lt;/li&gt;
&lt;li&gt;Firefox, with a container based workflow so I can continue using the same
browser session
&lt;ul&gt;
&lt;li&gt;e.g. to login to multiple accounts&lt;/li&gt;
&lt;li&gt;this means i retain my open tabs, hotkeys, extensions, and history&lt;/li&gt;
&lt;li&gt;i may use profiles if different Firefox settings are needed&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CLI / terminal
&lt;ul&gt;
&lt;li&gt;I love using CLI tools or a terminal over a GUI as you can be more
efficient. GUIs are opinionated, can differ between application, and are
generally heavier and slower than using a terminal.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;open-source tooling over proprietary tooling (where applicable)
&lt;ul&gt;
&lt;li&gt;I still use Microsoft&amp;rsquo;s Visual Studio Code build over the open-source one&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;VS Code or &lt;code&gt;vim&lt;/code&gt; to edit files, and finding ways to integrate more into these
editors&lt;/li&gt;
&lt;li&gt;Hardware keys instead of 2FA&lt;/li&gt;
&lt;li&gt;Unix programs:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bash&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;grep&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cut&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sed&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;jq&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fzf&lt;/code&gt; to find things&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="5-fast"&gt;5) fast&lt;/h3&gt;
&lt;p&gt;Building from my previous point. I want to be fast when working on the computer.
If you do things faster, then you can do more things. It&amp;rsquo;s a superpower, and
addicting. Once you get used to being fast you can&amp;rsquo;t go back to being slow.&lt;/p&gt;
&lt;p&gt;Having good, quick tooling, shortcuts / quicker ways of gaining access to
something or obtaining information will make you faster than most people.&lt;/p&gt;
&lt;p&gt;This doesn&amp;rsquo;t mean a bleeding edge, fast machine, or a gigabit internet
connection. It means spending less time doing or thinking about how to do &amp;ldquo;crap&amp;rdquo;
(cause you&amp;rsquo;ve automated it, or figured out you don&amp;rsquo;t need to do it), and
spending that time on more meaningful things.&lt;/p&gt;
&lt;p&gt;An example is &lt;code&gt;git&lt;/code&gt; aliases, or tools like
&lt;a href="https://github.com/bash-my-aws/bash-my-aws"&gt;bash-my-aws&lt;/a&gt;. It seems like a
gimmick at first, but overall you save a lot of time not typing out the same
verbose commands 20 times a day. Once you get used to them, you&amp;rsquo;ll find yourself
using them all the time instead of GUIs, and wanting to use GUIs less.&lt;/p&gt;
&lt;h3 id="6-resumable"&gt;6) resumable&lt;/h3&gt;
&lt;p&gt;If I&amp;rsquo;m working on something I&amp;rsquo;d like to be able to resume it at any given time on the same machine or another one. This helps me not skip a beat as I may drop what I&amp;rsquo;m doing to do something else, or move somewhere (e.g. the couch) and want to continue what I was doing quickly.&lt;/p&gt;
&lt;p&gt;Examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;my shell sessions all run &lt;code&gt;tmux&lt;/code&gt;, so if I use a different machine I can SSH back in and attach the &lt;code&gt;tmux&lt;/code&gt; session and pick up from where I was&lt;/li&gt;
&lt;li&gt;my Obsidian vault is synced live across my machines using &lt;a href="https://github.com/vrtmrz/obsidian-livesync"&gt;obsidian-livesync&lt;/a&gt; so I can write things on my phone and get it real-time on my machines (and vice-versa).&lt;/li&gt;
&lt;li&gt;my &lt;code&gt;cmus&lt;/code&gt; sessions resume from where I last used &lt;code&gt;cmus&lt;/code&gt; by setting &lt;code&gt;resume=true&lt;/code&gt;. See &lt;a href="https://linux.die.net/man/1/cmus"&gt;man cmus&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="7-seamless-experience-between-machines"&gt;7) seamless experience between machines&lt;/h3&gt;
&lt;p&gt;I find it frustrating when devices or software across machines behave
differently. Of course it takes &lt;em&gt;some&lt;/em&gt; effort to set up a &amp;ldquo;settings sync&amp;rdquo; (built
into many modern applications now days), or &lt;a href="https://github.com/itsjfx/dotfiles"&gt;dot
files&lt;/a&gt;, but it pays off.&lt;/p&gt;
&lt;p&gt;This is why I like hotkeys and config to be consistent across devices. It means
I don&amp;rsquo;t need to think about how I&amp;rsquo;m using one of my machines, and I just use it.&lt;/p&gt;
&lt;p&gt;Working across fundamental barriers (such as operating systems) makes this
challenging, but I&amp;rsquo;ve tried to keep things as seamless as possible.&lt;/p&gt;
&lt;p&gt;For example, my &lt;a href="https://i3wm.org/"&gt;i3&lt;/a&gt; setup uses tools like
&lt;a href="https://github.com/sagb/alttab"&gt;alttab&lt;/a&gt; and
&lt;a href="https://github.com/lincheney/i3-automark"&gt;i3-automark&lt;/a&gt; which make it &lt;em&gt;feel&lt;/em&gt; a
lot like Windows, which I used as my daily desktop operating system until 2022.
This makes it easy for me to use a Windows machine again as I&amp;rsquo;ve not lost all my
habits. I also don&amp;rsquo;t think I&amp;rsquo;ve made myself slower by doing this. I like to
think that I&amp;rsquo;ve taken the best from Windows and brought it to my &lt;a href="https://en.wikipedia.org/wiki/Tiling_window_manager"&gt;tiling window
manager&lt;/a&gt; workflow.&lt;/p&gt;
&lt;p&gt;I also bootstrap my machines the same way with &lt;a href="https://github.com/itsjfx/dotfiles"&gt;my dot
files&lt;/a&gt; and my scripts. &lt;a href="https://github.com/itsjfx/Win10-Initial-Setup-Script"&gt;Even on
Windows&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="8-consistent-experience-across-applications"&gt;8) consistent experience across applications&lt;/h3&gt;
&lt;p&gt;I prefer using vim bindings where possible so I don&amp;rsquo;t need to learn application
specific hotkeys. This enables me to use the keyboard more and be more
efficient.&lt;/p&gt;
&lt;p&gt;Other basic things, like CTRL/ALT+Number to select tabs, CTRL + T to open tabs.
I want things to be the same.&lt;/p&gt;
&lt;h3 id="9-version-controlled"&gt;9) version controlled&lt;/h3&gt;
&lt;p&gt;If I&amp;rsquo;ve set something up I like, it should be in some form version control. And
if not version controlled, then documented so I can revisit in the future.&lt;/p&gt;</description></item><item><title>Iterators and Generators in Go</title><link>https://jfx.ac/blog/go-iterators-and-generators/</link><pubDate>Sun, 21 Apr 2024 00:00:00 +0000</pubDate><guid>https://jfx.ac/blog/go-iterators-and-generators/</guid><description>&lt;h2 id="introduction"&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Something I&amp;rsquo;ve found missing in Go is built-in generator functions and an
iterator interface. Python &amp;amp; JavaScript both have them and I find myself using
them regularly. Having syntactic sugar to express a generator &amp;amp; iterators is
great for consistency sake, and also makes them more accessible for
implementers.&lt;/p&gt;
&lt;h2 id="what-are-generator-functions"&gt;What are generator functions?&lt;/h2&gt;
&lt;p&gt;A generator function produces values on the fly or &amp;ldquo;lazily&amp;rdquo;, instead of
generating them all at once and storing them in memory.&lt;/p&gt;
&lt;p&gt;In most languages/implementations, it typically utilises the &lt;code&gt;yield&lt;/code&gt; keyword to
temporarily suspend execution and &amp;ldquo;yield&amp;rdquo; a value to the caller, allowing the
function to be resumed later, efficiently maintaining its state.&lt;/p&gt;
&lt;p&gt;To a consumer iterating over the function, it behaves similar to a function
that returns an array, making it easy to consume.&lt;/p&gt;
&lt;p&gt;Generators significantly reduce memory overhead and improve performance where
memory constraints are a concern, or when dealing with computationally expensive
operations. Iterators created with generator functions facilitate clean and
concise code, which improves readability and maintainability.&lt;/p&gt;
&lt;h2 id="go-122-rangefunc-experiment"&gt;Go 1.22 rangefunc experiment&lt;/h2&gt;
&lt;p&gt;Luckily Go 1.22 shipped with &lt;a href="https://tip.golang.org/wiki/RangefuncExperiment"&gt;a preliminary implementation for function
iterators&lt;/a&gt;. It can be enabled
by setting &lt;code&gt;GOEXPERIMENT=rangefunc&lt;/code&gt; when building your Go program.&lt;/p&gt;
&lt;p&gt;Using VS Code? Add this to your &lt;code&gt;settings.json&lt;/code&gt; to fix the errors displayed:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;go.toolsEnvVars&amp;#34;&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;GOEXPERIMENT&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;rangefunc&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;ve been using it the past month and found it pleasant. Below I&amp;rsquo;ll quickly
explain the interface and also show some examples.&lt;/p&gt;
&lt;h3 id="the-simplest-generator"&gt;the simplest generator&lt;/h3&gt;
&lt;p&gt;A generator function which yields integers would have this interface:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;yield&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Where the function will call the exposed &lt;code&gt;yield&lt;/code&gt; function to &amp;ldquo;yield&amp;rdquo; results,
and return to complete.&lt;/p&gt;
&lt;p&gt;Below is an example of the above function prototype with a rate limited
fibonacci generator&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;fmt&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;time&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Fibonacci&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;yield&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;yield&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;num&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Fibonacci&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;num&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Millisecond&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="checking-the-return-value-of-yield"&gt;checking the return value of &lt;code&gt;yield()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Why must we check the return value of &lt;code&gt;yield()&lt;/code&gt; ? This is how the generator
knows it should stop yielding values, and can clean up anything before ending
execution by returning. If the function ignores the return value of &lt;code&gt;yield()&lt;/code&gt;
and continues it will &lt;code&gt;panic&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="yielding-an-error"&gt;yielding an error&lt;/h3&gt;
&lt;p&gt;We can also yield up to two values (but no more than 2!)&lt;/p&gt;
&lt;p&gt;e.g:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;yield&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This makes it great for returning errors in our second argument, then the
generator can clean up any resources and &lt;code&gt;return&lt;/code&gt; to complete execution.&lt;/p&gt;
&lt;h3 id="what-about-parameters"&gt;what about parameters?&lt;/h3&gt;
&lt;p&gt;The generator function prototype above does not allow you provide any
parameters, which in most situations would be pretty useless. Fortunately the
new &lt;code&gt;iter&lt;/code&gt; package returns &lt;code&gt;iter.Seq&lt;/code&gt; and &lt;code&gt;iter.Seq2&lt;/code&gt; types, which you can use
in a parent function to return your iterator.&lt;/p&gt;
&lt;p&gt;A simple example which yields the same string parameter (with value &lt;code&gt;ahh&lt;/code&gt;) ten
times looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Seq&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;yield&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;yield&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;ahh&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="an-example-with-http"&gt;an example with http&lt;/h3&gt;
&lt;p&gt;Here&amp;rsquo;s an example hitting a paginated HTTP endpoint and yielding the data.
Errors also yield back to the consumer stop execution.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;encoding/json&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;fmt&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;iter&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;net/http&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Pokemon&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`json:&amp;#34;name&amp;#34;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`json:&amp;#34;url&amp;#34;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;APIResponse&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Count&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`json:&amp;#34;count&amp;#34;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Next&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`json:&amp;#34;next&amp;#34;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Previous&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`json:&amp;#34;previous&amp;#34;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Results&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="nx"&gt;Pokemon&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`json:&amp;#34;results&amp;#34;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Seq2&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;Pokemon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;yield&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Pokemon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;APIResponse&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;https://pokeapi.co/api/v2/pokemon?limit=%d&amp;amp;offset=0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;yield&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Pokemon&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewDecoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;yield&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Pokemon&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pokemon&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Results&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;yield&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pokemon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Next&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Next&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pokemon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// stop anytime&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Name:&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pokemon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;URL:&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pokemon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="see-also"&gt;See also&lt;/h2&gt;
&lt;p&gt;There&amp;rsquo;s plenty more features from the &lt;code&gt;rangefunc&lt;/code&gt; experiment mentioned in the
below links. Check them out!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Go specs/proposals:
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://tip.golang.org/wiki/RangefuncExperiment"&gt;https://tip.golang.org/wiki/RangefuncExperiment&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/golang/go/issues/61897"&gt;https://github.com/golang/go/issues/61897&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/golang/go/issues/61897"&gt;https://github.com/golang/go/issues/61897&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Blogs:
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://medium.com/eureka-engineering/a-look-at-iterators-in-go-f8e86062937c"&gt;https://medium.com/eureka-engineering/a-look-at-iterators-in-go-f8e86062937c&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bitfieldconsulting.com/golang/iterators"&gt;https://bitfieldconsulting.com/golang/iterators&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.perfects.engineering/go_range_over_funcs"&gt;https://blog.perfects.engineering/go_range_over_funcs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://makubob.medium.com/exploring-the-upcoming-go-generators-344b2fb98ff9"&gt;https://makubob.medium.com/exploring-the-upcoming-go-generators-344b2fb98ff9&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>