<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
    <channel>
      <title>Alex Rosenfeld</title>
      <link>https://blog.arsfeld.dev</link>
      <description>Technical blog about NixOS configurations, self-hosting, and infrastructure automation</description>
      <generator>Zola</generator>
      <language>en</language>
      <atom:link href="https://blog.arsfeld.dev/rss.xml" rel="self" type="application/rss+xml"/>
      <lastBuildDate>Tue, 16 Sep 2025 00:00:00 +0000</lastBuildDate>
      <item>
          <title>Reel Rewrite: How Relm4 Saved My Media Streaming Client</title>
          <pubDate>Tue, 16 Sep 2025 00:00:00 +0000</pubDate>
          <author>Alex Rosenfeld</author>
          <link>https://blog.arsfeld.dev/posts/2025/09/16/reel-rewrite-architecture-journey/</link>
          <guid>https://blog.arsfeld.dev/posts/2025/09/16/reel-rewrite-architecture-journey/</guid>
          <description xml:base="https://blog.arsfeld.dev/posts/2025/09/16/reel-rewrite-architecture-journey/">&lt;p&gt;A few months ago, I &lt;a href=&quot;https:&#x2F;&#x2F;blog.arsfeld.dev&#x2F;posts&#x2F;2025&#x2F;08&#x2F;25&#x2F;gnome-reel-native-media-streaming&#x2F;&quot;&gt;introduced Reel&lt;&#x2F;a&gt;, my native GTK4 media streaming client for GNOME. I was excited about the progress and using it daily. But behind the scenes, I was fighting a losing battle against an architecture that was fundamentally broken. This is the story of how I nearly gave up, discovered Relm4, and rebuilt everything from the ground up.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tl-dr&quot;&gt;TL;DR&lt;&#x2F;h2&gt;
&lt;p&gt;After months of battling cascading bugs and an unmaintainable state machine, I rewrote Reel using &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;relm4.org&#x2F;&quot;&gt;Relm4&lt;&#x2F;a&gt;. The new architecture uses proper reactive components, message passing, and async workers. The migration is about 85% complete, and the result? Bugs that haunted me for weeks disappeared overnight, and adding features no longer breaks existing functionality. It’s been a journey of three false starts, but the payoff has been worth every refactored line.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;when-everything-is-connected-to-everything&quot;&gt;When Everything Is Connected to Everything&lt;&#x2F;h2&gt;
&lt;p&gt;The original Reel codebase had become what I call a “spaghetti state machine.” Every component talked to every other component. Fix authentication? Suddenly sync breaks in mysterious ways. Update the cache? The UI starts behaving erratically. Add a new feature? Watch three unrelated things break.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;mermaid&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;graph TD&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    UI[UI Components] --&amp;gt;|Direct Access| Cache[Cache&#x2F;AppState]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    UI --&amp;gt;|Mutates| Services[Services]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Services --&amp;gt;|Updates| Cache&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Services --&amp;gt;|Calls| UI&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Auth[Auth Manager] --&amp;gt;|Modifies| Cache&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Auth --&amp;gt;|Triggers| Services&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Sync[Sync Manager] --&amp;gt;|Writes to| Cache&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Sync --&amp;gt;|Updates| UI&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Player[Player] --&amp;gt;|Reads| Cache&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Player --&amp;gt;|Notifies| UI&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Cache --&amp;gt;|Controls| Everything[Everything Else]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    style Cache fill:#ff6b6b,stroke:#c92a2a&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    style UI fill:#ffd43b,stroke:#fab005&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    style Services fill:#ffd43b,stroke:#fab005&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;em&gt;Figure 1: The old architecture - everything connected to everything&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Here’s what the codebase structure looked like—notice how services were deeply entangled:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;src&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── services&#x2F;           # The problematic service layer&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   ├── cache.rs       # God object managing everything&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   ├── auth.rs        # Directly manipulates cache&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   ├── sync.rs        # Updates UI through cache&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   └── player.rs      # Stateful, tightly coupled&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── ui&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   ├── windows&#x2F;       # GTK windows with business logic&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   ├── widgets&#x2F;       # Widgets directly calling services&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   └── state.rs       # Global mutable state&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;└── models&#x2F;            # Data structures mixed with logic&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The problems started from day one with fundamental architectural mistakes:&lt;&#x2F;p&gt;
&lt;h3 id=&quot;wrong-abstractions&quot;&gt;Wrong Abstractions&lt;&#x2F;h3&gt;
&lt;p&gt;I initially designed around a single backend to Plex. Simple, right? Except users have multiple Plex servers on one account. And then Jellyfin support meant multiple account types. The entire foundation was wrong, and every feature built on top inherited these flaws.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-cache-that-ate-everything&quot;&gt;The Cache That Ate Everything&lt;&#x2F;h3&gt;
&lt;p&gt;What started as a simple key-value cache for performance became the backbone of the entire data service. It had to know about every data type in the application—movies, shows, episodes, servers, users, settings. The cache became an accidental god object, orchestrating the entire application state.&lt;&#x2F;p&gt;
&lt;p&gt;Here’s what the old AppState looked like—notice how everything is mutable and interconnected:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;&#x2F;&#x2F; Before: src&#x2F;core&#x2F;state.rs - The god object that ruled them all&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;pub struct&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; AppState&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;    pub&lt;&#x2F;span&gt;&lt;span&gt; auth_manager&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; Arc&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;AuthManager&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;    pub&lt;&#x2F;span&gt;&lt;span&gt; source_coordinator&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; Arc&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;SourceCoordinator&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;    pub&lt;&#x2F;span&gt;&lt;span&gt; current_user&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; Arc&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;RwLock&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;Option&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;User&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&amp;gt;&amp;gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;    pub&lt;&#x2F;span&gt;&lt;span&gt; current_library&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; Arc&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;RwLock&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;Option&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;Library&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&amp;gt;&amp;gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;    pub&lt;&#x2F;span&gt;&lt;span&gt; libraries&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; Arc&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;RwLock&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;HashMap&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;String&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; Vec&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;Library&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;    pub&lt;&#x2F;span&gt;&lt;span&gt; library_items&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; Arc&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;RwLock&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;HashMap&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;String&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; Vec&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;MediaItem&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;    pub&lt;&#x2F;span&gt;&lt;span&gt; data_service&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; Arc&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;DataService&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;    pub&lt;&#x2F;span&gt;&lt;span&gt; sync_manager&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; Arc&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;SyncManager&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;    pub&lt;&#x2F;span&gt;&lt;span&gt; playback_state&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; Arc&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;RwLock&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;PlaybackState&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&amp;gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;    pub&lt;&#x2F;span&gt;&lt;span&gt; config&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; Arc&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;RwLock&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;Config&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&amp;gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;    pub&lt;&#x2F;span&gt;&lt;span&gt; database&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; Arc&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;Database&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;    pub&lt;&#x2F;span&gt;&lt;span&gt; db_connection&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; DatabaseConnection&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;    pub&lt;&#x2F;span&gt;&lt;span&gt; event_bus&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; Arc&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;EventBus&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;&#x2F;&#x2F; UI components directly mutated this shared state&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;impl&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; AppState&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;    pub async fn&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt; set_library&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;font-style: italic;&quot;&gt;self&lt;&#x2F;span&gt;&lt;span&gt;, library&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; Library&lt;&#x2F;span&gt;&lt;span&gt;) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;        let mut&lt;&#x2F;span&gt;&lt;span&gt; current_library&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;font-style: italic;&quot;&gt; self&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span&gt;current_library&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;write&lt;&#x2F;span&gt;&lt;span&gt;()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.await&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;        *&lt;&#x2F;span&gt;&lt;span&gt;current_library&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; Some&lt;&#x2F;span&gt;&lt;span&gt;(library);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;    pub async fn&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt; sync_all_backends&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;font-style: italic;&quot;&gt;self&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; -&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; Result&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;Vec&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;SyncResult&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&amp;gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;        &#x2F;&#x2F; Direct coordination between services&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;        let&lt;&#x2F;span&gt;&lt;span&gt; all_backends&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;font-style: italic;&quot;&gt; self&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span&gt;source_coordinator&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;get_all_backends&lt;&#x2F;span&gt;&lt;span&gt;()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.await&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;        &#x2F;&#x2F; ... sync logic mixed with state management ...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Every bug fix made it worse. Every feature made it more entangled.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-false-hope-of-home-grown-reactivity&quot;&gt;The False Hope of Home-Grown Reactivity&lt;&#x2F;h2&gt;
&lt;p&gt;Desperate for a solution, I introduced reactivity with an event bus and a property system. The idea was sound: components would react to state changes instead of directly manipulating each other. For a brief, glorious moment, it felt like salvation.&lt;&#x2F;p&gt;
&lt;p&gt;I went all in, converting code piece by piece to this new system. UI components would update through ViewModels. State changes would propagate through events. It was going to fix everything.&lt;&#x2F;p&gt;
&lt;p&gt;It didn’t.&lt;&#x2F;p&gt;
&lt;p&gt;What I ended up with was worse—a hybrid monster with half the code using the old direct manipulation and half using my buggy home-grown property system. The reactive parts had their own bugs. The boundaries between old and new code were unclear. I’d created two problems instead of solving one.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-breaking-point&quot;&gt;The Breaking Point&lt;&#x2F;h2&gt;
&lt;p&gt;The moment I knew something had to change was when I broke the video player—again. This was supposed to be the stable part of the application, carefully designed and tested. But a seemingly unrelated change to the authentication flow cascaded through the system and broke video playback in a way I couldn’t even understand.&lt;&#x2F;p&gt;
&lt;p&gt;I was tired. Really tired.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;enter-relm4&quot;&gt;Enter Relm4&lt;&#x2F;h2&gt;
&lt;p&gt;That’s when I discovered &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;relm4.org&#x2F;&quot;&gt;Relm4&lt;&#x2F;a&gt;. It’s a reactive GTK4 framework for Rust that provides proper component architecture, message passing, and state management. Reading through the documentation felt like finding exactly what I’d been trying to build myself, but actually working.&lt;&#x2F;p&gt;
&lt;p&gt;But I hesitated. A lot.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;two-false-starts&quot;&gt;Two False Starts&lt;&#x2F;h3&gt;
&lt;p&gt;Twice I started converting code to Relm4. Twice I gave up. The problem wasn’t Relm4—it was that I was trying to retrofit it onto a broken architecture. I was putting lipstick on a pig.&lt;&#x2F;p&gt;
&lt;p&gt;The service layer I’d built was inherently stateful and completely incompatible with Relm4’s functional approach. The cache system fought against proper component isolation. Every conversion attempt hit the same walls because I was trying to preserve too much of the old code.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;going-all-in&quot;&gt;Going All In&lt;&#x2F;h2&gt;
&lt;p&gt;One particularly frustrating evening, after breaking the video player yet again while trying to add a minor feature, something snapped. I decided to burn it all down and rebuild from scratch using Relm4 properly.&lt;&#x2F;p&gt;
&lt;p&gt;This meant:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Throwing away the entire service layer&lt;&#x2F;li&gt;
&lt;li&gt;Deleting the god-object cache&lt;&#x2F;li&gt;
&lt;li&gt;Redesigning the data flow from first principles&lt;&#x2F;li&gt;
&lt;li&gt;Starting with the correct abstractions this time&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The actual rewrite happened remarkably fast. Over the course of just a few days, I implemented the core components: the MessageBroker for sync integration, a complete UI overhaul with modern Adwaita design, an immersive player with proper window chrome management, and thread-safe player communication using channels.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-new-architecture&quot;&gt;The New Architecture&lt;&#x2F;h2&gt;
&lt;p&gt;The rewrite brought clarity. With Relm4, I could finally map concepts to architecture the right way.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;mermaid&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;graph TD&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    subgraph &amp;quot;UI Layer (Relm4 Components)&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        MW[MainWindow]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        HP[HomePage]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        PP[PlayerPage]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        LP[LibraryPage]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    end&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    subgraph &amp;quot;Message System&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        MB[MessageBroker]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    end&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    subgraph &amp;quot;Business Logic&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        PC[PlayerController]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        SM[SyncManager]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        AM[AuthManager]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    end&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    subgraph &amp;quot;Data Layer&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        DB[(Database)]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        Repo[Repositories]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    end&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    MW --&amp;gt;|Message| MB&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    HP --&amp;gt;|Subscribe| MB&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    PP --&amp;gt;|Subscribe| MB&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    LP --&amp;gt;|Subscribe| MB&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    MB --&amp;gt;|Broadcast| HP&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    MB --&amp;gt;|Broadcast| PP&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    MB --&amp;gt;|Broadcast| LP&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    PP --&amp;gt;|Commands| PC&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    PC --&amp;gt;|Response| PP&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    SM --&amp;gt;|Notify| MB&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    AM --&amp;gt;|Notify| MB&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Repo --&amp;gt;|Query| DB&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    SM --&amp;gt;|Use| Repo&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    style MB fill:#51cf66,stroke:#37b24d&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    style MW fill:#74c0fc,stroke:#339af0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    style HP fill:#74c0fc,stroke:#339af0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    style PP fill:#74c0fc,stroke:#339af0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    style LP fill:#74c0fc,stroke:#339af0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;em&gt;Figure 2: The new architecture - clean message-based communication&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Here’s the new structure—notice the clean separation of concerns:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;src&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── backends&#x2F;            # Backend implementations (Plex, Jellyfin)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   ├── traits.rs       # Clean interface definitions&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   ├── plex&#x2F;           # Plex-specific logic only&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   └── jellyfin&#x2F;       # Jellyfin-specific logic only&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── platforms&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   └── relm4&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│       └── components&#x2F; # Isolated UI components&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│           ├── main_window.rs&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│           ├── pages&#x2F;  # Each page is a component&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│           └── shared&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│               ├── broker.rs    # Central message passing&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│               ├── messages.rs  # Message types&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│               └── commands.rs  # Async workers&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── player&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   ├── controller.rs   # Thread-safe controller&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   └── factory.rs      # Player abstraction&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;└── db&#x2F;                  # Clean data layer&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ├── entities&#x2F;       # Database models&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    └── repository&#x2F;     # Data access patterns&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;async-components&quot;&gt;Async Components&lt;&#x2F;h3&gt;
&lt;p&gt;Each major UI element is now a proper component with its own state, update logic, and message handling. Components don’t know about each other—they communicate through messages. Worker components handle heavy tasks like image loading and search operations without blocking the UI.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;message-broker&quot;&gt;Message Broker&lt;&#x2F;h3&gt;
&lt;p&gt;Instead of direct component communication, there’s now a central message broker. Components publish events; interested parties subscribe. Clean boundaries, clear data flow. The MessageBroker handles sync integration and eliminates race conditions that plagued the old architecture.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;&#x2F;&#x2F; After: src&#x2F;platforms&#x2F;relm4&#x2F;components&#x2F;shared&#x2F;broker.rs&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;#[derive(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;Debug&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; Clone&lt;&#x2F;span&gt;&lt;span&gt;)]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;pub enum&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; BrokerMessage&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;    Navigation&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;NavigationMessage&lt;&#x2F;span&gt;&lt;span&gt;),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;    Data&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;DataMessage&lt;&#x2F;span&gt;&lt;span&gt;),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;    Playback&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;PlaybackMessage&lt;&#x2F;span&gt;&lt;span&gt;),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;    Source&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;SourceMessage&lt;&#x2F;span&gt;&lt;span&gt;),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;&#x2F;&#x2F; Components subscribe to specific messages&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;impl&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; AsyncComponentParts&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; for&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; HomePage&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;    fn&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt; init&lt;&#x2F;span&gt;&lt;span&gt;() {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;        &#x2F;&#x2F; Subscribe to broker messages&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;        let&lt;&#x2F;span&gt;&lt;span&gt; broker_sender&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; sender&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;clone&lt;&#x2F;span&gt;&lt;span&gt;();&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        relm4&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;spawn&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;async move&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt;            BROKER&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;subscribe&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;&amp;quot;HomePage&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;to_string&lt;&#x2F;span&gt;&lt;span&gt;(), broker_sender)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.await&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        });&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;    fn&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt; update&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;&amp;amp;mut&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;font-style: italic;&quot;&gt; self&lt;&#x2F;span&gt;&lt;span&gt;, msg&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; Input&lt;&#x2F;span&gt;&lt;span&gt;) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;        match&lt;&#x2F;span&gt;&lt;span&gt; msg {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;            Input&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;BrokerMsg&lt;&#x2F;span&gt;&lt;span&gt;(BrokerMessage&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;Data&lt;&#x2F;span&gt;&lt;span&gt;(DataMessage&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;LibraryUpdated&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; ..&lt;&#x2F;span&gt;&lt;span&gt; }))&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;                &#x2F;&#x2F; React to library updates without direct coupling&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #BD93F9;font-style: italic;&quot;&gt;                self&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;refresh_content&lt;&#x2F;span&gt;&lt;span&gt;();&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            _&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; {}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;thread-safe-player-controller&quot;&gt;Thread-Safe Player Controller&lt;&#x2F;h3&gt;
&lt;p&gt;The video player now uses a channel-based PlayerController with PlayerHandle and PlayerCommand patterns. This eliminates all the threading issues that made the player so fragile before. The immersive mode properly manages window chrome and cursor hiding.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;mermaid&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;sequenceDiagram&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    participant UI as UI Component&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    participant H as PlayerHandle&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    participant C as Controller Thread&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    participant P as Player Backend&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    UI-&amp;gt;&amp;gt;H: load_media(url)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    H-&amp;gt;&amp;gt;H: Create oneshot channel&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    H-&amp;gt;&amp;gt;C: Send PlayerCommand::LoadMedia&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    C-&amp;gt;&amp;gt;P: Initialize player&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    P--&amp;gt;&amp;gt;C: Success&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    C--&amp;gt;&amp;gt;H: Send Result via channel&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    H--&amp;gt;&amp;gt;UI: Return Result&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Note over UI,P: All communication is async and thread-safe&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;em&gt;Figure 3: Thread-safe player communication flow&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;&#x2F;&#x2F; After: src&#x2F;player&#x2F;controller.rs - Thread-safe player communication&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;#[derive(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;Debug&lt;&#x2F;span&gt;&lt;span&gt;)]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;pub enum&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; PlayerCommand&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;    LoadMedia&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        url&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; String&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        respond_to&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span&gt; oneshot&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;Sender&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;Result&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;()&amp;gt;&amp;gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;    Play&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        respond_to&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span&gt; oneshot&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;Sender&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;Result&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;()&amp;gt;&amp;gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;    Seek&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        position&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; Duration&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        respond_to&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span&gt; oneshot&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;Sender&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;Result&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;()&amp;gt;&amp;gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;    &#x2F;&#x2F; ... more commands with response channels&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;&#x2F;&#x2F; Clean handle for UI components&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;pub struct&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; PlayerHandle&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    sender&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span&gt; mpsc&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;Sender&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;PlayerCommand&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;impl&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; PlayerHandle&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;    pub async fn&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt; load_media&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;&amp;amp;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;font-style: italic;&quot;&gt;self&lt;&#x2F;span&gt;&lt;span&gt;, url&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; String&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; -&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; Result&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;()&amp;gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;        let&lt;&#x2F;span&gt;&lt;span&gt; (tx, rx)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; oneshot&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;channel&lt;&#x2F;span&gt;&lt;span&gt;();&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #BD93F9;font-style: italic;&quot;&gt;        self&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span&gt;sender&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;            .&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;send&lt;&#x2F;span&gt;&lt;span&gt;(PlayerCommand&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;LoadMedia&lt;&#x2F;span&gt;&lt;span&gt; { url, respond_to&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span&gt; tx })&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;            .await?&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        rx&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.await?&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;    &#x2F;&#x2F; No shared mutable state, just message passing&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Compare this to the old player design where UI components directly manipulated player state through Arc&amp;lt;RwLock&amp;lt;&amp;gt;&amp;gt; patterns, leading to deadlocks and race conditions.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;proper-state-management&quot;&gt;Proper State Management&lt;&#x2F;h3&gt;
&lt;p&gt;State lives where it belongs. UI state in components, server data in dedicated stores, settings in a configuration manager. The reactive property system provides observable data without the complexity of the home-grown solution.&lt;&#x2F;p&gt;
&lt;p&gt;Here’s a complete Relm4 component showing the new pattern:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;&#x2F;&#x2F; After: Clean component with isolated state and message passing&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;pub struct&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; MainWindow&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    db&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; DatabaseConnection&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    sidebar&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; Controller&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;Sidebar&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    home_page&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; AsyncController&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;HomePage&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    player_page&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; Option&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;AsyncController&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;PlayerPage&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&amp;gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    navigation_view&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span&gt; adw&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;NavigationView&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;#[derive(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;Debug&lt;&#x2F;span&gt;&lt;span&gt;)]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;pub enum&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; MainWindowInput&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;    Navigate&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;NavigationTarget&lt;&#x2F;span&gt;&lt;span&gt;),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;    BrokerMsg&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;BrokerMessage&lt;&#x2F;span&gt;&lt;span&gt;),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;    ShowPlayerPage&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;MediaItemId&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; PlaylistContext&lt;&#x2F;span&gt;&lt;span&gt;),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;    UpdateWindowChrome&lt;&#x2F;span&gt;&lt;span&gt; { fullscreen&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; bool&lt;&#x2F;span&gt;&lt;span&gt; },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;impl&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; AsyncComponent&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; for&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; MainWindow&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;    type&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; Input&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; MainWindowInput&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;    type&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; Output&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; ();&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;    async fn&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt; update&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;&amp;amp;mut&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;font-style: italic;&quot;&gt; self&lt;&#x2F;span&gt;&lt;span&gt;, msg&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;font-style: italic;&quot;&gt; Self&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;Input&lt;&#x2F;span&gt;&lt;span&gt;, sender&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; AsyncComponentSender&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;font-style: italic;&quot;&gt;Self&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;        match&lt;&#x2F;span&gt;&lt;span&gt; msg {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;            MainWindowInput&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;Navigate&lt;&#x2F;span&gt;&lt;span&gt;(target)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;                &#x2F;&#x2F; Clean navigation without side effects&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #BD93F9;font-style: italic;&quot;&gt;                self&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;navigate_to&lt;&#x2F;span&gt;&lt;span&gt;(target)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.await&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;            MainWindowInput&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;ShowPlayerPage&lt;&#x2F;span&gt;&lt;span&gt;(media_id, context)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;                &#x2F;&#x2F; Create isolated player component&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;                let&lt;&#x2F;span&gt;&lt;span&gt; player&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; PlayerPage&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;builder&lt;&#x2F;span&gt;&lt;span&gt;()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;                    .&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;launch&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;PlayerPageInit&lt;&#x2F;span&gt;&lt;span&gt; { media_id, context })&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;                    .&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;forward&lt;&#x2F;span&gt;&lt;span&gt;(sender&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;input_sender&lt;&#x2F;span&gt;&lt;span&gt;(),&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; |&lt;&#x2F;span&gt;&lt;span&gt;msg&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;|&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                        MainWindowInput&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;BrokerMsg&lt;&#x2F;span&gt;&lt;span&gt;(msg&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;into&lt;&#x2F;span&gt;&lt;span&gt;())&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                    });&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #BD93F9;font-style: italic;&quot;&gt;                self&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span&gt;player_page &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt; Some&lt;&#x2F;span&gt;&lt;span&gt;(player);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;            MainWindowInput&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;font-style: italic;&quot;&gt;UpdateWindowChrome&lt;&#x2F;span&gt;&lt;span&gt; { fullscreen }&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;                &#x2F;&#x2F; UI updates are explicit and traceable&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #BD93F9;font-style: italic;&quot;&gt;                self&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;set_chrome_visibility&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;!&lt;&#x2F;span&gt;&lt;span&gt;fullscreen);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            _&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; {}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Compare this to the old GTK approach with its 150+ lines of wrapper enums and direct state manipulation. Each Relm4 component is self-contained, testable, and communicates only through messages.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-payoff&quot;&gt;The Payoff&lt;&#x2F;h2&gt;
&lt;p&gt;The difference has been night and day. Let me show you the numbers:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;mermaid&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;graph LR&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    subgraph &amp;quot;Before&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        B1[15+ Critical Bugs]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        B2[3-4 Days per Feature]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        B3[~50% Time Debugging]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        B4[UI Freezes Common]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    end&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    subgraph &amp;quot;After&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        A1[2 Known Issues]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        A2[&amp;lt;1 Day per Feature]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        A3[~10% Time Debugging]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        A4[Zero UI Freezes]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    end&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    B1 -.-&amp;gt;|🎉| A1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    B2 -.-&amp;gt;|🚀| A2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    B3 -.-&amp;gt;|✨| A3&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    B4 -.-&amp;gt;|⚡| A4&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    style A1 fill:#51cf66,stroke:#37b24d&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    style A2 fill:#51cf66,stroke:#37b24d&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    style A3 fill:#51cf66,stroke:#37b24d&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    style A4 fill:#51cf66,stroke:#37b24d&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    style B1 fill:#ff6b6b,stroke:#c92a2a&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    style B2 fill:#ff6b6b,stroke:#c92a2a&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    style B3 fill:#ff6b6b,stroke:#c92a2a&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    style B4 fill:#ff6b6b,stroke:#c92a2a&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;em&gt;Figure 4: The dramatic improvement in development metrics&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;concrete-improvements&quot;&gt;Concrete Improvements&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Bugs disappeared&lt;&#x2F;strong&gt;: Issues that had plagued the project for months just vanished. Proper component isolation meant fixes stayed fixed.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Features don’t break things&lt;&#x2F;strong&gt;: Adding new functionality no longer feels like playing Jenga. Components are isolated; changes don’t cascade.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Code is maintainable&lt;&#x2F;strong&gt;: I can now look at a component and understand it without needing to trace through five other files.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Async actually works&lt;&#x2F;strong&gt;: Proper async components mean no more UI freezes during sync operations.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;what-s-left&quot;&gt;What’s Left&lt;&#x2F;h2&gt;
&lt;p&gt;The migration is about 85% complete, with core functionality working but some polish needed:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Working:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Plex and Jellyfin server connections&lt;&#x2F;li&gt;
&lt;li&gt;Media browsing and playback&lt;&#x2F;li&gt;
&lt;li&gt;The new Adwaita UI with theme support&lt;&#x2F;li&gt;
&lt;li&gt;Thread-safe player with immersive mode&lt;&#x2F;li&gt;
&lt;li&gt;Reactive components and message passing&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;Still In Progress:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Jellyfin sync completion&lt;&#x2F;li&gt;
&lt;li&gt;Seek functionality refinement&lt;&#x2F;li&gt;
&lt;li&gt;Keyboard shortcuts&lt;&#x2F;li&gt;
&lt;li&gt;Some UI polish and missing features&lt;&#x2F;li&gt;
&lt;li&gt;Library view optimizations&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;But here’s the crucial difference: these are implementation tasks, not architectural battles. The foundation is solid. With 70+ tasks tracked in the backlog, I can methodically work through features knowing each addition won’t destabilize the entire system.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;lessons-learned&quot;&gt;Lessons Learned&lt;&#x2F;h2&gt;
&lt;p&gt;This rewrite taught me some hard lessons:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Wrong abstractions compound&lt;&#x2F;strong&gt;: Every feature built on a bad foundation makes things exponentially worse.&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Home-grown frameworks are rarely the answer&lt;&#x2F;strong&gt;: Unless you’re solving a genuinely unique problem, use battle-tested solutions.&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Sometimes you need to start over&lt;&#x2F;strong&gt;: I wasted months trying to fix unfixable architecture. The rewrite took weeks and solved everything.&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Functional components work&lt;&#x2F;strong&gt;: The Relm4 approach of isolated components with message passing isn’t just theory—it actually makes complex UIs manageable.&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;looking-forward&quot;&gt;Looking Forward&lt;&#x2F;h2&gt;
&lt;p&gt;With a solid foundation, I can finally focus on what matters: features and user experience. The roadmap from &lt;a href=&quot;https:&#x2F;&#x2F;blog.arsfeld.dev&#x2F;posts&#x2F;2025&#x2F;08&#x2F;25&#x2F;gnome-reel-native-media-streaming&#x2F;&quot;&gt;the original post&lt;&#x2F;a&gt; is still valid, but now it’s actually achievable.&lt;&#x2F;p&gt;
&lt;p&gt;The next few releases will bring:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Completed Jellyfin support&lt;&#x2F;li&gt;
&lt;li&gt;Search and filtering&lt;&#x2F;li&gt;
&lt;li&gt;Local file playback&lt;&#x2F;li&gt;
&lt;li&gt;Music library support&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;But more importantly, I can now add these features with confidence that they won’t break everything else.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;for-fellow-developers&quot;&gt;For Fellow Developers&lt;&#x2F;h2&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;mermaid&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;mindmap&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  root((Architecture Rewrite))&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Signs You Need It&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      Every fix breaks something else&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      Features take exponentially longer&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      You dread touching the code&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      Simple changes require complex workarounds&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    The Right Approach&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      Accept the current design is broken&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      Choose battle-tested frameworks&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      Start completely fresh&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      Focus on architecture first&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Relm4 Benefits&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      True component isolation&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      Message-based communication&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      Async that actually works&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      Testable by design&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    The Payoff&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      10x faster feature development&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      Bugs stay fixed&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      Code is enjoyable again&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      Confidence in changes&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;em&gt;Figure 6: Should you rewrite? A decision framework&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;If you’re fighting your architecture daily, if every bug fix creates two new problems, if you dread adding features because of what might break—consider a rewrite. Not a refactor, not a gradual migration, but a ground-up rebuild with the right tools.&lt;&#x2F;p&gt;
&lt;p&gt;Yes, it’s scary. Yes, you’ll lose some work. But the alternative is death by a thousand cuts, slowly losing the will to work on your project as it becomes increasingly unmaintainable.&lt;&#x2F;p&gt;
&lt;p&gt;For GTK4 and Rust developers specifically: give &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;relm4.org&#x2F;&quot;&gt;Relm4&lt;&#x2F;a&gt; a serious look. It’s not just another framework—it’s a fundamentally better way to build reactive UIs.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;&lt;em&gt;Reel is available on &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;reel&quot;&gt;GitHub&lt;&#x2F;a&gt;. The Relm4 migration is 85% complete and already more stable than the original ever was. Downloads are available as AppImage, .deb, and .rpm packages. Built with Rust, GTK4, Relm4, libadwaita, and hard-won lessons about software architecture.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Reel: Bringing Native Media Streaming to the GNOME Desktop</title>
          <pubDate>Mon, 25 Aug 2025 00:00:00 +0000</pubDate>
          <author>Alex Rosenfeld</author>
          <link>https://blog.arsfeld.dev/posts/2025/08/25/gnome-reel-native-media-streaming/</link>
          <guid>https://blog.arsfeld.dev/posts/2025/08/25/gnome-reel-native-media-streaming/</guid>
          <description xml:base="https://blog.arsfeld.dev/posts/2025/08/25/gnome-reel-native-media-streaming/">&lt;div align=&quot;center&quot;&gt;
  &lt;img src=&quot;https:&#x2F;&#x2F;raw.githubusercontent.com&#x2F;arsfeld&#x2F;gnome-reel&#x2F;refs&#x2F;heads&#x2F;master&#x2F;logo.svg&quot; alt=&quot;Reel Logo&quot; width=&quot;400&quot; &#x2F;&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;It’s always been a dream of mine to have a proper, native media streaming client for GNOME. Not a web wrapper, not a half-maintained GTK3 app from years ago, but a real, modern, performant application that feels like it belongs on my desktop. After years of waiting and watching the landscape, I decided to build it myself. Enter &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;gnome-reel&quot;&gt;Reel&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tl-dr&quot;&gt;TL;DR&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;gnome-reel&quot;&gt;Reel&lt;&#x2F;a&gt; is a native GTK4 media streaming client for GNOME that supports Plex and Jellyfin. Built with Rust, it offers multiple video backends (GStreamer and MPV), handles large libraries, and feels like a proper desktop app—not another web wrapper. Still in active development but already my daily driver for watching movies and shows.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-build-this&quot;&gt;Why Build This?&lt;&#x2F;h2&gt;
&lt;p&gt;Let’s be honest about where we stand today with media streaming on the Linux desktop. It’s… not great.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Girens&lt;&#x2F;strong&gt;: Was a GTK-based Plex client that showed what was possible with native integration. While I haven’t tried it in years, at the time the design felt dated and it didn’t quite hit the mark for what I was looking for in a modern media client.&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Official Plex App&lt;&#x2F;strong&gt;: Exists for Linux and works fine, though it’s essentially an Electron wrapper around their web app. It gets the job done but doesn’t feel native to the desktop.&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Infuse&lt;&#x2F;strong&gt;: On macOS, I use &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;firecore.com&#x2F;infuse&quot;&gt;Infuse&lt;&#x2F;a&gt; daily—it’s a fantastic native media client that shows what’s possible with proper desktop integration. It’s been my inspiration for what a Linux media app could be.&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Well, now we’re building it.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;raw.githubusercontent.com&#x2F;arsfeld&#x2F;gnome-reel&#x2F;refs&#x2F;heads&#x2F;master&#x2F;screenshots&#x2F;main-window.png&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;raw.githubusercontent.com&#x2F;arsfeld&#x2F;gnome-reel&#x2F;refs&#x2F;heads&#x2F;master&#x2F;screenshots&#x2F;main-window.png&quot; alt=&quot;Reel Main Window&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;building-reel-the-technical-journey&quot;&gt;Building Reel: The Technical Journey&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;why-rust&quot;&gt;Why Rust?&lt;&#x2F;h3&gt;
&lt;p&gt;Why not? 🦀&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-architecture-flexibility-first&quot;&gt;The Architecture: Flexibility First&lt;&#x2F;h3&gt;
&lt;p&gt;One of my key design decisions was to build a multi-backend architecture from day one. I didn’t want to create “just another Plex client”—I wanted to build a media streaming platform for GNOME that could adapt to whatever service users prefer. Each service (Plex, Jellyfin, and eventually local files) implements a common interface, making the UI completely agnostic about where the media comes from.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-player-backend-saga&quot;&gt;The Player Backend Saga&lt;&#x2F;h3&gt;
&lt;p&gt;Here’s where things got interesting. Video playback on Linux is… complicated.&lt;&#x2F;p&gt;
&lt;p&gt;GStreamer is the obvious choice for GTK applications—it’s lighter on resources, well-integrated with the GNOME stack, and powerful. However, I hit a frustrating wall: subtitle rendering. On my machine, GStreamer has colorspace issues with certain subtitle formats that make them nearly unreadable.&lt;&#x2F;p&gt;
&lt;p&gt;As a pragmatic solution, I added support for multiple player backends. Now Reel ships with both:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GStreamer&lt;&#x2F;strong&gt;: The preferred backend—lighter and better integrated with GNOME&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;MPV&lt;&#x2F;strong&gt;: A fallback option that handles subtitles correctly while we work on fixing the GStreamer issue&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Once the subtitle bug is resolved, MPV will purely be a fallback option, but for now this flexibility ensures everyone can enjoy their media regardless of subtitle formats.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;raw.githubusercontent.com&#x2F;arsfeld&#x2F;gnome-reel&#x2F;refs&#x2F;heads&#x2F;master&#x2F;screenshots&#x2F;player.png&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;raw.githubusercontent.com&#x2F;arsfeld&#x2F;gnome-reel&#x2F;refs&#x2F;heads&#x2F;master&#x2F;screenshots&#x2F;player.png&quot; alt=&quot;Player View&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-jellyfin-milestone&quot;&gt;The Jellyfin Milestone&lt;&#x2F;h2&gt;
&lt;p&gt;Just this week (v0.3.0), I reached a personal milestone: Jellyfin support. This wasn’t just about adding another backend—it was about proving the architecture works.&lt;&#x2F;p&gt;
&lt;p&gt;Jellyfin represents something important in the media server space: true open-source freedom. While Plex is great, having an open alternative that users can self-host without restrictions matters. The implementation came together surprisingly smoothly, validating the multi-backend design.&lt;&#x2F;p&gt;
&lt;p&gt;Now users can:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Connect to multiple servers (Plex and Jellyfin)&lt;&#x2F;li&gt;
&lt;li&gt;Switch between them seamlessly&lt;&#x2F;li&gt;
&lt;li&gt;Use the same intuitive interface regardless of the backend&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;raw.githubusercontent.com&#x2F;arsfeld&#x2F;gnome-reel&#x2F;refs&#x2F;heads&#x2F;master&#x2F;screenshots&#x2F;show-details.png&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;raw.githubusercontent.com&#x2F;arsfeld&#x2F;gnome-reel&#x2F;refs&#x2F;heads&#x2F;master&#x2F;screenshots&#x2F;show-details.png&quot; alt=&quot;Show Details&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;current-challenges-and-solutions&quot;&gt;Current Challenges and Solutions&lt;&#x2F;h2&gt;
&lt;p&gt;There are plenty of bugs still to squash, but I’m already using it as my daily driver to watch movies and shows. Here are the main areas I’m working on:&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-subtitle-situation&quot;&gt;The Subtitle Situation&lt;&#x2F;h3&gt;
&lt;p&gt;As mentioned, GStreamer subtitle rendering has some colorspace issues that I’m investigating. The MPV backend provides a workaround for now, but fixing the GStreamer issue properly is a priority—it’s the better integrated solution for GNOME.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;performance-optimization&quot;&gt;Performance Optimization&lt;&#x2F;h3&gt;
&lt;p&gt;Loading large libraries can still feel sluggish. I’m working on:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Better caching strategies with SQLite&lt;&#x2F;li&gt;
&lt;li&gt;Predictive prefetching for smoother browsing&lt;&#x2F;li&gt;
&lt;li&gt;Investigating why Jellyfin is hit particularly hard when loading libraries&lt;&#x2F;li&gt;
&lt;li&gt;Fixing MPV backend smoothness under system load (can show skipped frames occasionally)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;feature-parity&quot;&gt;Feature Parity&lt;&#x2F;h3&gt;
&lt;p&gt;Compared to official clients, we’re still missing:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Advanced search filters&lt;&#x2F;li&gt;
&lt;li&gt;Music library support (also on the roadmap)&lt;&#x2F;li&gt;
&lt;li&gt;Displaying cast and crew information&lt;&#x2F;li&gt;
&lt;li&gt;Marking episodes as watched&lt;&#x2F;li&gt;
&lt;li&gt;Downloading subtitles (you can select them in the player already)&lt;&#x2F;li&gt;
&lt;li&gt;Tons of small features that official clients have&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;the-road-ahead&quot;&gt;The Road Ahead&lt;&#x2F;h2&gt;
&lt;p&gt;Looking at my &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;gnome-reel&#x2F;blob&#x2F;master&#x2F;TASKS.md&quot;&gt;TASKS.md&lt;&#x2F;a&gt;, the focus is on polish and usability:&lt;&#x2F;p&gt;
&lt;h3 id=&quot;near-term-next-few-releases&quot;&gt;Near Term (Next Few Releases)&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Search functionality&lt;&#x2F;strong&gt;: Full-text search across all your media&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Filtering and sorting&lt;&#x2F;strong&gt;: Better ways to browse large libraries&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Performance optimizations&lt;&#x2F;strong&gt;: Faster loading and smoother scrolling&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Bug fixes and polish&lt;&#x2F;strong&gt;: Making the experience more reliable&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Meson build system&lt;&#x2F;strong&gt;: Adding support to fully integrate with the GNOME ecosystem, including translations&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;medium-term&quot;&gt;Medium Term&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Local files backend&lt;&#x2F;strong&gt;: For your personal collection&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Music support&lt;&#x2F;strong&gt;: Because media isn’t just video&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Flatpak distribution&lt;&#x2F;strong&gt;: Easy installation for everyone (&lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;flathub&#x2F;flathub&#x2F;pull&#x2F;6848&quot;&gt;PR in progress&lt;&#x2F;a&gt;)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;the-role-of-llms-in-building-this&quot;&gt;The Role of LLMs in Building This&lt;&#x2F;h2&gt;
&lt;p&gt;I need to be honest about something: without LLMs, I wouldn’t have gotten even close to this far with Reel. Whatever your opinion on AI tools, there’s no denying they’ve made side projects like this dramatically more feasible.&lt;&#x2F;p&gt;
&lt;p&gt;As a parent, my coding time is precious—usually just those quiet hours after the kids are asleep. LLMs have been invaluable for:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Getting the basic project up and running with Rust and GTK4&lt;&#x2F;li&gt;
&lt;li&gt;Debugging those cryptic GStreamer errors at 11 PM&lt;&#x2F;li&gt;
&lt;li&gt;Refactoring code became so much easier—we can try different approaches very quickly and discard anything that doesn’t work&lt;&#x2F;li&gt;
&lt;li&gt;The MPV backend was only possible because of AI—I wouldn’t have known where to start, and even once the code was in place, it took forever to get the flow of GLArea and MPV to work just right&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;More importantly, they’ve helped maintain momentum. When you’re exhausted after a full day and finally sit down to code, having an AI assistant to help push through blockers makes the difference between making progress and giving up for the night. It’s brought back some of the joy and motivation to work on passion projects, even when time and energy are limited.&lt;&#x2F;p&gt;
&lt;p&gt;One lesson learned is how to interact with the community when using AI. I made the pull request to Flathub using an LLM, and while I did read all the Flathub documentation myself and thought I had a good grasp of the process, the PR had a template that you could only see if you opened it through the web. Instead, I asked the AI to open it through the command-line and it wasn’t well received that a chatbot had written the PR. I understand the frustration—it’s hard to draw the line of what AI should and shouldn’t do.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;get-involved&quot;&gt;Get Involved&lt;&#x2F;h2&gt;
&lt;p&gt;Reel is still in active development, and I’d love your help:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Try it out&lt;&#x2F;strong&gt;: Install from &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;gnome-reel&quot;&gt;GitHub&lt;&#x2F;a&gt; and report your experience&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Report bugs&lt;&#x2F;strong&gt;: Every issue helps make it better&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Contribute code&lt;&#x2F;strong&gt;: Whether it’s features, fixes, or translations&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Spread the word&lt;&#x2F;strong&gt;: Let other Linux users know there’s a native option&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The project is 100% open source, built with Rust and GTK4, and designed to be hackable. Whether you want to add a new backend, improve the UI, or optimize performance, there’s room for your contributions.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;&lt;em&gt;Reel is available on &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;gnome-reel&quot;&gt;GitHub&lt;&#x2F;a&gt;. Currently supporting Plex and Jellyfin, with more backends on the way. Built with Rust, GTK4, and love for the Linux desktop.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Monitor Per-Client Network Usage with Custom Metrics</title>
          <pubDate>Tue, 22 Jul 2025 00:00:00 +0000</pubDate>
          <author>Alex Rosenfeld</author>
          <link>https://blog.arsfeld.dev/posts/2025/07/22/nixos-router-blog-post-3-monitoring/</link>
          <guid>https://blog.arsfeld.dev/posts/2025/07/22/nixos-router-blog-post-3-monitoring/</guid>
          <description xml:base="https://blog.arsfeld.dev/posts/2025/07/22/nixos-router-blog-post-3-monitoring/">&lt;p&gt;&lt;em&gt;Part 3 of the NixOS Router Series&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Ever wondered which device is hogging all your bandwidth? In this guide, we’ll set up comprehensive monitoring for your NixOS router that tracks exactly how much data each client uses in real-time. By the end, you’ll have beautiful dashboards showing per-device bandwidth usage, connection counts, and network trends.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-makes-this-special&quot;&gt;What Makes This Special?&lt;&#x2F;h2&gt;
&lt;p&gt;Most router monitoring solutions give you aggregate statistics - total bandwidth in and out. That’s useful, but it doesn’t tell you that your smart TV is downloading 50GB of updates at 3 AM, or that your teenager’s gaming PC is saturating your upload bandwidth.&lt;&#x2F;p&gt;
&lt;p&gt;Our custom &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;tree&#x2F;master&#x2F;packages&#x2F;network-metrics-exporter&quot;&gt;network-metrics-exporter&lt;&#x2F;a&gt; solves this by:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Tracking bandwidth per individual client IP&lt;&#x2F;li&gt;
&lt;li&gt;Maintaining persistent client names across reboots&lt;&#x2F;li&gt;
&lt;li&gt;Integrating directly with nftables for accurate counts&lt;&#x2F;li&gt;
&lt;li&gt;Exposing everything as Prometheus metrics&lt;&#x2F;li&gt;
&lt;li&gt;Zero performance impact on your routing&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;A working NixOS router (&lt;a href=&quot;&#x2F;posts&#x2F;nixos-router-getting-started&quot;&gt;from Part 1&lt;&#x2F;a&gt;)&lt;&#x2F;li&gt;
&lt;li&gt;Your router configuration in a flake (as introduced in &lt;a href=&quot;&#x2F;posts&#x2F;nixos-router-blog-post-2-testing&quot;&gt;Part 2&lt;&#x2F;a&gt;)&lt;&#x2F;li&gt;
&lt;li&gt;Basic familiarity with NixOS configuration&lt;&#x2F;li&gt;
&lt;li&gt;About 45 minutes&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;step-1-set-up-basic-monitoring-stack&quot;&gt;Step 1: Set Up Basic Monitoring Stack&lt;&#x2F;h2&gt;
&lt;p&gt;First, let’s get Prometheus and Grafana running on your router.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;enable-prometheus&quot;&gt;Enable Prometheus&lt;&#x2F;h3&gt;
&lt;p&gt;Add to your router configuration:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# configuration.nix&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  services&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;prometheus&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    port&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 9090&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;    # Scrape metrics every 15 seconds&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    globalConfig&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      scrape_interval&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;15s&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      evaluation_interval&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;15s&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;    # Start with just local node metrics&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    scrapeConfigs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        job_name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;node&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        static_configs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;          targets&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;localhost:9100&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt; ];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        }];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # Enable node exporter for system metrics&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  services&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;prometheus&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;exporters&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;node&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    port&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 9100&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    enabledCollectors&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      &amp;quot;systemd&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      &amp;quot;diskstats&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      &amp;quot;filesystem&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      &amp;quot;loadavg&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      &amp;quot;meminfo&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      &amp;quot;netdev&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      &amp;quot;stat&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      &amp;quot;time&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      &amp;quot;uname&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;enable-grafana&quot;&gt;Enable Grafana&lt;&#x2F;h3&gt;
&lt;p&gt;Add Grafana for visualization:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  services&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;grafana&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    settings&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      server&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        http_addr&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;0.0.0.0&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        http_port&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 3000&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        domain&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;192.168.1.1&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;      # Disable analytics&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      analytics&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;reporting_enabled&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; false&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;      # Anonymous access for read-only dashboards&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      &amp;quot;auth.anonymous&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        enabled&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        org_role&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;Viewer&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;    # Automatically configure Prometheus datasource&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    provision&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      datasources&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;settings&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;datasources&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;Prometheus&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        type&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;prometheus&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        url&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;http:&#x2F;&#x2F;localhost:9090&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        isDefault&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      }];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;open-firewall-ports&quot;&gt;Open Firewall Ports&lt;&#x2F;h3&gt;
&lt;p&gt;Allow access from your LAN:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  networking&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;firewall&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;interfaces&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;&amp;quot;br-lan&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    allowedTCPPorts&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 3000 9090&lt;&#x2F;span&gt;&lt;span&gt; ];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Deploy and verify:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# Update flake inputs to get the module&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;nix&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; flake update&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# Build and switch to the new configuration&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;sudo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; nixos-rebuild switch&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; --flake&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; .&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# Verify services&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;curl&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; http:&#x2F;&#x2F;192.168.1.1:3000&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # Should see Grafana login&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;step-2-deep-dive-network-metrics-exporter&quot;&gt;Step 2: Deep Dive - &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;tree&#x2F;master&#x2F;packages&#x2F;network-metrics-exporter&quot;&gt;network-metrics-exporter&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Now for the star of the show - our custom metrics exporter that makes per-client tracking possible.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;what-makes-this-exporter-special&quot;&gt;What Makes This Exporter Special&lt;&#x2F;h3&gt;
&lt;p&gt;Traditional exporters read from &lt;code&gt;&#x2F;proc&#x2F;net&#x2F;dev&lt;&#x2F;code&gt; or similar, giving you interface-level statistics. The &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;tree&#x2F;master&#x2F;packages&#x2F;network-metrics-exporter&quot;&gt;network-metrics-exporter&lt;&#x2F;a&gt; is a purpose-built Go program that provides granular, per-client network statistics with minimal overhead.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Key differences from standard exporters:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Per-client granularity&lt;&#x2F;strong&gt; - Tracks individual devices, not just interfaces&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Zero packet loss&lt;&#x2F;strong&gt; - Uses kernel-level counters, not sampling&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Connection tracking&lt;&#x2F;strong&gt; - Shows active connections per client&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Persistent device names&lt;&#x2F;strong&gt; - Remembers friendly names across reboots&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Efficient design&lt;&#x2F;strong&gt; - Written in Go for minimal resource usage&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;architecture-overview&quot;&gt;Architecture Overview&lt;&#x2F;h3&gt;
&lt;p&gt;The exporter consists of two main components working together:&lt;&#x2F;p&gt;
&lt;h4 id=&quot;1-the-go-exporter-network-metrics-exporter&quot;&gt;1. The Go Exporter (&lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;tree&#x2F;master&#x2F;packages&#x2F;network-metrics-exporter&quot;&gt;&lt;code&gt;network-metrics-exporter&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;)&lt;&#x2F;h4&gt;
&lt;p&gt;The main program is written in Go and handles:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Prometheus endpoint&lt;&#x2F;strong&gt; - Serves metrics on port 9101&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;State management&lt;&#x2F;strong&gt; - Maintains persistent client names in &lt;code&gt;&#x2F;var&#x2F;lib&#x2F;network-metrics&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Metric collection&lt;&#x2F;strong&gt; - Reads counters and connection tracking data&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;DHCP integration&lt;&#x2F;strong&gt; - Optionally reads DHCP leases for automatic naming&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;2-the-supporting-service&quot;&gt;2. The Supporting Service&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;client-traffic-tracker.service&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; - A bash script that runs when &lt;code&gt;enableNftablesIntegration = true&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Discovers active clients on your network using ARP and connection tracking&lt;&#x2F;li&gt;
&lt;li&gt;Creates nftables accounting rules for each client IP&lt;&#x2F;li&gt;
&lt;li&gt;Monitors for new devices every 60 seconds&lt;&#x2F;li&gt;
&lt;li&gt;Maintains the &lt;code&gt;CLIENT_TRAFFIC&lt;&#x2F;code&gt; chain in nftables&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;how-nftables-integration-works&quot;&gt;How nftables Integration Works&lt;&#x2F;h3&gt;
&lt;p&gt;When you set &lt;code&gt;enableNftablesIntegration = true&lt;&#x2F;code&gt;, the module sets up sophisticated packet accounting:&lt;&#x2F;p&gt;
&lt;h4 id=&quot;1-accounting-tables&quot;&gt;1. Accounting Tables&lt;&#x2F;h4&gt;
&lt;p&gt;The system creates dedicated nftables tables for metrics:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;table inet filter {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    chain CLIENT_TRAFFIC {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        # TX rules - count outgoing traffic per source IP&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        ip saddr 192.168.1.105 counter packets 41234 bytes 3234122 comment &amp;quot;tx_192.168.1.105&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        ip saddr 192.168.1.122 counter packets 8934 bytes 987123 comment &amp;quot;tx_192.168.1.122&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        ip saddr 192.168.1.150 counter packets 72819 bytes 8234122 comment &amp;quot;tx_192.168.1.150&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        # RX rules - count incoming traffic per destination IP&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        ip daddr 192.168.1.105 counter packets 58239 bytes 87359823 comment &amp;quot;rx_192.168.1.105&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        ip daddr 192.168.1.122 counter packets 7123 bytes 5234122 comment &amp;quot;rx_192.168.1.122&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        ip daddr 192.168.1.150 counter packets 91823 bytes 125789012 comment &amp;quot;rx_192.168.1.150&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    chain forward {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        # ... other forward rules ...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        jump CLIENT_TRAFFIC  # All forwarded traffic goes through accounting&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h4 id=&quot;2-connection-tracking&quot;&gt;2. Connection Tracking&lt;&#x2F;h4&gt;
&lt;p&gt;The exporter also reads from &lt;code&gt;conntrack&lt;&#x2F;code&gt; to count active connections:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# Raw conntrack data&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;tcp&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt;      6 431999&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; ESTABLISHED src=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt;192.168.1.105&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; dst=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt;142.250.185.142&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; sport=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt;55234&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; dport=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt;443&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;udp&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt;      17 59&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; src=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt;192.168.1.122&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; dst=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt;8.8.8.8&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; sport=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt;51234&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; dport=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt;53&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# Processed into metrics&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;network_client_connections&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;{ip=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;&amp;quot;192.168.1.105&amp;quot;}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 47&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;network_client_connections&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;{ip=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;&amp;quot;192.168.1.122&amp;quot;}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 12&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h4 id=&quot;3-the-collection-process&quot;&gt;3. The Collection Process&lt;&#x2F;h4&gt;
&lt;p&gt;Here’s how the complete flow works:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Discovery Phase&lt;&#x2F;strong&gt; (client-traffic-tracker):&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# Discover clients via ARP table&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;ip&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; neigh show&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; |&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt; grep&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt; &amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;br-lan&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; |&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt; grep&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; -E&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt; &amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;192.168.1.[0-9]+&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; |&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt; awk&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt; &amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;{print $1}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# Also check active connections&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;conntrack&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; -L&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; 2&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;&#x2F;dev&#x2F;null&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; |&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt; grep&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; -oE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt; &amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;192.168.1.[0-9]+&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; |&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt; sort&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; -u&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# For each discovered IP, create accounting rules&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;nft&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; add rule inet filter CLIENT_TRAFFIC ip saddr&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 192.168.1.105&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; counter comment&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;tx_192.168.1.105&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;nft&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; add rule inet filter CLIENT_TRAFFIC ip daddr&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 192.168.1.105&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; counter comment&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;rx_192.168.1.105&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Collection Phase&lt;&#x2F;strong&gt; (&lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;tree&#x2F;master&#x2F;packages&#x2F;network-metrics-exporter&quot;&gt;network-metrics-exporter&lt;&#x2F;a&gt;):&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;go&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;&#x2F;&#x2F; The Go program directly reads nftables counters&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;&#x2F;&#x2F; Parses rules from CLIENT_TRAFFIC chain&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;&#x2F;&#x2F; Reads connection tracking data&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;&#x2F;&#x2F; Enriches with persistent client names&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;&#x2F;&#x2F; Serves metrics on :9101&#x2F;metrics&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;performance-characteristics&quot;&gt;Performance Characteristics&lt;&#x2F;h3&gt;
&lt;p&gt;Real-world measurements from an Intel Celeron N5105 router (4 cores @ 2.0-2.9 GHz) with 25+ active clients:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;tree&#x2F;master&#x2F;packages&#x2F;network-metrics-exporter&quot;&gt;network-metrics-exporter&lt;&#x2F;a&gt; (Go)&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;CPU usage: ~1.5% average&lt;&#x2F;li&gt;
&lt;li&gt;Memory usage: 7.6MB (peak 11.9MB)&lt;&#x2F;li&gt;
&lt;li&gt;Process threads: 9&lt;&#x2F;li&gt;
&lt;li&gt;Runtime: 7+ hours stable&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;client-traffic-tracker (Bash)&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;CPU usage: &amp;lt; 0.1% (mostly sleeping)&lt;&#x2F;li&gt;
&lt;li&gt;Memory usage: &amp;lt; 1MB&lt;&#x2F;li&gt;
&lt;li&gt;Wake frequency: Every 60 seconds&lt;&#x2F;li&gt;
&lt;li&gt;Runtime: 2+ days stable&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;System impact&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Total CPU overhead: &amp;lt; 2%&lt;&#x2F;li&gt;
&lt;li&gt;Total memory: &amp;lt; 10MB combined&lt;&#x2F;li&gt;
&lt;li&gt;Network overhead: Zero (uses kernel counters)&lt;&#x2F;li&gt;
&lt;li&gt;Load average impact: Negligible (0.09 on a 4-core system)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;understanding-the-metrics&quot;&gt;Understanding the Metrics&lt;&#x2F;h3&gt;
&lt;p&gt;The exporter provides comprehensive metrics:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# Bandwidth metrics (cumulative counters)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;network_client_rx_bytes_total{ip=&amp;quot;192.168.1.105&amp;quot;, hostname=&amp;quot;gaming-pc&amp;quot;} 87359823&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;network_client_tx_bytes_total{ip=&amp;quot;192.168.1.105&amp;quot;, hostname=&amp;quot;gaming-pc&amp;quot;} 3234122&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# Connection metrics (current gauge)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;network_client_connections{ip=&amp;quot;192.168.1.105&amp;quot;, hostname=&amp;quot;gaming-pc&amp;quot;} 127&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# Status metrics&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;network_client_online{ip=&amp;quot;192.168.1.105&amp;quot;, hostname=&amp;quot;gaming-pc&amp;quot;} 1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;network_client_last_seen_timestamp{ip=&amp;quot;192.168.1.105&amp;quot;, hostname=&amp;quot;gaming-pc&amp;quot;} 1703123456&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# System information&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;network_exporter_up 1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;network_exporter_scrape_duration_seconds 0.012&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;why-this-architecture&quot;&gt;Why This Architecture?&lt;&#x2F;h3&gt;
&lt;p&gt;The combination of a Go exporter with a bash helper service provides the best of both worlds:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Go Exporter Benefits&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Efficient HTTP server for Prometheus scraping&lt;&#x2F;li&gt;
&lt;li&gt;Persistent state management for client names&lt;&#x2F;li&gt;
&lt;li&gt;Clean metric formatting and labeling&lt;&#x2F;li&gt;
&lt;li&gt;Low memory footprint&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Bash Helper Benefits&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Simple client discovery logic&lt;&#x2F;li&gt;
&lt;li&gt;Easy integration with system tools (ip, conntrack)&lt;&#x2F;li&gt;
&lt;li&gt;Transparent nftables rule management&lt;&#x2F;li&gt;
&lt;li&gt;Easy to debug and modify&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;The bash script handles dynamic rule creation as clients appear, while the Go program efficiently serves the metrics to Prometheus.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;step-3-configure-the-exporter&quot;&gt;Step 3: Configure the Exporter&lt;&#x2F;h2&gt;
&lt;p&gt;Let’s enable and configure the &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;tree&#x2F;master&#x2F;packages&#x2F;network-metrics-exporter&quot;&gt;network-metrics-exporter&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;basic-configuration&quot;&gt;Basic Configuration&lt;&#x2F;h3&gt;
&lt;p&gt;First, add my nixos repository as a flake input to get access to the &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;tree&#x2F;master&#x2F;packages&#x2F;network-metrics-exporter&quot;&gt;network-metrics-exporter&lt;&#x2F;a&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# flake.nix&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  inputs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    nixpkgs&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;url&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;github:NixOS&#x2F;nixpkgs&#x2F;nixos-24.11&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    arsfeld-nixos&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;url&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;github:arsfeld&#x2F;nixos&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  outputs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; self&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; nixpkgs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; arsfeld-nixos&lt;&#x2F;span&gt;&lt;span&gt; }: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    nixosConfigurations&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;router&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; nixpkgs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;lib&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;nixosSystem&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      system&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;x86_64-linux&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      modules&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [ &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;        .&#x2F;configuration.nix&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;        arsfeld-nixos&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;nixosModules&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;network-metrics-exporter&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      ];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Then configure the exporter in your configuration:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# configuration.nix&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  services&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;network-metrics-exporter&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;    # Network configuration&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    lanInterface&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;enp2s0&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;        # Your LAN interface (from Part 1)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    wanInterface&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;enp1s0&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;        # Your WAN interface (from Part 1)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    localSubnet&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;192.168.1.0&#x2F;24&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt; # Your LAN subnet&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;    # Persistent storage for client names&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    stateDir&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;&#x2F;var&#x2F;lib&#x2F;network-metrics&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;    # Update interval (seconds)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    updateInterval&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 5&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;    # Enable nftables integration&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    enableNftables&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;    # Web interface port&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    port&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 9101&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;configure-static-client-names&quot;&gt;Configure Static Client Names&lt;&#x2F;h3&gt;
&lt;p&gt;Pre-configure friendly names for known devices:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  services&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;network-metrics-exporter&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    staticClients&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      &amp;quot;192.168.1.105&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;gaming-pc&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      &amp;quot;192.168.1.122&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;smart-tv&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      &amp;quot;192.168.1.135&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;work-laptop&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      &amp;quot;192.168.1.150&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;nas&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;advanced-options&quot;&gt;Advanced Options&lt;&#x2F;h3&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  services&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;network-metrics-exporter&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;    # Automatically detect client names from DHCP leases&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    dhcpLeaseFile&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;&#x2F;var&#x2F;lib&#x2F;dhcp&#x2F;dhcpd.leases&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;    # How long before considering a client offline (seconds)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    offlineThreshold&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 300&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;    # Exclude certain IPs from monitoring&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    excludedIPs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [ &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      &amp;quot;192.168.1.1&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;     # Router itself&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      &amp;quot;192.168.1.255&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;   # Broadcast&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;    # Extra labels for all metrics&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    extraLabels&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      location&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;home&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      router&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;main&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;add-to-prometheus-scraping&quot;&gt;Add to Prometheus Scraping&lt;&#x2F;h3&gt;
&lt;p&gt;Update your Prometheus configuration:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  services&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;prometheus&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;scrapeConfigs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;    # ... existing configs ...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      job_name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;network-metrics&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      static_configs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        targets&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;localhost:9101&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt; ];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      }];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;      # Scrape more frequently for real-time data&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      scrape_interval&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;5s&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;step-4-build-powerful-dashboards&quot;&gt;Step 4: Build Powerful Dashboards&lt;&#x2F;h2&gt;
&lt;p&gt;Now the fun part - creating dashboards that give you instant visibility into your network usage.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;import-the-pre-built-dashboard&quot;&gt;Import the Pre-Built Dashboard&lt;&#x2F;h3&gt;
&lt;p&gt;The easiest way is to import our pre-built dashboard:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# configuration.nix&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  services&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;grafana&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;provision&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;dashboards&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;settings&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;providers&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;default&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    type&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;file&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    folder&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;Router&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    options&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;path&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; .&#x2F;dashboards&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # Path to your dashboard JSON files&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  }];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;key-dashboard-panels&quot;&gt;Key Dashboard Panels&lt;&#x2F;h3&gt;
&lt;p&gt;The repository includes &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;tree&#x2F;b3d094dc94c4e811daaa7fcca99451fcd3e2b1a4&#x2F;hosts&#x2F;router&#x2F;dashboards&#x2F;parts&quot;&gt;pre-built dashboard panels&lt;&#x2F;a&gt; that provide comprehensive network visibility:&lt;&#x2F;p&gt;
&lt;h4 id=&quot;active-clients-count&quot;&gt;Active Clients Count&lt;&#x2F;h4&gt;
&lt;p&gt;Shows the total number of active clients on your network:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;count(count by (ip) (client_active_connections{job=&amp;quot;network-metrics&amp;quot;}))&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h4 id=&quot;client-connection-count-table&quot;&gt;Client Connection Count Table&lt;&#x2F;h4&gt;
&lt;p&gt;Displays each client’s active connections in a sortable table:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;client_active_connections{job=&amp;quot;network-metrics&amp;quot;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h4 id=&quot;real-time-client-bandwidth&quot;&gt;Real-time Client Bandwidth&lt;&#x2F;h4&gt;
&lt;p&gt;Live graph showing download&#x2F;upload speeds per client:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# Download speed (bits per second)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;rate(client_traffic_rx_bytes{job=&amp;quot;network-metrics&amp;quot;}[1m]) * 8&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# Upload speed (bits per second)  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;rate(client_traffic_tx_bytes{job=&amp;quot;network-metrics&amp;quot;}[1m]) * 8&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h4 id=&quot;client-bandwidth-analysis&quot;&gt;Client Bandwidth Analysis&lt;&#x2F;h4&gt;
&lt;p&gt;Detailed per-client bandwidth usage over time with legend showing current&#x2F;avg&#x2F;max values.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;total-bandwidth-gauges&quot;&gt;Total Bandwidth Gauges&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Total Download Bandwidth (Mbps)&lt;&#x2F;strong&gt; - Aggregate download across all clients&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Total Upload Bandwidth (Mbps)&lt;&#x2F;strong&gt; - Aggregate upload across all clients&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;These panels are defined in &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;tree&#x2F;b3d094dc94c4e811daaa7fcca99451fcd3e2b1a4&#x2F;hosts&#x2F;router&#x2F;dashboards&#x2F;parts&#x2F;clients-panels.json&quot;&gt;clients-panels.json&lt;&#x2F;a&gt; and automatically provisioned when you enable Grafana.&lt;&#x2F;p&gt;
&lt;p&gt;The complete dashboard includes multiple sections:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;tree&#x2F;b3d094dc94c4e811daaa7fcca99451fcd3e2b1a4&#x2F;hosts&#x2F;router&#x2F;dashboards&#x2F;parts&#x2F;system-panels.json&quot;&gt;System Overview&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; - CPU, memory, disk usage&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;tree&#x2F;b3d094dc94c4e811daaa7fcca99451fcd3e2b1a4&#x2F;hosts&#x2F;router&#x2F;dashboards&#x2F;parts&#x2F;network-interfaces-panels.json&quot;&gt;Network Interfaces&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; - WAN&#x2F;LAN interface statistics&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;tree&#x2F;b3d094dc94c4e811daaa7fcca99451fcd3e2b1a4&#x2F;hosts&#x2F;router&#x2F;dashboards&#x2F;parts&#x2F;dns-panels.json&quot;&gt;DNS&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; - Query rates, cache hits, blocked domains&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;tree&#x2F;b3d094dc94c4e811daaa7fcca99451fcd3e2b1a4&#x2F;hosts&#x2F;router&#x2F;dashboards&#x2F;parts&#x2F;qos-panels.json&quot;&gt;QoS&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; - Traffic shaping and prioritization&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;tree&#x2F;b3d094dc94c4e811daaa7fcca99451fcd3e2b1a4&#x2F;hosts&#x2F;router&#x2F;dashboards&#x2F;parts&#x2F;natpmp-panels.json&quot;&gt;NAT-PMP&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; - Port mapping statistics&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;All panels are dynamically assembled by &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;tree&#x2F;b3d094dc94c4e811daaa7fcca99451fcd3e2b1a4&#x2F;hosts&#x2F;router&#x2F;dashboards&#x2F;default.nix&quot;&gt;default.nix&lt;&#x2F;a&gt; into a cohesive dashboard.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note on Declarative Dashboards&lt;&#x2F;strong&gt;: The approach shown here makes your entire monitoring stack declarative and reproducible. Your dashboards are defined as code, version-controlled, and automatically provisioned when you deploy. No more manually recreating dashboards or losing them during upgrades! For a deep dive into declarative Grafana dashboards with NixOS, see the upcoming bonus guide in this series.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;creating-custom-panels&quot;&gt;Creating Custom Panels&lt;&#x2F;h3&gt;
&lt;p&gt;For a bandwidth usage timeline:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Create new panel → Time series&lt;&#x2F;li&gt;
&lt;li&gt;Add query: &lt;code&gt;rate(network_client_rx_bytes_total[1m]) * 8 &#x2F; 1000000&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Legend: &lt;code&gt;{{hostname}} - Download&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Unit: &lt;code&gt;Mbps&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Stack series: Off (to see individual clients)&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;For a current usage table:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Create new panel → Table&lt;&#x2F;li&gt;
&lt;li&gt;Add queries:
&lt;ul&gt;
&lt;li&gt;A: &lt;code&gt;rate(network_client_rx_bytes_total[1m]) * 8 &#x2F; 1000000&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;B: &lt;code&gt;rate(network_client_tx_bytes_total[1m]) * 8 &#x2F; 1000000&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;C: &lt;code&gt;network_client_connections&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Transform → Merge&lt;&#x2F;li&gt;
&lt;li&gt;Override columns:
&lt;ul&gt;
&lt;li&gt;A: “Download (Mbps)”&lt;&#x2F;li&gt;
&lt;li&gt;B: “Upload (Mbps)”&lt;&#x2F;li&gt;
&lt;li&gt;C: “Connections”&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;dashboard-variables&quot;&gt;Dashboard Variables&lt;&#x2F;h3&gt;
&lt;p&gt;Add variables for filtering:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# In your dashboard JSON&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;&amp;quot;templating&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  &amp;quot;list&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #000000;background-color: #FFFFFF;&quot;&gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;[&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #000000;background-color: #FFFFFF;&quot;&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      &amp;quot;name&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;client&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;query&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      &amp;quot;query&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;label_values(network_client_online, hostname)&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      &amp;quot;multi&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      &amp;quot;includeAll&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt; true&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #000000;background-color: #FFFFFF;&quot;&gt;  &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Then use in queries:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;rate(network_client_rx_bytes_total{hostname=~&amp;quot;$client&amp;quot;}[1m])&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;troubleshooting&quot;&gt;Troubleshooting&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;no-metrics-appearing&quot;&gt;No Metrics Appearing&lt;&#x2F;h3&gt;
&lt;p&gt;Check the exporter is running:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;systemctl&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; status network-metrics-exporter&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;curl&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; http:&#x2F;&#x2F;localhost:9101&#x2F;metrics&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; |&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt; grep&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; network_client&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;missing-clients&quot;&gt;Missing Clients&lt;&#x2F;h3&gt;
&lt;p&gt;Ensure nftables rules are created:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;sudo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; nft list table netdev metrics&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;incorrect-traffic-counts&quot;&gt;Incorrect Traffic Counts&lt;&#x2F;h3&gt;
&lt;p&gt;Verify interfaces are correct:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;ip&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; link show&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # Find your LAN&#x2F;WAN interfaces&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;performance-impact&quot;&gt;Performance Impact&lt;&#x2F;h3&gt;
&lt;p&gt;The exporter is designed for minimal impact:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Atomic counter reads (no packet loss)&lt;&#x2F;li&gt;
&lt;li&gt;Efficient rule management&lt;&#x2F;li&gt;
&lt;li&gt;Configurable update intervals&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;If you have hundreds of clients, increase the update interval:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;services&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;network-metrics-exporter&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;updateInterval&lt;&#x2F;span&gt;&lt;span style=&quot;color: #000000;background-color: #FFFFFF;&quot;&gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 30&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;advanced-usage&quot;&gt;Advanced Usage&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;alert-on-bandwidth-hogs&quot;&gt;Alert on Bandwidth Hogs&lt;&#x2F;h3&gt;
&lt;p&gt;Add Prometheus alerts:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  services&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;prometheus&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;rules&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;&amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;    groups:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;    - name: bandwidth&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      rules:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      - alert: HighBandwidthUsage&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;        expr: rate(network_client_rx_bytes_total[5m]) &amp;gt; 100000000  # 100 MB&#x2F;s&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;        for: 5m&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;        annotations:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;          summary: &amp;quot;{{ $labels.hostname }} using high bandwidth&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;          description: &amp;quot;{{ $labels.hostname }} downloading at {{ $value | humanize }}B&#x2F;s&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  &amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;track-monthly-quotas&quot;&gt;Track Monthly Quotas&lt;&#x2F;h3&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# Monthly usage in GB&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;increase(network_client_rx_bytes_total[30d]) &#x2F; 1073741824&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;identify-streaming-devices&quot;&gt;Identify Streaming Devices&lt;&#x2F;h3&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# Sustained bandwidth over 4 Mbps (typical streaming)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;avg_over_time(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  rate(network_client_rx_bytes_total[1m])[1h:]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;) &amp;gt; 500000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h2&gt;
&lt;p&gt;You now have powerful per-client network monitoring! Your dashboards show:&lt;&#x2F;p&gt;
&lt;p&gt;✅ Real-time bandwidth usage per device&lt;br &#x2F;&gt;
✅ Historical usage trends&lt;br &#x2F;&gt;
✅ Connection counts and client status&lt;br &#x2F;&gt;
✅ Top bandwidth consumers&lt;br &#x2F;&gt;
✅ Daily&#x2F;monthly usage totals&lt;&#x2F;p&gt;
&lt;p&gt;With this visibility, you can finally answer questions like:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Why is my internet slow right now?&lt;&#x2F;li&gt;
&lt;li&gt;Which device used all our data this month?&lt;&#x2F;li&gt;
&lt;li&gt;Is someone streaming 4K video during work hours?&lt;&#x2F;li&gt;
&lt;li&gt;Are all my IoT devices phoning home constantly?&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;what-s-next&quot;&gt;What’s Next?&lt;&#x2F;h2&gt;
&lt;p&gt;This concludes the currently available posts in the NixOS Router Series. You now have:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;&#x2F;posts&#x2F;nixos-router-getting-started&quot;&gt;Part 1: Getting Started&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; - A working NixOS router with basic connectivity&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;&#x2F;posts&#x2F;nixos-router-blog-post-2-testing&quot;&gt;Part 2: Testing&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; - Comprehensive tests to ensure reliability&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Part 3: Monitoring&lt;&#x2F;strong&gt; (this post) - Per-client bandwidth tracking and dashboards&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;For a complete overview of the entire router build including advanced features like QoS, VLANs, and hardware selection, check out my &lt;strong&gt;&lt;a href=&quot;&#x2F;posts&#x2F;nixos-router-journey&quot;&gt;NixOS router journey&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; post.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;&lt;em&gt;Found this helpful? Check out the &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;tree&#x2F;master&#x2F;packages&#x2F;network-metrics-exporter&quot;&gt;complete code&lt;&#x2F;a&gt; and &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&quot;&gt;full router configuration&lt;&#x2F;a&gt; on GitHub.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>How to Write Tests for Your NixOS Router Configuration</title>
          <pubDate>Sun, 20 Jul 2025 00:00:00 +0000</pubDate>
          <author>Alex Rosenfeld</author>
          <link>https://blog.arsfeld.dev/posts/2025/07/20/nixos-router-blog-post-2-testing/</link>
          <guid>https://blog.arsfeld.dev/posts/2025/07/20/nixos-router-blog-post-2-testing/</guid>
          <description xml:base="https://blog.arsfeld.dev/posts/2025/07/20/nixos-router-blog-post-2-testing/">&lt;p&gt;&lt;em&gt;Part 2 of the NixOS Router Series&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;In the &lt;a href=&quot;&#x2F;posts&#x2F;nixos-router-getting-started&quot;&gt;previous post&lt;&#x2F;a&gt;, we built a minimal NixOS router that provides internet connectivity to your network. Now let’s ensure it stays reliable by writing comprehensive tests for our configuration.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Why test your router?&lt;&#x2F;strong&gt; A misconfigured router can leave you without internet access, making it difficult to fix remotely. By writing tests, you can:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Catch configuration errors before deployment&lt;&#x2F;li&gt;
&lt;li&gt;Verify features work as expected&lt;&#x2F;li&gt;
&lt;li&gt;Prevent regressions when making changes&lt;&#x2F;li&gt;
&lt;li&gt;Document expected behavior&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;moving-to-flakes&quot;&gt;Moving to Flakes&lt;&#x2F;h2&gt;
&lt;p&gt;Starting with this post, we’ll use Nix Flakes for a more structured and reproducible configuration. Flakes provide:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Reproducible builds&lt;&#x2F;strong&gt; with locked dependencies&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Better composability&lt;&#x2F;strong&gt; for modular configurations&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Built-in testing framework&lt;&#x2F;strong&gt; support&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Easier deployment&lt;&#x2F;strong&gt; with tools like deploy-rs&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;If you’re new to flakes, check out:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;nixos.wiki&#x2F;wiki&#x2F;Flakes&quot;&gt;Official Nix Flakes documentation&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;zero-to-nix.com&#x2F;concepts&#x2F;flakes&quot;&gt;Zero to Nix - Flakes guide&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;blob&#x2F;master&#x2F;flake.nix&quot;&gt;My example flake.nix&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;To migrate your &lt;code&gt;&#x2F;etc&#x2F;nixos&lt;&#x2F;code&gt; configuration to flakes:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# Enable flakes&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;mkdir&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; -p&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; ~&#x2F;.config&#x2F;nix&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt;echo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;experimental-features = nix-command flakes&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; &amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; ~&#x2F;.config&#x2F;nix&#x2F;nix.conf&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# Create a new directory for your flake&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;mkdir&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; ~&#x2F;nixos-router&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt;cd&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; ~&#x2F;nixos-router&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# Copy your existing configuration&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;cp&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &#x2F;etc&#x2F;nixos&#x2F;configuration.nix .&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;cp&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &#x2F;etc&#x2F;nixos&#x2F;hardware-configuration.nix .&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# Initialize git (flakes require version control)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;git&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; init&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;git&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; add .&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# Create a basic flake.nix&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;cat&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; &amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; flake.nix&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; &amp;lt;&amp;lt;&lt;&#x2F;span&gt;&lt;span&gt; &amp;#39;EOF&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  inputs.nixpkgs.url = &amp;quot;github:NixOS&#x2F;nixpkgs&#x2F;nixos-24.11&amp;quot;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  outputs = { self, nixpkgs }: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;    nixosConfigurations.router = nixpkgs.lib.nixosSystem {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      system = &amp;quot;x86_64-linux&amp;quot;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      modules = [ .&#x2F;configuration.nix ];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;    };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;EOF&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# Test your flake&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;nix&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; flake check&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;A working NixOS router from Part 1&lt;&#x2F;li&gt;
&lt;li&gt;Basic familiarity with Nix expressions&lt;&#x2F;li&gt;
&lt;li&gt;Nix with flakes enabled&lt;&#x2F;li&gt;
&lt;li&gt;About 30 minutes&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;step-1-introduction-to-nixos-tests&quot;&gt;Step 1: Introduction to NixOS Tests&lt;&#x2F;h2&gt;
&lt;p&gt;NixOS has a powerful testing framework that spins up virtual machines to test your configuration. Tests are written as Nix expressions that:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Define virtual machines with your configuration&lt;&#x2F;li&gt;
&lt;li&gt;Run test scripts to verify behavior&lt;&#x2F;li&gt;
&lt;li&gt;Report success or failure&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;basic-test-structure&quot;&gt;Basic Test Structure&lt;&#x2F;h3&gt;
&lt;p&gt;Let’s look at a real router test from &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;blob&#x2F;0933bcd1bf9a3d89e9c666fd46bb33bc8136f3a9&#x2F;tests&#x2F;router-test.nix&quot;&gt;my configuration&lt;&#x2F;a&gt;. Create a new file &lt;code&gt;tests&#x2F;router-test.nix&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; self&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; inputs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;,&lt;&#x2F;span&gt;&lt;span&gt; }: {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; lib&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; pkgs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;, ...&lt;&#x2F;span&gt;&lt;span&gt; }: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;router-test&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  nodes&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;    # The router VM using actual configuration&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    router&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; config&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; pkgs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;, ...&lt;&#x2F;span&gt;&lt;span&gt; }: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      imports&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;        # Import your router configuration&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;        &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;${&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;self&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;&#x2F;configuration.nix&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      ];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;      # Test-specific overrides&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      virtualisation&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;vlans&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt;1 2 3&lt;&#x2F;span&gt;&lt;span&gt;];&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt; # WAN + 2 LAN ports&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;      # Override hardware-specific settings for VM&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      boot&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;loader&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;grub&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; false&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      fileSystems&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;&amp;quot;&#x2F;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        device&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;tmpfs&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        fsType&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;tmpfs&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;      # Override interface names for test environment&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      networking&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;interfaces&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; lib&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;mkForce&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        eth1&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;useDHCP&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; false&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # WAN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        eth2&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;ipv4&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;addresses&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;          address&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;192.168.1.1&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;          prefixLength&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 24&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        }];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;      # For test, use static IP on WAN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      networking&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;interfaces&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;eth1&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;ipv4&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;addresses&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        address&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;10.0.2.2&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        prefixLength&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 24&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      }];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      networking&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;defaultGateway&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;10.0.2.1&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      networking&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;nat&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;externalInterface&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; lib&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;mkForce&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;eth1&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      networking&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;nat&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;internalInterfaces&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; lib&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;mkForce&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;eth2&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt; ];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;    # A client VM to test connectivity&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    client1&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; config&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; pkgs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;, ...&lt;&#x2F;span&gt;&lt;span&gt; }: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      virtualisation&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;vlans&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt;2&lt;&#x2F;span&gt;&lt;span&gt;];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      networking&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        useDHCP&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; false&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        useNetworkd&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      systemd&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;network&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        networks&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;&amp;quot;30-lan&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;          matchConfig&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;Name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;eth1&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;          networkConfig&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;            DHCP&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;yes&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;            IPv6AcceptRA&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; false&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  testScript&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;    start_all()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;    # Wait for machines to boot&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;    router.wait_for_unit(&amp;quot;multi-user.target&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;    client1.wait_for_unit(&amp;quot;multi-user.target&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;    # Your tests here&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  &amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;running-tests-locally&quot;&gt;Running Tests Locally&lt;&#x2F;h3&gt;
&lt;p&gt;With flakes, run your test like this:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;nix&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; build .#checks.x86_64-linux.router-test&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The test framework provides a Python API for controlling VMs and making assertions.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;step-2-write-connectivity-tests&quot;&gt;Step 2: Write Connectivity Tests&lt;&#x2F;h2&gt;
&lt;p&gt;Let’s write tests that verify our router provides internet access and serves DHCP correctly.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;test-internet-access&quot;&gt;Test Internet Access&lt;&#x2F;h3&gt;
&lt;p&gt;Here’s how we test connectivity in the actual router test:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;testScript&lt;&#x2F;span&gt;&lt;span style=&quot;color: #000000;background-color: #FFFFFF;&quot;&gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  start_all()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  # Wait for all machines to be ready&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  external.wait_for_unit(&amp;quot;multi-user.target&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  router.wait_for_unit(&amp;quot;multi-user.target&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  client1.wait_for_unit(&amp;quot;multi-user.target&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  # Wait for services&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  router.wait_for_unit(&amp;quot;dnsmasq.service&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  # Give DHCP time to assign addresses&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  client1.wait_until_succeeds(&amp;quot;ip addr show eth1 | grep 192.168.1&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  # Test basic connectivity&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  with subtest(&amp;quot;Clients can ping router&amp;quot;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      client1.succeed(&amp;quot;ping -c 1 192.168.1.1&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  with subtest(&amp;quot;Router can ping external&amp;quot;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      router.succeed(&amp;quot;ping -c 1 10.0.2.1&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  with subtest(&amp;quot;Clients can reach external through router&amp;quot;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      # Test routing&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      client1.succeed(&amp;quot;ping -c 1 10.0.2.1&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      # Test NAT and web connectivity&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      client1.succeed(&amp;quot;curl -f http:&#x2F;&#x2F;10.0.2.1&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;&amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;test-lan-connectivity&quot;&gt;Test LAN Connectivity&lt;&#x2F;h3&gt;
&lt;p&gt;Add another client to test LAN-to-LAN communication:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;nodes&lt;&#x2F;span&gt;&lt;span style=&quot;color: #000000;background-color: #FFFFFF;&quot;&gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # ... existing nodes ...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # Client on second LAN port&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  client2&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; config&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; pkgs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;, ...&lt;&#x2F;span&gt;&lt;span&gt; }: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    virtualisation&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;vlans&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt;3&lt;&#x2F;span&gt;&lt;span&gt;];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    networking&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      useDHCP&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; false&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      useNetworkd&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    systemd&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;network&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      networks&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;&amp;quot;30-lan&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        matchConfig&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;Name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;eth1&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        networkConfig&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;          DHCP&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;yes&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;          IPv6AcceptRA&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; false&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;testScript&lt;&#x2F;span&gt;&lt;span style=&quot;color: #000000;background-color: #FFFFFF;&quot;&gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  # ... existing tests ...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  with subtest(&amp;quot;Clients can communicate with each other&amp;quot;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      # Get IP addresses&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      client1_ip = client1.succeed(&amp;quot;ip -4 addr show eth1 | grep inet | awk &amp;#39;{print $2}&amp;#39; | cut -d&amp;#39;&#x2F;&amp;#39; -f1&amp;quot;).strip()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      client2_ip = client2.succeed(&amp;quot;ip -4 addr show eth1 | grep inet | awk &amp;#39;{print $2}&amp;#39; | cut -d&amp;#39;&#x2F;&amp;#39; -f1&amp;quot;).strip()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      # Test ping between clients&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      client1.succeed(f&amp;quot;ping -c 1 {client2_ip}&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      client2.succeed(f&amp;quot;ping -c 1 {client1_ip}&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      # Test HTTP between clients&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      client1.succeed(f&amp;quot;curl -f http:&#x2F;&#x2F;{client2_ip}:8080&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      client2.succeed(f&amp;quot;curl -f http:&#x2F;&#x2F;{client1_ip}:9090&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;&amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;test-service-availability&quot;&gt;Test Service Availability&lt;&#x2F;h3&gt;
&lt;p&gt;Verify essential services are running:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;testScript&lt;&#x2F;span&gt;&lt;span style=&quot;color: #000000;background-color: #FFFFFF;&quot;&gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  # ... existing tests ...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  with subtest(&amp;quot;Core services are running&amp;quot;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      # Check that dnsmasq is running (provides DHCP and DNS)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      router.succeed(&amp;quot;systemctl is-active dnsmasq&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      router.wait_for_open_port(53)  # DNS port&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      # Check that firewall is active&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      router.succeed(&amp;quot;systemctl is-active nftables&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;&amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;step-3-test-your-features&quot;&gt;Step 3: Test Your Features&lt;&#x2F;h2&gt;
&lt;p&gt;Now let’s test specific features of your router configuration.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;test-nat-and-firewall-rules&quot;&gt;Test NAT and Firewall Rules&lt;&#x2F;h3&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;testScript&lt;&#x2F;span&gt;&lt;span style=&quot;color: #000000;background-color: #FFFFFF;&quot;&gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  # ... existing tests ...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  with subtest(&amp;quot;Check NAT table&amp;quot;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      # Generate some traffic&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      client1.succeed(&amp;quot;curl -f http:&#x2F;&#x2F;10.0.2.1&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      # Check NAT translations exist&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      router.succeed(&amp;quot;nft list table ip nat | grep masquerade&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;&amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;test-dhcp-static-reservations&quot;&gt;Test DHCP Static Reservations&lt;&#x2F;h3&gt;
&lt;p&gt;The router test includes a static DHCP reservation test:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;nodes&lt;&#x2F;span&gt;&lt;span style=&quot;color: #000000;background-color: #FFFFFF;&quot;&gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # Storage server with static IP via DHCP reservation&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  storage&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; config&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; pkgs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;, ...&lt;&#x2F;span&gt;&lt;span&gt; }: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    virtualisation&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;vlans&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt;2&lt;&#x2F;span&gt;&lt;span&gt;];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    systemd&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;network&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;      # Set MAC address for static DHCP reservation&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      links&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;&amp;quot;10-eth1&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        matchConfig&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;OriginalName&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;eth1&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        linkConfig&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;MACAddress&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;00:e0:4c:bb:00:e3&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      networks&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;&amp;quot;30-lan&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        matchConfig&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;Name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;eth1&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        networkConfig&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;          DHCP&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;yes&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;          IPv6AcceptRA&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; false&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;testScript&lt;&#x2F;span&gt;&lt;span style=&quot;color: #000000;background-color: #FFFFFF;&quot;&gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  # ... existing tests ...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  with subtest(&amp;quot;Storage has correct static IP&amp;quot;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      # Verify storage got the static IP 192.168.1.50&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      storage.wait_until_succeeds(&amp;quot;ip addr show eth1 | grep 192.168.1.50&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      storage_ip = storage.succeed(&amp;quot;ip -4 addr show eth1 | grep inet | awk &amp;#39;{print $2}&amp;#39; | cut -d&amp;#39;&#x2F;&amp;#39; -f1&amp;quot;).strip()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      assert storage_ip == &amp;quot;192.168.1.50&amp;quot;, f&amp;quot;Storage IP is {storage_ip}, expected 192.168.1.50&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;&amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;test-port-forwarding-if-configured&quot;&gt;Test Port Forwarding (if configured)&lt;&#x2F;h3&gt;
&lt;p&gt;If you’ve added port forwarding rules:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;testScript&lt;&#x2F;span&gt;&lt;span style=&quot;color: #000000;background-color: #FFFFFF;&quot;&gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  # ... existing tests ...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  with subtest(&amp;quot;Port forwarding works&amp;quot;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      # Example: Test if port 80 is forwarded to internal server&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      # Assuming you have a rule like:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      # networking.firewall.extraCommands = &amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;      #   iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination 192.168.1.100:80&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;      # &amp;#39;&amp;#39;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;      # Start a simple web server on client&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;      client1&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;execute&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;&amp;quot;python3 -m http.server 80 &amp;amp;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;      # Test from external that port is accessible&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;      external&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;succeed&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;&amp;quot;curl -f http:&#x2F;&#x2F;10.0.2.2&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;&amp;#39;&amp;#39;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;step-4-integrate-with-deployment&quot;&gt;Step 4: Integrate with Deployment&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;pre-deployment-testing&quot;&gt;Pre-deployment Testing&lt;&#x2F;h3&gt;
&lt;p&gt;Create a script to run tests before deploying:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;#!&#x2F;usr&#x2F;bin&#x2F;env bash&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# deploy-with-tests.sh&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt;set&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; -e&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt;echo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;Running router tests...&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;nix&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; build .#checks.x86_64-linux.router-test&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;if&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;font-style: italic;&quot;&gt; $?&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; -eq&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 0&lt;&#x2F;span&gt;&lt;span&gt; ];&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; then&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt;    echo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;Tests passed! Deploying...&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;    nixos-rebuild&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; switch&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; --flake&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; .#router&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; --target-host 192.168.1.1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;else&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt;    echo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;Tests failed! Deployment cancelled.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #E9F284;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt;    exit&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;fi&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;automated-validation&quot;&gt;Automated Validation&lt;&#x2F;h3&gt;
&lt;p&gt;Add a post-deployment validation script:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# In your router configuration&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;environment&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;systemPackages&lt;&#x2F;span&gt;&lt;span style=&quot;color: #000000;background-color: #FFFFFF;&quot;&gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #000000;background-color: #FFFFFF;&quot;&gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;with&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; pkgs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;;&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;writeScriptBin&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;validate-router&amp;quot; &amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;    #!&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;${&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;stdenv&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;shell&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;    set -e&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;    echo &amp;quot;Validating router configuration...&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;    # Check services&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;    systemctl is-active dnsmasq.service &amp;gt;&#x2F;dev&#x2F;null || (echo &amp;quot;DHCP&#x2F;DNS failed&amp;quot; &amp;amp;&amp;amp; exit 1)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;    systemctl is-active nftables.service &amp;gt;&#x2F;dev&#x2F;null || (echo &amp;quot;Firewall failed&amp;quot; &amp;amp;&amp;amp; exit 1)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;    # Check connectivity&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;    ping -c 3 1.1.1.1 &amp;gt;&#x2F;dev&#x2F;null || (echo &amp;quot;WAN connectivity failed&amp;quot; &amp;amp;&amp;amp; exit 1)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;    # Check DNS&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;    nslookup example.com &amp;gt;&#x2F;dev&#x2F;null || (echo &amp;quot;DNS resolution failed&amp;quot; &amp;amp;&amp;amp; exit 1)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;    echo &amp;quot;All validations passed!&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  &amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;advanced-testing-patterns&quot;&gt;Advanced Testing Patterns&lt;&#x2F;h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;&#x2F;strong&gt;: The examples in this section are conceptual and untested. They demonstrate possible testing approaches you might want to explore.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h3 id=&quot;performance-testing&quot;&gt;Performance Testing&lt;&#x2F;h3&gt;
&lt;p&gt;Test throughput and latency:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;testScript&lt;&#x2F;span&gt;&lt;span style=&quot;color: #000000;background-color: #FFFFFF;&quot;&gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  # ... existing tests ...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  # Install iperf3 for performance testing&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  router.succeed(&amp;quot;nix-env -iA nixos.iperf3&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  client.succeed(&amp;quot;nix-env -iA nixos.iperf3&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  # Start iperf3 server on router&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  router.execute(&amp;quot;iperf3 -s -D&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  # Test throughput&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  result = client.succeed(&amp;quot;iperf3 -c 192.168.1.1 -t 10 -J&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  import json&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  data = json.loads(result)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  throughput = data[&amp;#39;end&amp;#39;][&amp;#39;sum_received&amp;#39;][&amp;#39;bits_per_second&amp;#39;] &#x2F; 1_000_000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  print(f&amp;quot;Throughput: {throughput:.2f} Mbps&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  # Ensure minimum performance (adjust based on hardware)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  assert throughput &amp;gt; 100, f&amp;quot;Throughput too low: {throughput} Mbps&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;&amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;failure-testing&quot;&gt;Failure Testing&lt;&#x2F;h3&gt;
&lt;p&gt;Test recovery from failures:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;testScript&lt;&#x2F;span&gt;&lt;span style=&quot;color: #000000;background-color: #FFFFFF;&quot;&gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  # ... existing tests ...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  # Test DHCP server restart&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  router.systemctl(&amp;quot;restart dhcpd4.service&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  router.wait_for_unit(&amp;quot;dhcpd4.service&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  # Client should still get lease after restart&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  client.succeed(&amp;quot;dhclient -r eth1 &amp;amp;&amp;amp; dhclient eth1&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  client.succeed(&amp;quot;ping -c 3 192.168.1.1&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  # Test firewall reload&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  router.succeed(&amp;quot;nft flush ruleset&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  router.systemctl(&amp;quot;restart nftables.service&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  router.wait_for_unit(&amp;quot;nftables.service&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  # NAT should still work&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  client.succeed(&amp;quot;curl -f https:&#x2F;&#x2F;example.com&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;&amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;troubleshooting-common-issues&quot;&gt;Troubleshooting Common Issues&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;tests-hang&quot;&gt;Tests Hang&lt;&#x2F;h3&gt;
&lt;p&gt;If tests hang, add timeouts:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;testScript&lt;&#x2F;span&gt;&lt;span style=&quot;color: #000000;background-color: #FFFFFF;&quot;&gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  with subtest(&amp;quot;DHCP lease acquisition&amp;quot;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      client.wait_until_succeeds(&amp;quot;ip addr show eth1 | grep -q &amp;#39;inet &amp;#39;&amp;quot;, timeout=30)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;&amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;debugging-failed-tests&quot;&gt;Debugging Failed Tests&lt;&#x2F;h3&gt;
&lt;p&gt;Enable interactive mode to debug:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;nix&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; build .#checks.x86_64-linux.router-test&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; --keep-failed&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# Then run the test interactively:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt;cd&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; result&lt;&#x2F;span&gt;&lt;span&gt; &amp;amp;&amp;amp;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt; .&#x2F;bin&#x2F;nixos-test-driver&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; --interactive&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;resource-constraints&quot;&gt;Resource Constraints&lt;&#x2F;h3&gt;
&lt;p&gt;Reduce VM resources if tests fail due to memory:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;nodes&lt;&#x2F;span&gt;&lt;span style=&quot;color: #000000;background-color: #FFFFFF;&quot;&gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  router&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; config&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; pkgs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;, ...&lt;&#x2F;span&gt;&lt;span&gt; }: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    virtualisation&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;memorySize&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 512&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # MB&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    virtualisation&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;cores&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 1&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;&#x2F;h2&gt;
&lt;p&gt;You now have a comprehensive test suite for your NixOS router! Your tests verify:&lt;&#x2F;p&gt;
&lt;p&gt;✅ Basic connectivity and DHCP&lt;br &#x2F;&gt;
✅ Internet access through NAT&lt;br &#x2F;&gt;
✅ Firewall rules and security&lt;br &#x2F;&gt;
✅ Service availability&lt;br &#x2F;&gt;
✅ Configuration-specific features&lt;&#x2F;p&gt;
&lt;p&gt;With these tests in place, you can confidently make changes knowing you’ll catch any issues before they affect your network.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;next-steps&quot;&gt;Next Steps&lt;&#x2F;h2&gt;
&lt;p&gt;In the next post, we’ll add monitoring to track per-client network usage with custom metrics. You’ll gain real-time visibility into which devices are using your bandwidth!&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Continue to:&lt;&#x2F;strong&gt; &lt;a href=&quot;&#x2F;posts&#x2F;nixos-router-blog-post-3-monitoring&quot;&gt;Part 3 - Monitor Per-Client Network Usage →&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;For a complete overview of the entire router build including advanced features like QoS, VLANs, and hardware selection, check out my &lt;strong&gt;&lt;a href=&quot;&#x2F;posts&#x2F;nixos-router-journey&quot;&gt;NixOS router journey&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; post.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;&lt;em&gt;Found this helpful? Check out the &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;blob&#x2F;0933bcd1bf9a3d89e9c666fd46bb33bc8136f3a9&#x2F;tests&#x2F;router-test.nix&quot;&gt;complete test file&lt;&#x2F;a&gt; and &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&quot;&gt;full router configuration&lt;&#x2F;a&gt; on GitHub.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Build Your Own Router with NixOS: Part 1 - Getting Started</title>
          <pubDate>Fri, 18 Jul 2025 00:00:00 +0000</pubDate>
          <author>Alex Rosenfeld</author>
          <link>https://blog.arsfeld.dev/posts/2025/07/18/nixos-router-getting-started/</link>
          <guid>https://blog.arsfeld.dev/posts/2025/07/18/nixos-router-getting-started/</guid>
          <description xml:base="https://blog.arsfeld.dev/posts/2025/07/18/nixos-router-getting-started/">&lt;p&gt;&lt;img src=&quot;&#x2F;images&#x2F;nixos-router-getting-started.png&quot; alt=&quot;A futuristic mini PC transforming into a network router with glowing ethernet connections&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;&#x2F;strong&gt;: Turn a $150 mini PC into a powerful, declarative router using NixOS. This guide covers hardware selection, basic installation, and minimal router configuration to get you online.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-build-your-own-router&quot;&gt;Why Build Your Own Router?&lt;&#x2F;h2&gt;
&lt;p&gt;Commercial routers often come with limitations: locked-down firmware, poor update cycles, and limited customization. Building your own router with NixOS gives you:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Complete control&lt;&#x2F;strong&gt; over your network configuration&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Declarative configuration&lt;&#x2F;strong&gt; that’s version-controlled and reproducible&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Regular security updates&lt;&#x2F;strong&gt; through NixOS’s excellent package management&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Unlimited customization&lt;&#x2F;strong&gt; for advanced networking features&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;This is the first in a series of guides that will take you from zero to a fully-featured NixOS router. For a deep dive into my complete router setup and the journey that led here, check out my &lt;a href=&quot;&#x2F;posts&#x2F;nixos-router-journey&#x2F;&quot;&gt;NixOS router journey post&lt;&#x2F;a&gt;. Let’s start with the basics!&lt;&#x2F;p&gt;
&lt;h2 id=&quot;step-1-choose-your-hardware&quot;&gt;Step 1: Choose Your Hardware&lt;&#x2F;h2&gt;
&lt;p&gt;The beauty of building your own router is flexibility in hardware choice. Here’s what you need:&lt;&#x2F;p&gt;
&lt;h3 id=&quot;minimum-requirements&quot;&gt;Minimum Requirements&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2+ network interfaces&lt;&#x2F;strong&gt; (NICs) - one for WAN, one for LAN&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;x86_64 CPU&lt;&#x2F;strong&gt; - NixOS has excellent x86_64 support&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;4GB+ RAM&lt;&#x2F;strong&gt; - More if you plan to run additional services&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;16GB+ storage&lt;&#x2F;strong&gt; - SSD preferred for reliability&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;recommended-budget-option-intel-n5105-mini-pc&quot;&gt;Recommended Budget Option: Intel N5105 Mini PC&lt;&#x2F;h3&gt;
&lt;p&gt;For around $150, Intel N5105-based mini PCs offer excellent value:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Specifications:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;- CPU: Intel Celeron N5105 (4 cores @ 2.0-2.9 GHz)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;- RAM: 8GB DDR4 (expandable)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;- Storage: 128GB SSD&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;- Network: 4x Intel i226-V 2.5GbE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;- Power: ~10-15W typical consumption&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;alternative-arm-based-sbcs&quot;&gt;Alternative: ARM-based SBCs&lt;&#x2F;h3&gt;
&lt;p&gt;I’ve also used the NanoPi R2S (ARM-based SBC) as a router, and while my &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;tree&#x2F;master&#x2F;hosts&#x2F;r2s&quot;&gt;NixOS configuration still supports it&lt;&#x2F;a&gt;, I don’t recommend it for beginners:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Installation is more complex&lt;&#x2F;strong&gt; - requires building custom images&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Limited performance&lt;&#x2F;strong&gt; - struggles with QoS, monitoring, and multiple services&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Feature trade-offs&lt;&#x2F;strong&gt; - you’ll need to carefully choose which features to enable&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Maintenance overhead&lt;&#x2F;strong&gt; - ARM support in NixOS requires more manual work&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;For a first NixOS router, stick with x86_64 hardware for the best experience.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;where-to-buy&quot;&gt;Where to Buy&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;AliExpress&lt;&#x2F;strong&gt;: Best prices, 2-4 week shipping
&lt;ul&gt;
&lt;li&gt;Search for “N5105 mini PC 4 LAN”&lt;&#x2F;li&gt;
&lt;li&gt;Popular vendors: Topton, CWWK, Beelink&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Amazon&lt;&#x2F;strong&gt;: Faster shipping, slightly higher prices
&lt;ul&gt;
&lt;li&gt;Look for “fanless mini PC firewall”&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;eBay&lt;&#x2F;strong&gt;: Good for used enterprise hardware
&lt;ul&gt;
&lt;li&gt;Search “Dell Optiplex USFF” + USB NIC&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;&#x2F;strong&gt;: Ensure your chosen hardware has Intel or Realtek NICs for best driver support. Avoid obscure chipsets.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;step-2-install-nixos&quot;&gt;Step 2: Install NixOS&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;download-and-prepare-installation-media&quot;&gt;Download and Prepare Installation Media&lt;&#x2F;h3&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# Download the latest NixOS ISO (Graphical installer recommended for beginners)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;wget&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; https:&#x2F;&#x2F;nixos.org&#x2F;download&#x2F;nixos-iso-graphical-24.11.tar.xz&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# Write to USB drive (replace &#x2F;dev&#x2F;sdX with your USB device)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;sudo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; dd if=nixos-24.11.iso of=&#x2F;dev&#x2F;sdX bs=4M status=progress&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;installation-process&quot;&gt;Installation Process&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Boot from USB&lt;&#x2F;strong&gt; and select the graphical installer&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Partition your disk&lt;&#x2F;strong&gt; - a simple single partition with ext4 works fine&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Set up networking&lt;&#x2F;strong&gt; temporarily for the installation&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Create your user&lt;&#x2F;strong&gt; and set passwords&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Generate initial configuration&lt;&#x2F;strong&gt;:&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# This creates &#x2F;etc&#x2F;nixos&#x2F;configuration.nix&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;nixos-generate-config&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; --root&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &#x2F;mnt&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;advanced-installation-method&quot;&gt;Advanced Installation Method&lt;&#x2F;h3&gt;
&lt;p&gt;For advanced users, I use &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;nix-community&#x2F;disko&quot;&gt;disko&lt;&#x2F;a&gt; for declarative disk partitioning and &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;nix-community&#x2F;nix-anywhere&quot;&gt;nix-anywhere&lt;&#x2F;a&gt; for remote installations:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Disko configuration&lt;&#x2F;strong&gt;: &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;blob&#x2F;master&#x2F;hosts&#x2F;router&#x2F;disk-config.nix&quot;&gt;hosts&#x2F;router&#x2F;disk-config.nix&lt;&#x2F;a&gt; - Declaratively defines disk layout&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Remote deployment&lt;&#x2F;strong&gt;: Can install NixOS on a target machine over SSH without physical access&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;This approach allows for:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Reproducible disk partitioning&lt;&#x2F;li&gt;
&lt;li&gt;Automated remote installations&lt;&#x2F;li&gt;
&lt;li&gt;Zero-touch deployments&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;I’ve automated these tasks in my &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;blob&#x2F;master&#x2F;justfile&quot;&gt;justfile&lt;&#x2F;a&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# Install router configuration on new hardware&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;just&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; install router&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 192.168.1.100&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# Deploy updates to existing router&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;just&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; deploy router&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# List network interfaces for configuration&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;just&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; router-interfaces router.local&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;See my &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos#deployment&quot;&gt;deployment documentation&lt;&#x2F;a&gt; for detailed examples.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;identify-your-network-interfaces&quot;&gt;Identify Your Network Interfaces&lt;&#x2F;h3&gt;
&lt;p&gt;Before configuring the router, identify your NICs:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# List all network interfaces&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;ip&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; link show&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# You should see something like:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# enp1s0: WAN interface (connect to modem)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# enp2s0: LAN interface (connect to switch&#x2F;devices)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;step-3-create-minimal-router-configuration&quot;&gt;Step 3: Create Minimal Router Configuration&lt;&#x2F;h2&gt;
&lt;p&gt;Replace your &lt;code&gt;&#x2F;etc&#x2F;nixos&#x2F;configuration.nix&lt;&#x2F;code&gt; with this minimal router setup. We’ll use dnsmasq which provides both DHCP and DNS services in a single, lightweight package:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; config&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; pkgs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;, ...&lt;&#x2F;span&gt;&lt;span&gt; }:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  imports&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; .&#x2F;hardware-configuration.nix&lt;&#x2F;span&gt;&lt;span&gt; ];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # Basic system configuration&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  boot&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;loader&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;systemd-boot&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  boot&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;loader&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;efi&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;canTouchEfiVariables&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  networking&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;hostName&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;nixos-router&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # Enable IP forwarding&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  boot&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;kernel&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;sysctl&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;    &amp;quot;net.ipv4.ip_forward&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 1&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;    &amp;quot;net.ipv6.conf.all.forwarding&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 1&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # Configure network interfaces&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  networking&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;    # Disable NetworkManager for manual configuration&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    networkmanager&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; false&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    useDHCP&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; false&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;    # WAN interface (adjust interface name as needed)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    interfaces&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;enp1s0&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      useDHCP&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # Get IP from ISP&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;    # LAN interface&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    interfaces&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;enp2s0&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      ipv4&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;addresses&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        address&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;192.168.1.1&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        prefixLength&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 24&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      }];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;    # NAT for internet sharing&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    nat&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      externalInterface&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;enp1s0&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # WAN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      internalInterfaces&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;enp2s0&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt; ];&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # LAN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;    # Simple firewall rules&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    firewall&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;      # Allow SSH from LAN only&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      extraCommands&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;        iptables -A nixos-fw -p tcp --dport 22 -s 192.168.1.0&#x2F;24 -j ACCEPT&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      &amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # DHCP and DNS server (dnsmasq provides both)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  services&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;dnsmasq&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    settings&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;      # DHCP Configuration&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      dhcp-range&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;192.168.1.100,192.168.1.200,12h&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt; ];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      interface&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;enp2s0&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;      # DNS Configuration  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      server&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;8.8.8.8&amp;quot; &amp;quot;8.8.4.4&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt; ];&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # Upstream DNS servers&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;      # Don&amp;#39;t use &#x2F;etc&#x2F;hosts&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      no-hosts&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;      # DHCP Options&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      dhcp-option&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;        &amp;quot;option:router,192.168.1.1&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;        &amp;quot;option:dns-server,192.168.1.1&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # Use router as DNS server&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      ];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # Basic services&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  services&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;openssh&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # User configuration&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  users&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;users&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;admin&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    isNormalUser&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    extraGroups&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;wheel&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt; ];&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # Enable &amp;#39;sudo&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;    # Don&amp;#39;t forget to set a password with &amp;#39;passwd admin&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  system&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;stateVersion&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;24.11&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;apply-the-configuration&quot;&gt;Apply the Configuration&lt;&#x2F;h3&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# Switch to the new configuration&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;sudo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; nixos-rebuild switch&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# Check service status&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;systemctl&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; status dnsmasq&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;systemctl&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; status nftables&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;step-4-test-your-router&quot;&gt;Step 4: Test Your Router&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;verify-wan-connectivity&quot;&gt;Verify WAN Connectivity&lt;&#x2F;h3&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# Check if router has internet&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;ping&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; -c 3&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; google.com&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# Check WAN IP address&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;ip&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; addr show enp1s0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;test-lan-connectivity&quot;&gt;Test LAN Connectivity&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Connect a device&lt;&#x2F;strong&gt; to your LAN port (enp2s0)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Verify DHCP&lt;&#x2F;strong&gt; - the device should get an IP in 192.168.1.100-200 range&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Test internet access&lt;&#x2F;strong&gt; from the connected device&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;basic-troubleshooting&quot;&gt;Basic Troubleshooting&lt;&#x2F;h3&gt;
&lt;p&gt;If things aren’t working:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# Check interface status&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;ip&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; link show&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;ip&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; addr show&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# Monitor DHCP leases&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;journalctl&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; -u&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; dnsmasq&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; -f&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# View active DHCP leases&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;cat&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &#x2F;var&#x2F;lib&#x2F;dnsmasq&#x2F;dnsmasq.leases&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# Check firewall rules&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;sudo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; nft list ruleset&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# Watch packet flow&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;&quot;&gt;sudo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; tcpdump&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; -i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; enp2s0&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; -n&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;common-issues-and-fixes&quot;&gt;Common Issues and Fixes&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;No internet on LAN devices?&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Verify NAT is working: &lt;code&gt;sudo iptables -t nat -L -v -n&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Check IP forwarding: &lt;code&gt;sysctl net.ipv4.ip_forward&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;DHCP not working?&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Ensure dnsmasq is running: &lt;code&gt;systemctl status dnsmasq&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Check logs: &lt;code&gt;journalctl -u dnsmasq&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;View leases: &lt;code&gt;cat &#x2F;var&#x2F;lib&#x2F;dnsmasq&#x2F;dnsmasq.leases&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;Can’t access router via SSH?&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Verify firewall allows SSH from LAN: &lt;code&gt;sudo iptables -L -v -n&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;what-s-next&quot;&gt;What’s Next?&lt;&#x2F;h2&gt;
&lt;p&gt;Congratulations! You now have a working NixOS router. It’s basic, but it’s yours. Continue with the series to add more features:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;&#x2F;posts&#x2F;nixos-router-blog-post-2-testing&quot;&gt;Part 2: Testing Your Configuration&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; - Write comprehensive tests to ensure reliability and catch errors before deployment&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;&#x2F;posts&#x2F;nixos-router-blog-post-3-monitoring&quot;&gt;Part 3: Per-Client Monitoring&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; - Track bandwidth usage per device with Prometheus and Grafana&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;For a complete overview of the entire router build including advanced features like QoS, VLANs, and hardware selection, check out my &lt;strong&gt;&lt;a href=&quot;&#x2F;posts&#x2F;nixos-router-journey&quot;&gt;NixOS router journey&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; post.&lt;&#x2F;p&gt;
&lt;p&gt;The complete configuration for this series is available in my &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&quot;&gt;nixos-config repository&lt;&#x2F;a&gt;. Feel free to explore and adapt it to your needs.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;resources&quot;&gt;Resources&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;nixos.org&#x2F;manual&#x2F;nixos&#x2F;stable&#x2F;#sec-networking&quot;&gt;NixOS Manual - Networking&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;tree&#x2F;master&#x2F;hosts&#x2F;router&quot;&gt;My Router Configuration&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;discourse.nixos.org&#x2F;c&#x2F;help&#x2F;networking&#x2F;23&quot;&gt;NixOS Discourse - Networking Topics&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Have questions or run into issues? Feel free to open an issue on the &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;issues&quot;&gt;repository&lt;&#x2F;a&gt; or reach out on the NixOS forums!&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Building a 2.5Gbps Home Router with NixOS</title>
          <pubDate>Wed, 16 Jul 2025 00:00:00 +0000</pubDate>
          <author>Alex Rosenfeld</author>
          <link>https://blog.arsfeld.dev/posts/2025/07/16/nixos-router-journey/</link>
          <guid>https://blog.arsfeld.dev/posts/2025/07/16/nixos-router-journey/</guid>
          <description xml:base="https://blog.arsfeld.dev/posts/2025/07/16/nixos-router-journey/">&lt;p&gt;&lt;img src=&quot;&#x2F;images&#x2F;nixos-router-hero.png&quot; alt=&quot;NixOS Router Setup&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;&#x2F;strong&gt;: Built a NixOS-based router on a $150 Intel N5105 mini PC that handles 2.5Gbps fiber with advanced QoS, monitoring, and declarative configuration. After experiences with pfSense and OPNsense, NixOS with comprehensive testing provides the control and reliability I was seeking. Check out the &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;tree&#x2F;0ef5f6f7f22809a16f9f742d8418dd11cd0ea04e&#x2F;hosts&#x2F;router&quot;&gt;full router configuration&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;motivation&quot;&gt;Motivation&lt;&#x2F;h2&gt;
&lt;p&gt;Consumer routers work fine for most people. They’re reliable, easy to set up, and just work. But if you’re reading this, you’re probably not “most people.”&lt;&#x2F;p&gt;
&lt;p&gt;For me, the limitation wasn’t about failures or frustration - it was about curiosity. I wanted to:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;See real traffic data, not just pretty graphs&lt;&#x2F;li&gt;
&lt;li&gt;Add custom DNS rules for my homelab&lt;&#x2F;li&gt;
&lt;li&gt;Experiment with different QoS algorithms&lt;&#x2F;li&gt;
&lt;li&gt;Actually understand what my network was doing&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Consumer routers are black boxes. Even the “advanced” settings are usually just basic toggles. Want to try a different packet scheduler? Add custom monitoring? Write your own firewall rules? Good luck with that.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;evolution-of-my-router-setup&quot;&gt;Evolution of My Router Setup&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;first-attempt-with-nixos&quot;&gt;First Attempt with NixOS&lt;&#x2F;h3&gt;
&lt;p&gt;Three years ago, I discovered NixOS and was attracted to its declarative configuration approach. Version control for system configuration seemed ideal for a router.&lt;&#x2F;p&gt;
&lt;p&gt;The concept was appealing:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;networking&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;firewall&lt;&#x2F;span&gt;&lt;span style=&quot;color: #000000;background-color: #FFFFFF;&quot;&gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # Just declare what I want!&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;However, I encountered challenges with UPnP implementation. Gaming consoles reported strict NAT types, and port forwarding proved difficult to configure properly. After multiple connectivity issues, I decided to switch to a more established solution.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;experience-with-pfsense&quot;&gt;Experience with pfSense&lt;&#x2F;h3&gt;
&lt;p&gt;pfSense provided a stable solution with its web UI, extensive package ecosystem, and strong community support. It served reliably for several years.&lt;&#x2F;p&gt;
&lt;p&gt;However, a configuration corruption incident after an update highlighted a critical limitation: without declarative configuration or version control, recovering the exact router state required manual reconstruction from memory.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;transition-to-opnsense&quot;&gt;Transition to OPNsense&lt;&#x2F;h3&gt;
&lt;p&gt;Following the pfSense incident, I migrated to OPNsense. It’s well-engineered software with good stability and maintenance.&lt;&#x2F;p&gt;
&lt;p&gt;While functional, the GUI-based configuration lacked the version control and reproducibility benefits of declarative systems. The contrast with Infrastructure as Code principles was apparent.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;return-to-nixos-with-better-tooling&quot;&gt;Return to NixOS with Better Tooling&lt;&#x2F;h3&gt;
&lt;p&gt;In 2025, with improved tooling including AI coding assistants, I revisited NixOS for routing. The key difference was developing a comprehensive test suite.&lt;&#x2F;p&gt;
&lt;p&gt;The test coverage includes:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Basic connectivity validation&lt;&#x2F;li&gt;
&lt;li&gt;DNS resolution verification&lt;&#x2F;li&gt;
&lt;li&gt;Firewall rule testing&lt;&#x2F;li&gt;
&lt;li&gt;UPnP functionality checks&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;See the &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;blob&#x2F;0ef5f6f7f22809a16f9f742d8418dd11cd0ea04e&#x2F;tests&#x2F;router-test.nix&quot;&gt;complete test suite&lt;&#x2F;a&gt; that validates the entire configuration.&lt;&#x2F;p&gt;
&lt;p&gt;This testing infrastructure enables confident deployment. Changes are validated before reaching production, ensuring network stability.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-nixos-for-a-router&quot;&gt;Why NixOS for a Router?&lt;&#x2F;h2&gt;
&lt;p&gt;With proper testing in place, all the NixOS benefits I originally wanted became reality:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Declarative Configuration&lt;&#x2F;strong&gt;: My entire router setup is in git. Every firewall rule, every service, every setting - tracked, reviewable, and reproducible.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# This is my actual DNS configuration&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;services&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;blocky&lt;&#x2F;span&gt;&lt;span style=&quot;color: #000000;background-color: #FFFFFF;&quot;&gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  settings&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    upstream&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;default&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      &amp;quot;1.1.1.1&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      &amp;quot;8.8.8.8&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    blocking&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;blackLists&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      ads&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;&amp;quot;https:&#x2F;&#x2F;someonewhocares.org&#x2F;hosts&#x2F;zero&#x2F;hosts&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    customDNS&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;mapping&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      &amp;quot;router.lan&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;192.168.10.1&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;      &amp;quot;nas.lan&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;192.168.10.10&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;See the &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;tree&#x2F;0ef5f6f7f22809a16f9f742d8418dd11cd0ea04e&#x2F;hosts&#x2F;router&#x2F;services&#x2F;dns.nix&quot;&gt;full DNS configuration&lt;&#x2F;a&gt; for complete setup including conditional forwarding for Tailscale.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Atomic Updates&lt;&#x2F;strong&gt;: Updates either work completely or roll back. No more half-applied configs that break networking.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Reproducible Builds&lt;&#x2F;strong&gt;: I can build the exact same router configuration on a test VM, verify it works, then deploy to production.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Test-Driven Development&lt;&#x2F;strong&gt;: Comprehensive testing enables reliable changes:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# Simplified example from my test suite&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;testScript&lt;&#x2F;span&gt;&lt;span style=&quot;color: #000000;background-color: #FFFFFF;&quot;&gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  router.wait_for_unit(&amp;quot;upnp&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  client.succeed(&amp;quot;upnpc -a 192.168.10.2 8080 8080 TCP&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;  router.succeed(&amp;quot;nft list ruleset | grep 8080&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;&amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;considerations-for-custom-solutions&quot;&gt;Considerations for Custom Solutions&lt;&#x2F;h2&gt;
&lt;p&gt;Important considerations for custom router solutions:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Full responsibility&lt;&#x2F;strong&gt;: No vendor support available. Troubleshooting relies on system logs and personal expertise.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Time constraints&lt;&#x2F;strong&gt;: Network issues require immediate resolution, especially in households dependent on connectivity.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Maintenance overhead&lt;&#x2F;strong&gt;: Security updates, monitoring, and system health are self-managed.&lt;&#x2F;p&gt;
&lt;p&gt;The benefit is complete understanding and control of the network stack. Issues can be diagnosed and features added as needed.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-final-setup&quot;&gt;The Final Setup&lt;&#x2F;h2&gt;
&lt;p&gt;Here’s what I ended up with:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Hardware&lt;&#x2F;strong&gt;: A $150 Intel N5105 mini PC from &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;www.aliexpress.com&#x2F;item&#x2F;1005004822012472.html&quot;&gt;AliExpress&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;4-core Celeron with AES-NI (plenty for routing)&lt;&#x2F;li&gt;
&lt;li&gt;4x Intel I226-V 2.5GbE NICs&lt;&#x2F;li&gt;
&lt;li&gt;8GB RAM, 16GB NVMe&lt;&#x2F;li&gt;
&lt;li&gt;Fanless, ~15W power draw&lt;&#x2F;li&gt;
&lt;li&gt;Standard x86 architecture for broad compatibility&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;Software Stack&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;NixOS for the base system&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;tree&#x2F;0ef5f6f7f22809a16f9f742d8418dd11cd0ea04e&#x2F;hosts&#x2F;router&#x2F;network.nix&quot;&gt;nftables for firewall&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;tree&#x2F;0ef5f6f7f22809a16f9f742d8418dd11cd0ea04e&#x2F;hosts&#x2F;router&#x2F;services&#x2F;dns.nix&quot;&gt;Blocky for DNS with ad blocking&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;tree&#x2F;0ef5f6f7f22809a16f9f742d8418dd11cd0ea04e&#x2F;hosts&#x2F;router&#x2F;services&#x2F;upnp.nix&quot;&gt;miniupnpd for UPnP&#x2F;NAT-PMP&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;tree&#x2F;0ef5f6f7f22809a16f9f742d8418dd11cd0ea04e&#x2F;hosts&#x2F;router&#x2F;traffic-shaping.nix&quot;&gt;CAKE for QoS and bufferbloat mitigation&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;tree&#x2F;0ef5f6f7f22809a16f9f742d8418dd11cd0ea04e&#x2F;hosts&#x2F;router&#x2F;services&#x2F;monitoring.nix&quot;&gt;Prometheus + Grafana for monitoring&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Tailscale for remote access&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;Performance&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;2.5Gbps symmetric with &amp;lt;1ms added latency&lt;&#x2F;li&gt;
&lt;li&gt;Full IDS&#x2F;IPS at line rate&lt;&#x2F;li&gt;
&lt;li&gt;50+ days uptime (only reboots for kernel updates)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;monitoring-dashboard&quot;&gt;Monitoring Dashboard&lt;&#x2F;h2&gt;
&lt;p&gt;With Prometheus and Grafana, I have complete visibility into my network’s performance:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;images&#x2F;grafana.png&quot; alt=&quot;Grafana Router Dashboard&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The dashboard shows real-time metrics including CPU usage, memory utilization, disk I&#x2F;O, network traffic per interface, DNS query statistics, client bandwidth analysis, and QoS performance metrics.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-s-next&quot;&gt;What’s Next?&lt;&#x2F;h2&gt;
&lt;p&gt;If you’re interested in building your own NixOS router, I’ve created a tutorial series that walks you through the process:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;&#x2F;posts&#x2F;nixos-router-getting-started&quot;&gt;Part 1: Getting Started&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; - Hardware selection, basic installation, and minimal router configuration&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;&#x2F;posts&#x2F;nixos-router-blog-post-2-testing&quot;&gt;Part 2: Testing Your Configuration&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; - Write comprehensive tests to ensure reliability&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;&#x2F;posts&#x2F;nixos-router-blog-post-3-monitoring&quot;&gt;Part 3: Per-Client Monitoring&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; - Track bandwidth usage per device with Prometheus and Grafana&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The full configuration demonstrates many advanced features including comprehensive testing, network architecture with bridging and VLANs, DNS with ad blocking, QoS with CAKE for bufferbloat mitigation, monitoring with Prometheus and Grafana, VPN integration, and dynamic port forwarding.&lt;&#x2F;p&gt;
&lt;p&gt;The full configuration is available on &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;tree&#x2F;0ef5f6f7f22809a16f9f742d8418dd11cd0ea04e&#x2F;hosts&#x2F;router&quot;&gt;GitHub&lt;&#x2F;a&gt;. The &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;tree&#x2F;0ef5f6f7f22809a16f9f742d8418dd11cd0ea04e&#x2F;hosts&#x2F;router&#x2F;configuration.nix&quot;&gt;main configuration file&lt;&#x2F;a&gt; ties everything together. For details on how I manage NixOS across multiple hosts in my homelab, see my post on &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;blog.arsfeld.dev&#x2F;posts&#x2F;managing-homelab-with-nixos&#x2F;&quot;&gt;Managing a Homelab with NixOS&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;&#x2F;h2&gt;
&lt;p&gt;Building a custom router with NixOS requires significant technical investment. For my use case, the benefits of declarative configuration, comprehensive testing, and complete control justify the effort.&lt;&#x2F;p&gt;
&lt;p&gt;The combination of &lt;code&gt;nixos-rebuild switch&lt;&#x2F;code&gt; with passing tests provides confidence in network changes that GUI-based solutions cannot match.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Simplifying NixOS Configurations with Reusable Modules</title>
          <pubDate>Wed, 11 Jun 2025 00:00:00 +0000</pubDate>
          <author>Alex Rosenfeld</author>
          <link>https://blog.arsfeld.dev/posts/2025/06/11/constellation-pattern/</link>
          <guid>https://blog.arsfeld.dev/posts/2025/06/11/constellation-pattern/</guid>
          <description xml:base="https://blog.arsfeld.dev/posts/2025/06/11/constellation-pattern/">&lt;p&gt;&lt;img src=&quot;&#x2F;images&#x2F;constellation-pattern-hero.png&quot; alt=&quot;NixOS Constellation Pattern - A network of connected nodes representing modular infrastructure&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Managing multiple NixOS machines quickly becomes unwieldy when you copy-paste configurations between hosts. You end up with duplicated code, inconsistent settings, and the nightmare of keeping everything in sync. After running a fleet of 10+ NixOS machines ranging from ARM routers to x86 servers, I developed what I call the “Constellation Pattern” - a modular system that eliminates configuration duplication while maintaining the flexibility to customize each host.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;&#x2F;strong&gt;: All code examples in this post are from my real production NixOS configuration, available at &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&quot;&gt;github.com&#x2F;arsfeld&#x2F;nixos&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;the-problem-with-traditional-nixos-multi-host-management&quot;&gt;The Problem with Traditional NixOS Multi-Host Management&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;images&#x2F;configuration-drift-problems.png&quot; alt=&quot;Configuration drift and copy-paste problems in traditional NixOS setups&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Most NixOS configurations start simple. You have one machine, one &lt;code&gt;configuration.nix&lt;&#x2F;code&gt;, and life is good. But as you add more hosts, you face several challenges:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Configuration Drift&lt;&#x2F;strong&gt;: Each host accumulates unique tweaks, making it impossible to apply consistent updates across your fleet.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Copy-Paste Hell&lt;&#x2F;strong&gt;: You copy working configurations between machines, creating maintenance nightmares when you need to update common settings.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;All-or-Nothing Modules&lt;&#x2F;strong&gt;: Standard NixOS modules are often too rigid - you can’t easily enable just the parts you need on different hosts.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Dependency Management&lt;&#x2F;strong&gt;: Services on one host might depend on services running on another host, but there’s no clean way to express these relationships.&lt;&#x2F;p&gt;
&lt;p&gt;Here’s what a typical problematic setup looks like:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# hosts&#x2F;server1&#x2F;configuration.nix&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; pkgs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;, ...&lt;&#x2F;span&gt;&lt;span&gt; }: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # 200 lines of common configuration&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  services&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;tailscale&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  services&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;openssh&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # ... lots of repeated configuration&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # Server-specific stuff mixed in&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  services&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;postgresql&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# hosts&#x2F;server2&#x2F;configuration.nix  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; pkgs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;, ...&lt;&#x2F;span&gt;&lt;span&gt; }: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # Same 200 lines copied and pasted&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  services&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;tailscale&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  services&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;openssh&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # ... same repeated configuration&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # Different server-specific stuff&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  services&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;nginx&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;enter-the-constellation-pattern&quot;&gt;Enter the Constellation Pattern&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;images&#x2F;constellation-architecture.png&quot; alt=&quot;Constellation pattern architecture showing modular, composable infrastructure&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The Constellation Pattern solves this by creating &lt;strong&gt;opt-in feature modules&lt;&#x2F;strong&gt; that can be selectively enabled on any host. Instead of copying configuration, you compose your hosts from a set of reusable, well-tested modules.&lt;&#x2F;p&gt;
&lt;p&gt;The pattern consists of:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Base Module (&lt;code&gt;constellation.common&lt;&#x2F;code&gt;)&lt;&#x2F;strong&gt;: Common configuration that nearly every host needs&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Feature Modules&lt;&#x2F;strong&gt;: Specialized modules for specific capabilities (media, backup, services, etc.)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Host Configurations&lt;&#x2F;strong&gt;: Minimal files that just enable the modules they need&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Here’s how my hosts are now configured:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# hosts&#x2F;storage&#x2F;configuration.nix&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  constellation&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;backup&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  constellation&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;services&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  constellation&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;media&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  constellation&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;podman&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # Host-specific configuration&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  networking&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;hostName&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;storage&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # ... minimal host-specific settings&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# hosts&#x2F;cloud&#x2F;configuration.nix  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  constellation&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;podman&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  constellation&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;backup&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  constellation&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;services&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  constellation&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;media&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  constellation&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;supabase&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # Host-specific configuration&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  networking&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;hostName&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;cloud&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  nixpkgs&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;hostPlatform&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;aarch64-linux&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Clean, declarative, and immediately obvious what each host provides.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;anatomy-of-a-constellation-module&quot;&gt;Anatomy of a Constellation Module&lt;&#x2F;h2&gt;
&lt;p&gt;Let’s examine the &lt;code&gt;constellation.common&lt;&#x2F;code&gt; module to understand the pattern:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# modules&#x2F;constellation&#x2F;common.nix&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# Source: https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;blob&#x2F;master&#x2F;modules&#x2F;constellation&#x2F;common.nix&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;  inputs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;  config&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;  pkgs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;  lib&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;  ...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;with&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; lib&lt;&#x2F;span&gt;&lt;span&gt;; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  options&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;constellation&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;common&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; mkOption&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      type&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; types&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;bool&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      description&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;Enable common configuration&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      default&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # This is key - enabled by default&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  config&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; lib&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;mkIf config&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;constellation&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;common&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;enable&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;    # Nix configuration that every host needs&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    nix&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      settings&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        experimental-features&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;nix-command flakes&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        auto-optimise-store&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        substituters&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;          &amp;quot;https:&#x2F;&#x2F;nix-community.cachix.org?priority=41&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;          &amp;quot;https:&#x2F;&#x2F;fly-attic.fly.dev&#x2F;system&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;          # ... more caches&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        ];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        trusted-public-keys&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;          &amp;quot;nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+&#x2F;rkCWyvRCYg3Fs=&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;          # ... corresponding keys&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        ];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;    # Essential services every host needs&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    services&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;tailscale&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    services&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;openssh&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    services&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;avahi&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;    # Common packages&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    environment&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;systemPackages&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; = with&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; pkgs&lt;&#x2F;span&gt;&lt;span&gt;; [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;      git htop tmux wget&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;      # ... essential tools&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;    # Security and performance defaults&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    networking&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;firewall&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;trustedInterfaces&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;&amp;quot;tailscale0&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    zramSwap&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    nix&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;gc&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      automatic&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      dates&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;Sat *-*-* 03:15:00&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      options&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;--delete-older-than 30d&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Key principles:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Single Responsibility&lt;&#x2F;strong&gt;: Each module has a clear, focused purpose&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Sensible Defaults&lt;&#x2F;strong&gt;: The module works out of the box with minimal configuration&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Override Capability&lt;&#x2F;strong&gt;: Hosts can still override specific settings when needed&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Dependency Declaration&lt;&#x2F;strong&gt;: Modules can reference other constellation modules&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;real-world-example-mixing-containers-and-native-services&quot;&gt;Real-World Example: Mixing Containers and Native Services&lt;&#x2F;h2&gt;
&lt;p&gt;The true power of the Constellation Pattern emerges when modules orchestrate both container-based and native NixOS services seamlessly. This hybrid approach lets you choose the best deployment method for each service while maintaining a unified gateway and service discovery system.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;why-the-hybrid-approach&quot;&gt;Why the Hybrid Approach?&lt;&#x2F;h3&gt;
&lt;p&gt;My &lt;code&gt;constellation.media&lt;&#x2F;code&gt; module runs the *arr stack (Sonarr, Radarr, Prowlarr) as containers because:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;NixOS modules for these services weren’t updated frequently enough&lt;&#x2F;li&gt;
&lt;li&gt;Container images provide consistent configuration across updates&lt;&#x2F;li&gt;
&lt;li&gt;The *arr ecosystem expects certain behaviors that containers handle better&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Meanwhile, services deeply integrated with NixOS (like PostgreSQL databases, Grafana, or Gitea) run as native services for better system integration.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-media-constellation-in-action&quot;&gt;The Media Constellation in Action&lt;&#x2F;h3&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# modules&#x2F;constellation&#x2F;media.nix&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# Container-based services with automatic deployment&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  config&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; lib&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;mkIf cfg&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;enable&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    media&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;containers&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; = let&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      storageServices&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;        # The full *arr stack as containers&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        prowlarr&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt; listenPort&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 9696&lt;&#x2F;span&gt;&lt;span&gt;; };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        sonarr&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; { &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;          listenPort&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 8989&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;          mediaVolumes&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # Automatically mounts media directories&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        radarr&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; { &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;          listenPort&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 7878&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;          mediaVolumes&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;        # Media servers with hardware acceleration&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;        plex&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;          mediaVolumes&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;          network&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;host&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;          devices&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;&amp;quot;&#x2F;dev&#x2F;dri:&#x2F;dev&#x2F;dri&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;];&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # Intel GPU passthrough&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;    in&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;      lib&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;mapAttrs&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;addHost&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;storage&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; storageServices&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# modules&#x2F;constellation&#x2F;services.nix&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# Native NixOS services registered for gateway routing&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;let&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;  services&lt;&#x2F;span&gt;&lt;span style=&quot;color: #000000;background-color: #FFFFFF;&quot;&gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    storage&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;      # Native services get simple port registration&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      grafana&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 3010&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      gitea&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 3001&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      postgresql&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 5432&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;      # Container services just need their exposed port&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      jellyfin&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 8096&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      immich&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 15777&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      sonarr&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 8989&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;      radarr&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 7878&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  }&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;unified-gateway-system&quot;&gt;Unified Gateway System&lt;&#x2F;h3&gt;
&lt;p&gt;Both container and native services register with the same gateway system:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# All services - container or native - get automatic:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# - Reverse proxy configuration (https:&#x2F;&#x2F;service.domain.com)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# - Service discovery across hosts&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# - Authentication rules&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# - Health monitoring&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;media&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;gateway&lt;&#x2F;span&gt;&lt;span style=&quot;color: #000000;background-color: #FFFFFF;&quot;&gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  services&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; generateServices services&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # Works for both types!&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The beauty of this approach:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Use containers when they make sense&lt;&#x2F;strong&gt;: For services with complex dependencies or frequent updates&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Use native when better&lt;&#x2F;strong&gt;: For NixOS-integrated services or those needing deep system access&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Same gateway for everything&lt;&#x2F;strong&gt;: Users don’t know or care how services are deployed&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Flexible migration&lt;&#x2F;strong&gt;: Start with native, move to containers (or vice versa) without changing the gateway configuration&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;This flexibility is where the constellation system truly shines - it doesn’t force you into one deployment model but lets you choose the best tool for each job while maintaining a cohesive system.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;benefits-of-the-constellation-pattern&quot;&gt;Benefits of the Constellation Pattern&lt;&#x2F;h2&gt;
&lt;p&gt;After running this pattern for over a year across 10+ hosts, the benefits are substantial:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Consistency&lt;&#x2F;strong&gt;: Every host gets the same base configuration, eliminating configuration drift.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Maintainability&lt;&#x2F;strong&gt;: Updating all hosts is as simple as updating a single module and redeploying.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Composability&lt;&#x2F;strong&gt;: New hosts are trivial to create - just enable the modules you need.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Testing&lt;&#x2F;strong&gt;: You can test new configurations on a single host before rolling out to the fleet.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Documentation&lt;&#x2F;strong&gt;: The module structure serves as living documentation of your infrastructure capabilities.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Flexibility&lt;&#x2F;strong&gt;: Hosts can still override specific settings when needed, maintaining NixOS’s flexibility.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;comparison-with-alternatives&quot;&gt;Comparison with Alternatives&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;vs. NixOps&lt;&#x2F;strong&gt;: More lightweight and doesn’t require a separate deployment tool. Works with any deployment method (deploy-rs, nixos-rebuild, etc.).&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;vs. Terraform&#x2F;Ansible&lt;&#x2F;strong&gt;: Purely declarative with NixOS’s atomic rollback capabilities. No imperative state management.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;vs. Kubernetes&lt;&#x2F;strong&gt;: Simpler for homelab scale. No YAML hell, built-in secret management, and works on bare metal.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;vs. Docker Compose&lt;&#x2F;strong&gt;: Better hardware integration, atomic updates, and cross-host service discovery built-in.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;how-it-all-ties-together-automatic-module-loading-with-haumea&quot;&gt;How It All Ties Together: Automatic Module Loading with Haumea&lt;&#x2F;h2&gt;
&lt;p&gt;The magic that makes the Constellation Pattern truly effortless is &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;nix-community&#x2F;haumea&quot;&gt;haumea&lt;&#x2F;a&gt;, a library that automatically loads all files as NixOS modules. Instead of manually importing each module file, haumea discovers and loads them for you:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# flake.nix&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# Source: https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&#x2F;blob&#x2F;master&#x2F;flake.nix&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;let&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  modules&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; inputs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;haumea&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;lib&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;load&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    src&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; .&#x2F;modules&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    loader&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; inputs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;haumea&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;lib&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;loaders&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;path&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;in&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;  getAllValues modules&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;  # Flattens nested module structure&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This means:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Zero Import Boilerplate&lt;&#x2F;strong&gt;: Drop a new &lt;code&gt;.nix&lt;&#x2F;code&gt; file in &lt;code&gt;modules&#x2F;&lt;&#x2F;code&gt; and it’s automatically available&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Nested Organization&lt;&#x2F;strong&gt;: Create subdirectories like &lt;code&gt;constellation&#x2F;&lt;&#x2F;code&gt; for logical grouping&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Instant Recognition&lt;&#x2F;strong&gt;: New constellation modules are immediately available to all hosts&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Clean Flake&lt;&#x2F;strong&gt;: Your &lt;code&gt;flake.nix&lt;&#x2F;code&gt; stays minimal and focused&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Combined with the constellation pattern, this creates a self-organizing module system where adding new capabilities is as simple as creating a file in the right directory.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;advanced-patterns-a-glimpse-into-the-future&quot;&gt;Advanced Patterns: A Glimpse into the Future&lt;&#x2F;h2&gt;
&lt;p&gt;The Constellation Pattern enables sophisticated infrastructure patterns that would be complex to implement otherwise. Multi-host service meshes, automatic service discovery, hardware-aware deployments, and declarative secret distribution all become straightforward.&lt;&#x2F;p&gt;
&lt;p&gt;In a future post, we’ll explore these advanced patterns in detail, showing how constellation modules can orchestrate complex multi-host deployments, handle cross-host dependencies, and create self-healing infrastructure - all while maintaining the simplicity of enabling a single option.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;&#x2F;h2&gt;
&lt;p&gt;The Constellation Pattern has transformed how I manage my self-hosted infrastructure. What started as a mess of copy-pasted configurations is now a clean, maintainable system that scales from a single Raspberry Pi to a full server fleet. By combining opt-in modules with automatic loading via haumea, the pattern achieves both flexibility and simplicity - the holy grail of infrastructure management.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Managing a Multi-Host Homelab with NixOS</title>
          <pubDate>Tue, 10 Jun 2025 00:00:00 +0000</pubDate>
          <author>Alex Rosenfeld</author>
          <link>https://blog.arsfeld.dev/posts/2025/06/10/managing-homelab-with-nixos/</link>
          <guid>https://blog.arsfeld.dev/posts/2025/06/10/managing-homelab-with-nixos/</guid>
          <description xml:base="https://blog.arsfeld.dev/posts/2025/06/10/managing-homelab-with-nixos/">&lt;p&gt;&lt;img src=&quot;&#x2F;images&#x2F;nixos-homelab-hero.png&quot; alt=&quot;Modern NixOS homelab infrastructure with storage and cloud servers connected by network flows&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;After years of managing services across multiple Ubuntu servers, I finally hit my breaking point. Docker Compose files scattered everywhere, manual package updates breaking production services at 2 AM, and the constant fear of “did I document how I set this up?” It was time for a change. Enter NixOS - a declarative Linux distribution that transformed my homelab from a house of cards into a reproducible, version-controlled infrastructure.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-ubuntu-problem&quot;&gt;The Ubuntu Problem&lt;&#x2F;h2&gt;
&lt;p&gt;Picture this: You’re running 30+ services across multiple hosts. Your main server has Jellyfin, the *arr stack, databases, monitoring tools. Your cloud VPS runs your public-facing services. Each service has its own quirks:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Docker Compose files that reference specific versions you forgot to pin&lt;&#x2F;li&gt;
&lt;li&gt;System packages installed via apt that conflict with each other&lt;&#x2F;li&gt;
&lt;li&gt;Configuration files edited in-place that you &lt;em&gt;definitely&lt;&#x2F;em&gt; backed up (right?)&lt;&#x2F;li&gt;
&lt;li&gt;That one service that requires a specific kernel module you compiled 6 months ago&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The breaking point came when a routine &lt;code&gt;apt upgrade&lt;&#x2F;code&gt; on my main server broke PostgreSQL, which cascaded into breaking Gitea, which meant I couldn’t access my infrastructure documentation. While fixing it at 3 AM, I realized I was solving the wrong problem. The issue wasn’t the broken package - it was the entire approach.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;images&#x2F;ubuntu-to-nixos-transformation.png&quot; alt=&quot;Transformation from chaotic Ubuntu server setup to clean organized NixOS infrastructure&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;enter-nixos-infrastructure-as-code-but-actually&quot;&gt;Enter NixOS: Infrastructure as Code, But Actually&lt;&#x2F;h2&gt;
&lt;p&gt;NixOS takes a radically different approach. Instead of imperatively installing packages and editing configs, you declare your entire system configuration in Nix files. Want PostgreSQL 15 with specific settings? Here’s your entire “installation process”:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;nix&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;services&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;postgresql&lt;&#x2F;span&gt;&lt;span style=&quot;color: #000000;background-color: #FFFFFF;&quot;&gt; &lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  enable&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  package&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt; pkgs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFB86C;font-style: italic;&quot;&gt;postgresql_15&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;  settings&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    shared_buffers&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; &amp;quot;256MB&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #50FA7B;font-style: italic;&quot;&gt;    max_connections&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt; 100&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF5555;font-style: italic;text-decoration: underline;&quot;&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Deploy it with &lt;code&gt;nixos-rebuild switch&lt;&#x2F;code&gt;. Made a mistake? Roll back instantly with &lt;code&gt;nixos-rebuild switch --rollback&lt;&#x2F;code&gt;. Every change is atomic, reproducible, and version controlled.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;my-infrastructure-evolution&quot;&gt;My Infrastructure Evolution&lt;&#x2F;h2&gt;
&lt;p&gt;Today, my homelab runs entirely on NixOS, managed through a &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&quot;&gt;single Git repository&lt;&#x2F;a&gt;. In fact, the blog you’re reading right now is hosted on this very infrastructure, built and deployed through the same NixOS configuration. Let me introduce the main players:&lt;&#x2F;p&gt;
&lt;h3 id=&quot;storage-the-workhorse&quot;&gt;Storage: The Workhorse&lt;&#x2F;h3&gt;
&lt;p&gt;My primary server is “storage” - an Intel powered box with 32GB DDR5 RAM and a bcachefs array for data integrity. It’s the heart of my homelab, running 30+ services from media streaming to development tools:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Service Categories:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Media Stack&lt;&#x2F;strong&gt;: Plex, Jellyfin, complete *arr suite for automation&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Storage&lt;&#x2F;strong&gt;: Nextcloud, Seafile, Samba shares, Time Machine server&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Development&lt;&#x2F;strong&gt;: Gitea, code-server, CI&#x2F;CD runners&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Monitoring&lt;&#x2F;strong&gt;: Grafana, Prometheus, Netdata, Loki for logs&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The Intel integrated GPU handles all video transcoding, keeping the CPU free for other tasks. With bcachefs providing data integrity and compression, it’s both reliable and efficient.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;cloud-the-public-face&quot;&gt;Cloud: The Public Face&lt;&#x2F;h3&gt;
&lt;p&gt;“Cloud” is an ARM64 Oracle Cloud VPS (free tier - 4 cores, 24GB RAM) that serves as my authentication hub and public gateway. It runs LLDAP for user management, Dex for OIDC, and Authelia for 2FA - essentially handling all authentication for my infrastructure. Public services like blogs and chat run here too, all behind Cloudflare and Authelia protection.&lt;&#x2F;p&gt;
&lt;p&gt;The beauty? Both machines share 90% of their configuration through modules. Storage handles the heavy lifting, Cloud provides secure access.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-github-actions-magic&quot;&gt;The GitHub Actions Magic&lt;&#x2F;h2&gt;
&lt;p&gt;Here’s where it gets interesting. Every push to my repository triggers automated builds and deployments:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #F8F8F2; background-color: #282A36;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6272A4;&quot;&gt;# .github&#x2F;workflows&#x2F;deploy.yml&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt;name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; Deploy NixOS Hosts&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #BD93F9;&quot;&gt;on&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt;  push&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt;    branches&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; main&lt;&#x2F;span&gt;&lt;span&gt; ]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt;jobs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt;  deploy&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt;    runs-on&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; ubuntu-latest&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt;    steps&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;      -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt; uses&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; actions&#x2F;checkout@v3&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;      -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; Install Nix&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt;        uses&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; cachix&#x2F;install-nix-action@v22&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;      -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; Build configurations&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt;        run&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;: |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;          nix build .#nixosConfigurations.storage.config.system.build.toplevel&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;          nix build .#nixosConfigurations.cloud.config.system.build.toplevel&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;          &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;      -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt; Deploy to hosts&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #8BE9FD;&quot;&gt;        run&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FF79C6;&quot;&gt;: |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;          nix run .#deploy.storage&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F1FA8C;&quot;&gt;          nix run .#deploy.cloud&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Push to main, grab coffee, and watch your infrastructure update itself. If something breaks? The old configuration is still running until the new one successfully builds.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;images&#x2F;nixos-github-actions.png&quot; alt=&quot;Automated deployment pipeline flowing from GitHub to NixOS servers&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-not-insert-solution-here&quot;&gt;Why Not [Insert Solution Here]?&lt;&#x2F;h2&gt;
&lt;p&gt;The homelab world is full of solutions, each with its own philosophy:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;TrueNAS&lt;&#x2F;strong&gt;: Great for storage, but I wanted more flexibility for running arbitrary services. Plus, I like my ZFS configuration in version control.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Proxmox + LXC&#x2F;VMs&lt;&#x2F;strong&gt;: Powerful, but adds a virtualization layer I didn’t need. I prefer bare metal performance for media transcoding.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;CasaOS&#x2F;Umbrel&#x2F;etc&lt;&#x2F;strong&gt;: Perfect for beginners! But I wanted more control and the ability to run services these platforms don’t support.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Kubernetes&lt;&#x2F;strong&gt;: I use it at work. My homelab is where I go to &lt;em&gt;escape&lt;&#x2F;em&gt; YAML hell, not create more of it.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Docker Swarm&#x2F;Nomad&lt;&#x2F;strong&gt;: Still requires managing the underlying OS. NixOS manages everything from kernel to containers.&lt;&#x2F;p&gt;
&lt;p&gt;NixOS sits in the “very technical” category - it’s not for everyone. The learning curve is steep, the documentation can be sparse, and you’ll definitely spend a weekend figuring out why your first configuration won’t build. But once it clicks? You’ll never want to manage infrastructure any other way.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-reality-check&quot;&gt;The Reality Check&lt;&#x2F;h2&gt;
&lt;p&gt;Let’s be honest - NixOS isn’t all roses:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Learning Curve&lt;&#x2F;strong&gt;: Nix’s functional language takes time to grok&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Ecosystem&lt;&#x2F;strong&gt;: Some software needs packaging or workarounds&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Resource Usage&lt;&#x2F;strong&gt;: Keeping multiple system generations eats disk space&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Compilation&lt;&#x2F;strong&gt;: Sometimes you’ll need to build packages from source&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;But for a technical user who wants reproducible infrastructure? The tradeoffs are worth it.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-s-next&quot;&gt;What’s Next?&lt;&#x2F;h2&gt;
&lt;p&gt;In my next post, I’ll dive into the Constellation Pattern - how I structure my NixOS configurations to share code between hosts while maintaining flexibility. We’ll look at real examples from my repository and how to build your own modular NixOS infrastructure.&lt;&#x2F;p&gt;
&lt;p&gt;For now, if you’re tired of the “pets vs cattle” debate and want infrastructure that’s more like “robots that configure themselves,” give NixOS a look. Your future self at 3 AM will thank you.&lt;&#x2F;p&gt;
&lt;p&gt;Want to see how it all works? Check out my &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&quot;&gt;NixOS configuration on GitHub&lt;&#x2F;a&gt; - everything from this blog’s deployment to my entire homelab is there.&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>About</title>
          <pubDate>Sun, 01 Jan 2023 00:00:00 +0000</pubDate>
          <author>Alex Rosenfeld</author>
          <link>https://blog.arsfeld.dev/about/</link>
          <guid>https://blog.arsfeld.dev/about/</guid>
          <description xml:base="https://blog.arsfeld.dev/about/">&lt;h1 id=&quot;about-alexandre-rosenfeld&quot;&gt;About Alexandre Rosenfeld&lt;&#x2F;h1&gt;
&lt;p&gt;I’m a Senior Software Architect based in Montreal, Canada, with a passion for building great teams and products. My journey in technology spans over 15 years, from contributing to open source projects as a Computer Engineering student in Brazil to architecting scalable solutions for global companies.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;professional-journey&quot;&gt;Professional Journey&lt;&#x2F;h2&gt;
&lt;p&gt;Currently serving as Senior Software Architect at ACCESS Newswire, I specialize in re-architecting products using microservices and modern cloud architectures. Previously, I led the Programming &amp;amp; DevOps team at Ubisoft, where I implemented DevOps practices and observability systems that transformed how teams deliver software.&lt;&#x2F;p&gt;
&lt;p&gt;My technical expertise spans multiple languages and frameworks - from C# and Python to Laravel and React - but my true passion lies in solving complex problems and helping teams grow.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;a-long-history-with-open-source&quot;&gt;A Long History with Open Source&lt;&#x2F;h2&gt;
&lt;p&gt;My open source journey began in 2008 as a Google Summer of Code participant, working on Conduit for the GNOME project. This experience shaped my philosophy about data ownership and interoperability - themes that still resonate in my work today. Back then, I dreamed of applications that could seamlessly share and transform data, making computers truly useful for their users.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;from-data-liberation-to-self-hosting&quot;&gt;From Data Liberation to Self-Hosting&lt;&#x2F;h2&gt;
&lt;p&gt;Over the years, my focus evolved from data interoperability to infrastructure ownership. I’ve been running self-hosted infrastructure for over a decade, progressing through various solutions:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Started with simple scripts and manual configurations&lt;&#x2F;li&gt;
&lt;li&gt;Moved to Docker Compose for containerization&lt;&#x2F;li&gt;
&lt;li&gt;Experimented with Kubernetes for orchestration&lt;&#x2F;li&gt;
&lt;li&gt;Finally settled on NixOS for declarative, reproducible systems&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Today, I maintain a fleet of NixOS machines ranging from ARM-based routers to powerful x86 servers, all managed through declarative configuration.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-you-ll-find-here&quot;&gt;What You’ll Find Here&lt;&#x2F;h2&gt;
&lt;p&gt;This blog chronicles my ongoing experiments with:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;NixOS patterns and configurations&lt;&#x2F;strong&gt; from real production systems&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Self-hosting solutions&lt;&#x2F;strong&gt; as alternatives to cloud services&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Infrastructure automation&lt;&#x2F;strong&gt; using declarative approaches&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Technical deep-dives&lt;&#x2F;strong&gt; into networking, containerization, and system design&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;All configurations and code shared here are from my actual setup, available on &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&#x2F;nixos&quot;&gt;GitHub&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;beyond-code&quot;&gt;Beyond Code&lt;&#x2F;h2&gt;
&lt;p&gt;I speak five languages (Portuguese, English, French, Spanish, and German) and believe that effective communication is as crucial as technical skills. I’m passionate about mentoring, always learning, and helping others grow in their careers.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;contact&quot;&gt;Contact&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Email&lt;&#x2F;strong&gt;: alex@rosenfeld.one&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;GitHub&lt;&#x2F;strong&gt;: &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;arsfeld&quot;&gt;@arsfeld&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;LinkedIn&lt;&#x2F;strong&gt;: &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;linkedin.com&#x2F;in&#x2F;a-rosenfeld&quot;&gt;linkedin.com&#x2F;in&#x2F;a-rosenfeld&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Website&lt;&#x2F;strong&gt;: &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;arsfeld.dev&quot;&gt;arsfeld.dev&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Feel free to reach out if you have questions about any of the configurations or patterns discussed here, or if you just want to chat about self-hosting, NixOS, or technology in general!&lt;&#x2F;p&gt;
</description>
      </item>
      <item>
          <title>Laravel and Azure SQL Server</title>
          <pubDate>Tue, 14 Jul 2015 00:00:00 +0000</pubDate>
          <author>Alex Rosenfeld</author>
          <link>https://blog.arsfeld.dev/posts/2015/07/14/laravel-and-azure-sql-server/</link>
          <guid>https://blog.arsfeld.dev/posts/2015/07/14/laravel-and-azure-sql-server/</guid>
          <description xml:base="https://blog.arsfeld.dev/posts/2015/07/14/laravel-and-azure-sql-server/">&lt;p&gt;From the same project as my &lt;a href=&quot;https:&#x2F;&#x2F;arosenfeld.wordpress.com&#x2F;2015&#x2F;06&#x2F;30&#x2F;laravel-5-and-filemaker&#x2F;&quot;&gt;last blog post&lt;&#x2F;a&gt;, we had to connect a Laravel 5 web application to a SQL Server instance running on Azure. It took me awhile to get everything working well, so I want to share a few tips for anyone looking for something like that.&lt;&#x2F;p&gt;
&lt;p&gt;A quick Google search gives lots of good resources and once you find out the SQL Server connector in Ubuntu &#x2F; Linux is actually called FreeTDS or Sybase (btw, Wikipedia has &lt;a href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Sybase&quot;&gt;a nice article&lt;&#x2F;a&gt; about why this name), you&amp;#8217;re good to go with a few &lt;strong&gt;apt-get &lt;&#x2F;strong&gt;commands if you&amp;#8217;re on Ubuntu:&lt;&#x2F;p&gt;
&lt;pre&gt;sudo apt-get install freetds-common freetds-bin unixodbc php5-sybase
&lt;&#x2F;pre&gt;
&lt;p&gt;Great, but then you get this error:&lt;&#x2F;p&gt;
&lt;pre&gt;SQLSTATE[01002] Adaptive Server connection failed (severity 9)
&lt;&#x2F;pre&gt;
&lt;p&gt;Oops! This happens because we&amp;#8217;re trying to connect to an Azure server, so you can actually ignore this if you&amp;#8217;re not getting this error.&lt;&#x2F;p&gt;
&lt;p&gt;Somewhere in the internet you discover you have to &lt;a href=&quot;http:&#x2F;&#x2F;martinrichards.tumblr.com&#x2F;post&#x2F;28488121620&#x2F;connecting-to-sql-azure-using-freetds&quot;&gt;change the TDS&lt;&#x2F;a&gt; protocol version to connect to Azure. Cool! You also discover you can do that in a configuration file. So, open up &lt;code&gt;&#x2F;etc&#x2F;freetds&#x2F;freetds.conf&lt;&#x2F;code&gt; and change a few lines around:&lt;&#x2F;p&gt;
&lt;pre&gt;[global]
        # TDS protocol version
        ;tds version = 4.2
        tds version = 8.0
&lt;&#x2F;pre&gt;
&lt;p&gt;Everything is great and life can move on!&lt;&#x2F;p&gt;
&lt;p&gt;Or, so you thought! Once you started handling dates in your models, all hell break loose and you start to get Carbon and Datetime issues everywhere. Don&amp;#8217;t worry, the fix is &lt;a href=&quot;http:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;11824323&#x2F;freetds-strange-date-time-format&quot;&gt;simple again&lt;&#x2F;a&gt;, now go to &lt;code&gt;&#x2F;etc&#x2F;freetds&#x2F;locales.conf&lt;&#x2F;code&gt; and make it look like:&lt;&#x2F;p&gt;
&lt;pre&gt;[default]
date format = %Y-%m-%d %I:%M:%S.%z&lt;&#x2F;pre&gt;
&lt;p&gt;Now you can actually start working on something productive again!&lt;&#x2F;p&gt;</description>
      </item>
      <item>
          <title>Laravel 5 and FileMaker</title>
          <pubDate>Tue, 30 Jun 2015 00:00:00 +0000</pubDate>
          <author>Alex Rosenfeld</author>
          <link>https://blog.arsfeld.dev/posts/2015/06/30/laravel-5-and-filemaker/</link>
          <guid>https://blog.arsfeld.dev/posts/2015/06/30/laravel-5-and-filemaker/</guid>
          <description xml:base="https://blog.arsfeld.dev/posts/2015/06/30/laravel-5-and-filemaker/">&lt;p&gt;A few weeks ago I was searching Google for exactly the two words in the title, how to connect a Laravel 5 web application to a FileMaker database and I couldn&amp;#8217;t find anything at all. Not really surprising, since that is one of the last things you would want to do.&lt;&#x2F;p&gt;
&lt;p&gt;So, why we wanted to do that in the first place? Well, we have a client that has a legacy application running on FileMaker and maintaining it was becoming a huge burden, so they want to migrate to something else. The server itself to host FileMaker is expensive (let alone it has to be a Mac or Windows) and it&amp;#8217;s almost impossible to add new features.&lt;&#x2F;p&gt;
&lt;p&gt;While a Laravel connector for FileMaker was not found, I did find a PHP library to connect to FileMaker called &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;soliantconsulting&#x2F;SimpleFM&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;SimpleFM&lt;&#x2F;a&gt;. Adding a provider for it in Laravel was pretty easy (I also included the .env configuration introduced in Laravel 5 and the database.php configuration).&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Code:&lt;&#x2F;strong&gt; &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gist.github.com&#x2F;arsfeld&#x2F;3a6995d50128061ba709&quot;&gt;View GitHub Gist&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;There is a bunch of ways you could have implemented this, I just wanted an easy way to get a FileMaker connection.&lt;&#x2F;p&gt;
&lt;p&gt;I also wanted to see information about the available layouts, since I prefer to avoid logging in to FileMaker at all costs. So I wrote a command to display all layout names in a database or to display column information in a specific database:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Code:&lt;&#x2F;strong&gt; &lt;a rel=&quot;noopener nofollow noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https:&#x2F;&#x2F;gist.github.com&#x2F;arsfeld&#x2F;eed276cd33bf61b857db&quot;&gt;View GitHub Gist&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Please note: I&amp;#8217;m using a Laravel 5.1 feature to describe the command as a signature, instead of the obscure and error-prone getOptions and getArguments.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Use it like this:&lt;&#x2F;p&gt;
&lt;pre&gt;.&#x2F;artisan fm:show
&lt;&#x2F;pre&gt;
&lt;pre&gt;Getting layout names
• Layout1
• Layout2
...
&lt;&#x2F;pre&gt;
&lt;p&gt;And you get a pretty table in response of a specific layout:&lt;&#x2F;p&gt;
&lt;pre&gt;
.&#x2F;artisan fm:show REPORTS
&lt;&#x2F;pre&gt;
&lt;pre&gt;Getting info for REPORTS
+-------+-------+-------+
| index | recid | modid |
+-------+-------+-------+
| 0 | 77 | 0 |
+-------+-------+-------+
&lt;&#x2F;pre&gt;
&lt;p&gt;Take a look &lt;a href=&quot;https:&#x2F;&#x2F;www.filemaker.com&#x2F;support&#x2F;product&#x2F;docs&#x2F;12&#x2F;fms&#x2F;fms12_cwp_xml_en.pdf&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;at the FileMaker docs&lt;&#x2F;a&gt; for more commands and then you&amp;#8217;re ready to do whatever you want with FileMaker inside your Laravel 5 application.&lt;&#x2F;p&gt;
&lt;p&gt;Happy hacking!&lt;&#x2F;p&gt;</description>
      </item>
      <item>
          <title>GNOME 3.0 and Fedora 15</title>
          <pubDate>Tue, 12 Apr 2011 00:00:00 +0000</pubDate>
          <author>Alex Rosenfeld</author>
          <link>https://blog.arsfeld.dev/posts/2011/04/12/gnome-30-and-fedora-15/</link>
          <guid>https://blog.arsfeld.dev/posts/2011/04/12/gnome-30-and-fedora-15/</guid>
          <description xml:base="https://blog.arsfeld.dev/posts/2011/04/12/gnome-30-and-fedora-15/">&lt;p&gt;On last sunday it happened the FLISOL, which I was able to partially attend here in La Paz. It was a good event, though I wasn&amp;#8217;t able to see all the talks. Unfortunately I didn&amp;#8217;t had much time to convince people to use GNOME 3, but I did a short 15 minute demonstration of GNOME 3.0 running on the Fedora livecd. Most questions were about how to use it in Ubuntu, so I had some bad news for them.&lt;&#x2F;p&gt;
&lt;p&gt;As a user of GNOME, I need to say thank you for everyone involved in the GNOME 3.0 release. I started using it only last week and I fell in love immediately. It&amp;#8217;s not only beatiful and great to use, it&amp;#8217;s also inspiring. It&amp;#8217;s great to see the direction GNOME is taking and what we can build on.&lt;&#x2F;p&gt;
&lt;p&gt;I also have to say congratulations to the Fedora guys. I installed Fedora 15 Alpha then updated to the latest packages. Even with the Alpha packages it was rock solid. Sure enough after using it for my day to day work for a week I had some crashes, but it&amp;#8217;s amazingly stable. And it has some cool features as well (I love the systemd idea). So at least for me, Fedora has everything to be the best distro around this year.&lt;&#x2F;p&gt;</description>
      </item>
      <item>
          <title>First impressions with Gnome 3</title>
          <pubDate>Fri, 25 Mar 2011 00:00:00 +0000</pubDate>
          <author>Alex Rosenfeld</author>
          <link>https://blog.arsfeld.dev/posts/2011/03/25/first-impressions-with-gnome-3/</link>
          <guid>https://blog.arsfeld.dev/posts/2011/03/25/first-impressions-with-gnome-3/</guid>
          <description xml:base="https://blog.arsfeld.dev/posts/2011/03/25/first-impressions-with-gnome-3/">&lt;p&gt;I tried Gnome Shell a few months ago but I had so many issues I didn&amp;#8217;t actually experienced anything. Yesterday I downloaded the Gnome 3 livecd but it seems my Radeon doesn&amp;#8217;t work at all with it (all I see are some random dots in the screen even with safe mode on). Since I still wanted to test Gnome 3, I updated an Arch Linux installation I had to Gnome 3 (took a long time to download since I had tons of stuff to update). So then I was finally able to actually test Gnome 3 today. And I was quite impressed by two things.&lt;&#x2F;p&gt;
&lt;p&gt;One, it crashed a lot for me. Don&amp;#8217;t know if it&amp;#8217;s a Gnome 3 issue or an Arch Linux issue. But it crashed every 5 minutes. I couldn&amp;#8217;t change the wallpaper without it crashing. And most of the times it wouldn&amp;#8217;t login telling me something went wrong and I had to logout. I even added a new user to make sure the old settings were not giving it problems, but it was the same.&lt;&#x2F;p&gt;
&lt;p&gt;I do hope these things are eventually fixed, because of my second point. I loved it! It&amp;#8217;s amazingly beautiful, I loved the animations, loved the details like how items glow when you hover them. I loved the menus on the top right and how I could press the Super button to get the overview mode. The new control center is beautiful and it just feels right.&lt;&#x2F;p&gt;
&lt;p&gt;I tried Unity last week and in my personal opinion, the Gnome 3 experience is far better then anything Ubuntu can offer right now.&lt;&#x2F;p&gt;</description>
      </item>
      <item>
          <title>Back to Ext4 from Btrfs</title>
          <pubDate>Mon, 27 Dec 2010 00:00:00 +0000</pubDate>
          <author>Alex Rosenfeld</author>
          <link>https://blog.arsfeld.dev/posts/2010/12/27/back-to-ext4-from-btrfs/</link>
          <guid>https://blog.arsfeld.dev/posts/2010/12/27/back-to-ext4-from-btrfs/</guid>
          <description xml:base="https://blog.arsfeld.dev/posts/2010/12/27/back-to-ext4-from-btrfs/">&lt;p&gt;After using Btrfs on both my work and home machines, I&amp;#8217;m switching back to ext4. I actually like Btrfs and I wish I could keep using it, but I had two major issues.&lt;&#x2F;p&gt;
&lt;p&gt;The first one and this was enough for me to switch back to ext4, Google Chrome startup time was almost 10 times slower. At first I didn&amp;#8217;t realize it was Btrfs causing this, but after some &lt;a href=&quot;http:&#x2F;&#x2F;comments.gmane.org&#x2F;gmane.comp.file-systems.btrfs&#x2F;7304&quot;&gt;investigation&lt;&#x2F;a&gt;, it&amp;#8217;s noticeable how fragmentation affects some applications using Btrfs. I actually switched my work machine to ext4 after I found I could not use the virtual machines I had anymore, because it kept reading the disk for hours at anything I did in the virtual machine (in the end they could not even boot anymore). Btrfs is not always slower, or at least not noticeable, but for a few scenarios that can cause fragmentation, they really do make Btrfs unusable for me.&lt;&#x2F;p&gt;
&lt;p&gt;The second one might just be a misunderstanding of my part, but I read manuals and wikis and could not find an answer. I started with one 150GB partition at work and had another 150GB partition if I wanted to easily switch back to ext4. Then I wanted to test adding multiple devices to Btrfs, which it makes really easy, and added the other partition to the first and did a balance operation, so data was distributed between them. What I did not realize, is that by doing that it created a RAID 1 with my partitions. As far as I know, RAID 1 with two partitions in the same drive are just useless, it just duplicates data between both partitions. After reading more manuals, I learned that on creation time you can control the RAID level, but I found no way to do it afterwards. And the worst of it, I found no way to degrade back to RAID 0, so I found no way to remove the second partition from the filesystem, which means I ended up with 300GB of space being used as 150GB.&lt;&#x2F;p&gt;
&lt;p&gt;And another issue I had, it was not always obvious how much disk space I had free. In some places it indicated 300GB, others 150GB. And in my home machine I even had an out of free space error when it indicated I had 6GB free.&lt;&#x2F;p&gt;
&lt;p&gt;Despite its powerful features, I could not justify the problems I had. Actually, I was not using snapshots and sobvolumes as much as I expected. I believe this may change as more tools are written to take advantage of these features.&lt;&#x2F;p&gt;
&lt;p&gt;I knew well before I started using it that it&amp;#8217;s not production ready or even finished yet. Btrfs is definitely a step in the right direction and I hope I can try using it again in some time. It even managed not to lose any of my data despite me poking with stuff I did not understand.&lt;&#x2F;p&gt;</description>
      </item>
      <item>
          <title>CONSOL 2010</title>
          <pubDate>Tue, 14 Dec 2010 00:00:00 +0000</pubDate>
          <author>Alex Rosenfeld</author>
          <link>https://blog.arsfeld.dev/posts/2010/12/14/consol-2010/</link>
          <guid>https://blog.arsfeld.dev/posts/2010/12/14/consol-2010/</guid>
          <description xml:base="https://blog.arsfeld.dev/posts/2010/12/14/consol-2010/">&lt;p&gt;I just got back from Santa Cruz de la Sierra, Bolivia, where I was attending CONSOL 2010, the Congreso de Software Libre of Bolivia. And I had an amazing time over there and met some really amazing people. This is a thank you note for everyone there, for welcoming me and trying to understand my lousy spanish talking about Google Summer of Code and Gnome.&lt;&#x2F;p&gt;
&lt;p&gt;It amazes me every day how far Gnome and my Summer of Code experience is taking me. I&amp;#8217;m doing an internship in an open-source company in Bolivia simply because I had Gnome in my curriculum and it got the attention from someone. And I am learning so much, both from my work here and from living in such an incredible country as Bolivia, where the diversity and contrast is amazing even for someone coming from Brazil. And the motivation I saw there, not only because in the poorest country of South America, free software makes a lot of sense, but because of their passion to hacking on stuff, to learn about new stuff and make something useful out of all of that.&lt;&#x2F;p&gt;
&lt;p&gt;So, congrats to these guys there and thank you for everything (especially Amos Batto and Hardy Beltran for inviting me).&lt;&#x2F;p&gt;
&lt;p&gt;On a side note, Rhythmbox 0.13.2 released a few weeks ago contains my first real contribution to Gnome, with my Google Summer of Project of this year being released. So if you have an iPhone or Android, enable the DAAP plugin together with the Remote switch and enjoy controlling Rhythmbox anywhere in your home! Thanks to W. Michael Petulio, Jonathan Matthew and Peter, without their help this would never be released.&lt;&#x2F;p&gt;</description>
      </item>
      <item>
          <title>Analyzing HTTP packets with Wireshark and Python</title>
          <pubDate>Sun, 21 Nov 2010 00:00:00 +0000</pubDate>
          <author>Alex Rosenfeld</author>
          <link>https://blog.arsfeld.dev/posts/2010/11/21/analyzing-http-packets-with-wireshark-and-python/</link>
          <guid>https://blog.arsfeld.dev/posts/2010/11/21/analyzing-http-packets-with-wireshark-and-python/</guid>
          <description xml:base="https://blog.arsfeld.dev/posts/2010/11/21/analyzing-http-packets-with-wireshark-and-python/">&lt;p&gt;I&amp;#8217;m doing some reverse-engineering stuff and it has been quite fun so far (hopefully I&amp;#8217;ll blog more about why I&amp;#8217;m doing this in the future). I needed to dump some HTTP traffic and analyse the data. Of course, Wireshark comes straight to mind for something like this and it is indeed really useful. It took me some time to understand the Wireshark interface and I still think it&amp;#8217;s hiding some great functionality from me. But anyway, I was able to set the filters I wanted and it was showing me exactly the data I wanted. But I still had to right-click the data I wanted and save it to disk, which was not ideal.&lt;&#x2F;p&gt;
&lt;p&gt;Then I thought, if people were smart enough to build such a powerful tool, they probably created a command-line interface as well, probably with scripting. Indeed they did! The command-line interface is called Tshark and the scripting is done in Lua. But I don&amp;#8217;t know Lua and it would take too much time to learn it for this task. So I started to look a way to dump everything and then write a small script in Python to extract the data I really want. Took some time but the solution was much simpler then I thought (by the way, there are probably other solutions for this, but my Google skills were not good enough to find anything obvious).&lt;&#x2F;p&gt;
&lt;p&gt;First you run Tshark to dump any HTTP traffic to a XML file (I usually hate XML, but this time it was useful). This is what I used:&lt;&#x2F;p&gt;
&lt;pre&gt;sudo tshark -i wlan0 &quot;host 192.168.1.100 and port 45000&quot; -d tcp.port==45000,http -T pdml &amp;gt; dump.xml&lt;&#x2F;pre&gt;
&lt;p&gt;Of course, it all depends on what you want to dump. You should read the &amp;#8220;man pcap-filter&amp;#8221; to get the capture filter right and it is really useful (crucial sometimes) to only get the traffic you want. And I wanted to treat traffic in port 45000 as HTTP, so I think that is what the -d switch does 😉 The most important thing it &amp;#8220;-T pdml&amp;#8221;, which tells tshark to dump in this XML format.&lt;&#x2F;p&gt;
&lt;p&gt;Next thing is to analyze in Python, which was much easier than I thought. I was only worried about the data field in the HTTP packets, but if you take a look in the dumped file, you&amp;#8217;ll see you have information about all kind of things. My script turned out to be this:&lt;&#x2F;p&gt;
&lt;pre&gt;from lxml import etree
import binascii
tree = etree.parse(&#x27;dump.xml&#x27;)
data = [binascii.unhexlify(e.get(&quot;value&quot;)) for e in tree.xpath(&#x27;&#x2F;pdml&#x2F;packet&#x2F;proto[@name=&quot;http&quot;]&#x2F;field[@name=&quot;data&quot;]&#x27;)]&lt;&#x2F;pre&gt;
&lt;p&gt;I used lxml because I found it has great support for XPath, which is quite useful here. Also, the HTTP data is stored as a hex string, which you can easily convert with unhexlify. So, in the end I was able to automate an annoying process with just a few lines of code. And if I need anything else, it&amp;#8217;s quite easy to expand the script. I&amp;#8217;m quite happy with the results!&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Update: &lt;&#x2F;strong&gt;Someone pointed out in the comments about Scapy (http:&#x2F;&#x2F;www.secdev.org&#x2F;projects&#x2F;scapy&#x2F;), which by reading it&amp;#8217;s documentation seems awesome!&lt;&#x2F;p&gt;</description>
      </item>
      <item>
          <title>glib-mkenums</title>
          <pubDate>Thu, 12 Aug 2010 00:00:00 +0000</pubDate>
          <author>Alex Rosenfeld</author>
          <link>https://blog.arsfeld.dev/posts/2010/08/12/glib-mkenums/</link>
          <guid>https://blog.arsfeld.dev/posts/2010/08/12/glib-mkenums/</guid>
          <description xml:base="https://blog.arsfeld.dev/posts/2010/08/12/glib-mkenums/">&lt;p&gt;I&amp;#8217;m posting this here both to help someone else looking for this and to check if I got everything right.&lt;&#x2F;p&gt;
&lt;p&gt;I needed to use enums in a GObject property. So I needed a &lt;a href=&quot;http:&#x2F;&#x2F;library.gnome.org&#x2F;devel&#x2F;gobject&#x2F;unstable&#x2F;gobject-Enumeration-and-Flag-Types.html&quot;&gt;enum type&lt;&#x2F;a&gt; for my property &lt;a href=&quot;http:&#x2F;&#x2F;library.gnome.org&#x2F;devel&#x2F;gobject&#x2F;unstable&#x2F;gobject-Standard-Parameter-and-Value-Types.html#g-param-spec-enum&quot;&gt;param spec&lt;&#x2F;a&gt;. I thought I could hard-code it somehow, but after some long time pondering (actually knocking my head into the wall) I decided to integrate &lt;a href=&quot;http:&#x2F;&#x2F;library.gnome.org&#x2F;devel&#x2F;gobject&#x2F;unstable&#x2F;glib-mkenums.html&quot;&gt;glib-mkenums&lt;&#x2F;a&gt; into autoconf so that it could generate the types for me automatically when running make from my C sources. Unfortunately for me, Google wasn&amp;#8217;t being friendly when searching for information on glib-mkenums.&lt;&#x2F;p&gt;
&lt;p&gt;I found somewhere, some program that used glib-mkenums in a simple way (sorry, I forgot where I fount it), close to what I had in mind, so I decided to adapt it to my needs. What I had to do was to add two more files, automatically generated (in this case named dmap-enums.c and dmap-enums.h) adding &lt;a href=&quot;http:&#x2F;&#x2F;gitorious.org&#x2F;arosenfeld-gsoc-2010&#x2F;libdmapsharing&#x2F;blobs&#x2F;c7c495219221e6d90dc581822262c99563894011&#x2F;libdmapsharing&#x2F;Makefile.am#line103&quot;&gt;some commands&lt;&#x2F;a&gt; to Makefile.am (linked to Gitorious because WordPress removes all formatting). Hopefully that is the right way to do it, at least it is working for me.&lt;&#x2F;p&gt;</description>
      </item>
      <item>
          <title>DACP in Rhythmbox: Week 11</title>
          <pubDate>Fri, 06 Aug 2010 00:00:00 +0000</pubDate>
          <author>Alex Rosenfeld</author>
          <link>https://blog.arsfeld.dev/posts/2010/08/06/dacp-in-rhythmbox-week-11/</link>
          <guid>https://blog.arsfeld.dev/posts/2010/08/06/dacp-in-rhythmbox-week-11/</guid>
          <description xml:base="https://blog.arsfeld.dev/posts/2010/08/06/dacp-in-rhythmbox-week-11/">&lt;p&gt;I just went from &lt;a href=&quot;http:&#x2F;&#x2F;arosenfeld.wordpress.com&#x2F;2010&#x2F;06&#x2F;07&#x2F;dacp-in-rhythmbox-week-2&#x2F;&quot;&gt;week 2&lt;&#x2F;a&gt; to week 11 in the &lt;a href=&quot;http:&#x2F;&#x2F;live.gnome.org&#x2F;SummerOfCode2010&#x2F;AlexandreRosenfeld_Rhythmbox&quot;&gt;GSoC progress&lt;&#x2F;a&gt; in my blog 😉 Well, there is not much to tell in a blog if there is not a picture to show (and showing off the iPhone remote working with Rhythmbox should not be any different than iTunes if I&amp;#8217;m doing my job correctly).&lt;&#x2F;p&gt;
&lt;p&gt;I realized these last weeks that I had completely mis-planned my project, because I had no idea what I was getting into. I thought DACP would be quite easy to implement and I would focus on other things (making a client library for instance). But I discovered it is a much more complex protocol then I thought, mostly because of DAAP.&lt;&#x2F;p&gt;
&lt;p&gt;What I discovered is that DACP is just an extension for DAAP, and being an Apple protocol, it&amp;#8217;s closed and has been reverse-engineered and re-implemented several times in the open source world. The problem is that DACP uses several features in DAAP that were not implemented in libdmapsharing, simply because there is no real standard on DAAP. It took some time for me to learn DAAP enough to find out what was missing in libdmapsharing for DACP to work.&lt;&#x2F;p&gt;
&lt;p&gt;So I spent most of the time fixing, tweaking and implementing stuff on DAAP in libdmapsharing. Which was pretty cool, I improved a lot of my C skills, learned GObject (and quite frankly, liked it a lot) and learned a lot about DAAP and libdmapharing.&lt;&#x2F;p&gt;
&lt;p&gt;By the way, thanks for all the people who have &lt;a href=&quot;http:&#x2F;&#x2F;dacp.jsharkey.org&#x2F;&quot;&gt;reverse-engineered DACP&lt;&#x2F;a&gt; and the &lt;a href=&quot;http:&#x2F;&#x2F;jsharkey.org&#x2F;blog&#x2F;2009&#x2F;06&#x2F;21&#x2F;itunes-dacp-pairing-hash-is-broken&#x2F;&quot;&gt;pairing process&lt;&#x2F;a&gt;, you have greatly simplified my life. It&amp;#8217;s truly great to work in the open-source world.&lt;&#x2F;p&gt;</description>
      </item>
      <item>
          <title>Cryptkeeper in your Indicator Applet</title>
          <pubDate>Thu, 24 Jun 2010 00:00:00 +0000</pubDate>
          <author>Alex Rosenfeld</author>
          <link>https://blog.arsfeld.dev/posts/2010/06/24/cryptkeeper-in-your-indicator-applet/</link>
          <guid>https://blog.arsfeld.dev/posts/2010/06/24/cryptkeeper-in-your-indicator-applet/</guid>
          <description xml:base="https://blog.arsfeld.dev/posts/2010/06/24/cryptkeeper-in-your-indicator-applet/">&lt;p&gt;I like the Indicator Applet and I like Cryptkeeper, so I decided to create an indicator for Cryptkeeper:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;__GHOST_URL__&#x2F;content&#x2F;images&#x2F;wordpress&#x2F;2010&#x2F;06&#x2F;screenshot2.jpg&quot;&gt;&lt;img decoding=&quot;async&quot; loading=&quot;lazy&quot; class=&quot;aligncenter size-full wp-image-76&quot; title=&quot;Cryptkeeper in your Indicator Applet&quot; src=&quot;__GHOST_URL__&#x2F;content&#x2F;images&#x2F;wordpress&#x2F;2010&#x2F;06&#x2F;screenshot2.jpg&quot; alt=&quot;&quot; width=&quot;607&quot; height=&quot;48&quot; srcset=&quot;__GHOST_URL__&#x2F;content&#x2F;images&#x2F;wordpress&#x2F;2010&#x2F;06&#x2F;screenshot2.jpg 607w, __GHOST_URL__&#x2F;content&#x2F;images&#x2F;wordpress&#x2F;2010&#x2F;06&#x2F;screenshot2-300x24.jpg 300w&quot; sizes=&quot;(max-width: 607px) 100vw, 607px&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;For anyone who doesn&amp;#8217;t know, &lt;a href=&quot;http:&#x2F;&#x2F;tom.noflag.org.uk&#x2F;cryptkeeper.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Cryptkeeper&lt;&#x2F;a&gt; is a very useful application that allows you to mount&#x2F;unmount an encrypted folder with just one click. It&amp;#8217;s one of the most useful applications running on my startup. But up until now it had a quite ugly icon:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;__GHOST_URL__&#x2F;content&#x2F;images&#x2F;wordpress&#x2F;2010&#x2F;06&#x2F;screenshot.jpg&quot;&gt;&lt;img decoding=&quot;async&quot; loading=&quot;lazy&quot; class=&quot;aligncenter size-full wp-image-77&quot; title=&quot;Cryptkeeper before&quot; src=&quot;__GHOST_URL__&#x2F;content&#x2F;images&#x2F;wordpress&#x2F;2010&#x2F;06&#x2F;screenshot.jpg&quot; alt=&quot;&quot; width=&quot;607&quot; height=&quot;48&quot; srcset=&quot;__GHOST_URL__&#x2F;content&#x2F;images&#x2F;wordpress&#x2F;2010&#x2F;06&#x2F;screenshot.jpg 607w, __GHOST_URL__&#x2F;content&#x2F;images&#x2F;wordpress&#x2F;2010&#x2F;06&#x2F;screenshot-300x24.jpg 300w&quot; sizes=&quot;(max-width: 607px) 100vw, 607px&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Can you spot the difference?&lt;&#x2F;p&gt;
&lt;p&gt;This is actually a plot to make people to like it and finish it. The patch adds support for showing and letting you mount&#x2F;unmount folders, but it doesn&amp;#8217;t let you delete folders or view information, as it did. Also, when adding a new folder, it doesn&amp;#8217;t show up on the list if you don&amp;#8217;t restart Cryptkeeper. But the patch does what I wanted it to (I rarely create or delete an encrypted folder), so I probably won&amp;#8217;t change it further.&lt;&#x2F;p&gt;
&lt;p&gt;So, the patch is &lt;a href=&quot;https:&#x2F;&#x2F;bugs.launchpad.net&#x2F;ubuntu&#x2F;+source&#x2F;cryptkeeper&#x2F;+bug&#x2F;571473&quot;&gt;here&lt;&#x2F;a&gt;. Have fun 😉&lt;&#x2F;p&gt;</description>
      </item>
      <item>
          <title>DACP in Rhythmbox: Week 2</title>
          <pubDate>Mon, 07 Jun 2010 00:00:00 +0000</pubDate>
          <author>Alex Rosenfeld</author>
          <link>https://blog.arsfeld.dev/posts/2010/06/07/dacp-in-rhythmbox-week-2/</link>
          <guid>https://blog.arsfeld.dev/posts/2010/06/07/dacp-in-rhythmbox-week-2/</guid>
          <description xml:base="https://blog.arsfeld.dev/posts/2010/06/07/dacp-in-rhythmbox-week-2/">&lt;p&gt;Last week there was the &lt;a href=&quot;https:&#x2F;&#x2F;bugzilla.gnome.org&#x2F;show_bug.cgi?id=566852&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;inclusion of libdmapsharing&lt;&#x2F;a&gt; in Rhythmbox (by the way, for all of you alpha testing Ubuntu, DAAP isn&amp;#8217;t working in Rhythmbox because they haven&amp;#8217;t included libdmapsharing in Ubuntu yet). So now I&amp;#8217;m just a git pull and git merge away from following Rhythmbox master, which makes my job much easier.&lt;&#x2F;p&gt;
&lt;p&gt;Last week didn&amp;#8217;t see much work from me, I was mostly fixing some bugs and some bad decisions. But I noticed I never showed screenshots of my work here. Because I love screenshots, here they are.&lt;&#x2F;p&gt;
&lt;p&gt;First you open your Remote application in iPhone, iPod Touch or Android device (note that on Android there is no passcode):&lt;&#x2F;p&gt;&lt;figure class=&quot;kg-card kg-image-card&quot;&gt;&lt;img src=&quot;__GHOST_URL__&#x2F;content&#x2F;images&#x2F;2023&#x2F;07&#x2F;2010-10-32-13.jpg&quot; class=&quot;kg-image&quot; alt loading=&quot;lazy&quot; width=&quot;320&quot; height=&quot;480&quot;&gt;&lt;&#x2F;figure&gt;&lt;!--kg-card-begin: markdown--&gt;&lt;p&gt;Then in Rhythmbox, you type the passcode shown in your Remote. This allows Rhythmbox to pair with your Remote:&lt;&#x2F;p&gt;
&lt;!--kg-card-end: markdown--&gt;&lt;figure class=&quot;kg-card kg-image-card&quot;&gt;&lt;img src=&quot;__GHOST_URL__&#x2F;content&#x2F;images&#x2F;2023&#x2F;07&#x2F;screenshot_rhythmbox.png&quot; class=&quot;kg-image&quot; alt loading=&quot;lazy&quot; width=&quot;864&quot; height=&quot;627&quot; srcset=&quot;__GHOST_URL__&#x2F;content&#x2F;images&#x2F;size&#x2F;w600&#x2F;2023&#x2F;07&#x2F;screenshot_rhythmbox.png 600w, __GHOST_URL__&#x2F;content&#x2F;images&#x2F;2023&#x2F;07&#x2F;screenshot_rhythmbox.png 864w&quot; sizes=&quot;(min-width: 720px) 720px&quot;&gt;&lt;&#x2F;figure&gt;&lt;!--kg-card-begin: markdown--&gt;&lt;p&gt;Then Rhythmbox magically appears at your Remote list of libraries:&lt;&#x2F;p&gt;
&lt;!--kg-card-end: markdown--&gt;&lt;figure class=&quot;kg-card kg-image-card&quot;&gt;&lt;img src=&quot;__GHOST_URL__&#x2F;content&#x2F;images&#x2F;2023&#x2F;07&#x2F;iphone.jpg&quot; class=&quot;kg-image&quot; alt loading=&quot;lazy&quot; width=&quot;320&quot; height=&quot;480&quot;&gt;&lt;&#x2F;figure&gt;&lt;p&gt;&amp;#8220;DACP on Rhythmbox&amp;#8221; will become your library&amp;#8217;s name over time. And of course, this doesn&amp;#8217;t fully work yet, because Rhythmbox segfaults when the iPhone tries to connect. Well, what did you expected for about two weeks of work? I was pretty excited already when I saw my iPhone on the list of devices in Rhythmbox (by the way, I think I will create a new group called Remotes, I just didn&amp;#8217;t find out how yet).&lt;&#x2F;p&gt;
&lt;p&gt;Stay tuned for more later.&lt;&#x2F;p&gt;</description>
      </item>
      <item>
          <title>GSoC 2010: DACP Support in Rhythmbox</title>
          <pubDate>Fri, 21 May 2010 00:00:00 +0000</pubDate>
          <author>Alex Rosenfeld</author>
          <link>https://blog.arsfeld.dev/posts/2010/05/21/gsoc-2010-dacp-support-in-rhythmbox/</link>
          <guid>https://blog.arsfeld.dev/posts/2010/05/21/gsoc-2010-dacp-support-in-rhythmbox/</guid>
          <description xml:base="https://blog.arsfeld.dev/posts/2010/05/21/gsoc-2010-dacp-support-in-rhythmbox/">&lt;p&gt;Community bonding is almost over so I thought I could share some details about my Google Summer of Code project.&lt;&#x2F;p&gt;
&lt;p&gt;As the title says, I will implement DACP support for Rhythmbox. You probably never heard of DACP but you&amp;#8217;ve probably seen it in action. It&amp;#8217;s the protocol that is used by the Remote application in iPhone and iPod Touch.&lt;&#x2F;p&gt;&lt;figure class=&quot;kg-card kg-image-card&quot;&gt;&lt;img src=&quot;__GHOST_URL__&#x2F;content&#x2F;images&#x2F;2023&#x2F;07&#x2F;apple-iphone-remote.jpg&quot; class=&quot;kg-image&quot; alt loading=&quot;lazy&quot; width=&quot;555&quot; height=&quot;350&quot;&gt;&lt;&#x2F;figure&gt;&lt;p&gt;I&amp;#8217;ll be implementing the server side in Rhythmbox, so you&amp;#8217;ll be able to control Rhythmbox with your iPhone, iPod Touch or even your &lt;a href=&quot;http:&#x2F;&#x2F;dacp.jsharkey.org&#x2F;&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Android&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;I&amp;#8217;ll actually be implementing this inside &lt;a href=&quot;http:&#x2F;&#x2F;www.flyn.org&#x2F;projects&#x2F;libdmapsharing&#x2F;&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;libdmapsharing&lt;&#x2F;a&gt;, a &lt;a href=&quot;http:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Digital_Audio_Access_Protocol&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;DMAP (DAAP &amp;amp; DPAP)&lt;&#x2F;a&gt; library that was once extracted from Rhythmbox sources, improved and will be integrated again into Rhythmbox as a library, once my &lt;a href=&quot;https:&#x2F;&#x2F;bugzilla.gnome.org&#x2F;show_bug.cgi?id=566852&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;mentor&amp;#8217;s patch&lt;&#x2F;a&gt; is accepted. So hopefully, more applications will be able to support DACP, DAAP and DPAP by using libdmapsharing.&lt;&#x2F;p&gt;</description>
      </item>
      <item>
          <title>Breadcrumb in Gtk</title>
          <pubDate>Thu, 18 Feb 2010 00:00:00 +0000</pubDate>
          <author>Alex Rosenfeld</author>
          <link>https://blog.arsfeld.dev/posts/2010/02/18/breadcrumb-in-gtk/</link>
          <guid>https://blog.arsfeld.dev/posts/2010/02/18/breadcrumb-in-gtk/</guid>
          <description xml:base="https://blog.arsfeld.dev/posts/2010/02/18/breadcrumb-in-gtk/">&lt;p&gt;Anyone knows how to implement (or anywhere it&amp;#8217;s implemented) a breadcrumb &#x2F; pathbar like the one in Nautilus in PyGtk? I just don&amp;#8217;t know how to hide buttons that doent fit the window.&lt;&#x2F;p&gt;
&lt;p&gt;By the way, anyone has seen this:&lt;&#x2F;p&gt;
&lt;p style=&quot;text-align:center;&quot;&gt;&lt;a href=&quot;http:&#x2F;&#x2F;www.gnome-look.org&#x2F;content&#x2F;show.php&#x2F;elementary-remake?content=120287&quot;&gt;&lt;img decoding=&quot;async&quot; loading=&quot;lazy&quot; class=&quot;aligncenter&quot; title=&quot;Elementary Remake&quot; src=&quot;http:&#x2F;&#x2F;www.gnome-look.org&#x2F;CONTENT&#x2F;content-pre1&#x2F;120287-1.jpg&quot; alt=&quot;&quot; width=&quot;500&quot; height=&quot;300&quot; &#x2F;&gt;&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p style=&quot;text-align:left;&quot;&gt;That pathbar is gorgeous. It doesn&amp;#8217;t look as good in action tough.&lt;&#x2F;p&gt;
&lt;p style=&quot;text-align:left;&quot;&gt;I&amp;#8217;ve been using the smaller Nautilus with the Elementary theme and it is just beautiful (the picture is from Elementary Remake, the original theme with that pathbar).&lt;&#x2F;p&gt;</description>
      </item>
      <item>
          <title>Conduit GSoC</title>
          <pubDate>Wed, 26 Aug 2009 00:00:00 +0000</pubDate>
          <author>Alex Rosenfeld</author>
          <link>https://blog.arsfeld.dev/posts/2009/08/26/conduit-gsoc/</link>
          <guid>https://blog.arsfeld.dev/posts/2009/08/26/conduit-gsoc/</guid>
          <description xml:base="https://blog.arsfeld.dev/posts/2009/08/26/conduit-gsoc/">&lt;p&gt;Google Summer of Code is over and I still didnt blogged about the new interface for Conduit I wrote. Well, going back to university and doing multiple things at once can really slow things down.&lt;&#x2F;p&gt;
&lt;figure id=&quot;attachment_33&quot; aria-describedby=&quot;caption-attachment-33&quot; style=&quot;width: 426px&quot; class=&quot;wp-caption aligncenter&quot;&gt;&lt;img decoding=&quot;async&quot; loading=&quot;lazy&quot; class=&quot;size-full wp-image-33&quot; title=&quot;New UI - Screenshot 1&quot; src=&quot;__GHOST_URL__&#x2F;content&#x2F;images&#x2F;wordpress&#x2F;2009&#x2F;08&#x2F;screenshot-conduit-0-3-17-development-version-running-uninstalled-2.png&quot; alt=&quot;This shows the new UI with some conduits at the left&quot; width=&quot;426&quot; height=&quot;319&quot; srcset=&quot;__GHOST_URL__&#x2F;content&#x2F;images&#x2F;wordpress&#x2F;2009&#x2F;08&#x2F;screenshot-conduit-0-3-17-development-version-running-uninstalled-2.png 802w, __GHOST_URL__&#x2F;content&#x2F;images&#x2F;wordpress&#x2F;2009&#x2F;08&#x2F;screenshot-conduit-0-3-17-development-version-running-uninstalled-2-300x225.png 300w, __GHOST_URL__&#x2F;content&#x2F;images&#x2F;wordpress&#x2F;2009&#x2F;08&#x2F;screenshot-conduit-0-3-17-development-version-running-uninstalled-2-768x576.png 768w&quot; sizes=&quot;(max-width: 426px) 100vw, 426px&quot; &#x2F;&gt;&lt;figcaption id=&quot;caption-attachment-33&quot; class=&quot;wp-caption-text&quot;&gt;This shows the new UI with some conduits at the left&lt;&#x2F;figcaption&gt;&lt;&#x2F;figure&gt;
&lt;p&gt;Ok, so this is not final but I think it&amp;#8217;s an improvement over &lt;a href=&quot;http:&#x2F;&#x2F;conduit-project.org&#x2F;attachment&#x2F;wiki&#x2F;Screenshots&#x2F;Conduit-0.3.13-some.png?format=raw&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;the old UI&lt;&#x2F;a&gt;. The thing is, I was tired of owner-drawn widgets and I was tired of right-clicking things at the canvas (users didn&amp;#8217;t even know that thing in the middle was the canvas). And the new UI gives me a reason to use all the work I did to rebuild the configuration system (see why dialogs was not enough?).&lt;&#x2F;p&gt;
&lt;p&gt;What I still don&amp;#8217;t like about it, is that it&amp;#8217;s hard to get an overview of the conduit. Sinks are usually hidden so you don&amp;#8217;t even know they are there sometimes. I was thinking about adding a button to hide and show the configuration, but I think that is confusing and not HIG-friendly.&lt;&#x2F;p&gt;
&lt;p&gt;Another thing I&amp;#8217;m unsure about is tabs. Everyone has tabs nowadays, do why not Conduit?&lt;&#x2F;p&gt;
&lt;figure id=&quot;attachment_34&quot; aria-describedby=&quot;caption-attachment-34&quot; style=&quot;width: 426px&quot; class=&quot;wp-caption aligncenter&quot;&gt;&lt;img decoding=&quot;async&quot; loading=&quot;lazy&quot; class=&quot;size-full wp-image-34&quot; title=&quot;Conduit Screenshot 2&quot; src=&quot;__GHOST_URL__&#x2F;content&#x2F;images&#x2F;wordpress&#x2F;2009&#x2F;08&#x2F;screenshot-conduit-0-3-17-development-version-running-uninstalled-1.png&quot; alt=&quot;New UI with tabs&quot; width=&quot;426&quot; height=&quot;518&quot; srcset=&quot;__GHOST_URL__&#x2F;content&#x2F;images&#x2F;wordpress&#x2F;2009&#x2F;08&#x2F;screenshot-conduit-0-3-17-development-version-running-uninstalled-1.png 553w, __GHOST_URL__&#x2F;content&#x2F;images&#x2F;wordpress&#x2F;2009&#x2F;08&#x2F;screenshot-conduit-0-3-17-development-version-running-uninstalled-1-247x300.png 247w&quot; sizes=&quot;(max-width: 426px) 100vw, 426px&quot; &#x2F;&gt;&lt;figcaption id=&quot;caption-attachment-34&quot; class=&quot;wp-caption-text&quot;&gt;New UI with tabs&lt;&#x2F;figcaption&gt;&lt;&#x2F;figure&gt;
&lt;p&gt;Hm, I don&amp;#8217;t like that, it does look better but it&amp;#8217;s rather confusing.&lt;&#x2F;p&gt;
&lt;p&gt;I do like design, but I&amp;#8217;m not very good at it. Anyone interested in giving me some tips? Maybe someone could draw those Mac-looking mockups, that would be great!&lt;&#x2F;p&gt;
&lt;p&gt;Btw, thanks a lot John Carr and John Stowers for giving me the opportunity to work on this Summer of Code.&lt;&#x2F;p&gt;</description>
      </item>
      <item>
          <title>Conduit GSoC Progress</title>
          <pubDate>Thu, 06 Aug 2009 00:00:00 +0000</pubDate>
          <author>Alex Rosenfeld</author>
          <link>https://blog.arsfeld.dev/posts/2009/08/06/conduit-gsoc-progress/</link>
          <guid>https://blog.arsfeld.dev/posts/2009/08/06/conduit-gsoc-progress/</guid>
          <description xml:base="https://blog.arsfeld.dev/posts/2009/08/06/conduit-gsoc-progress/">&lt;p&gt;Thinking about another thing I&amp;#8217;m implementing in Conduit I noticed I totally forgot to blog about my Summer of Code progress. So far there has been some hacking and fixing in several places and now I&amp;#8217;m starting something very cool, which I really want to blog later.&lt;&#x2F;p&gt;
&lt;p&gt;One of the first things I did this Summer of Code was to improve Conduit startup time. Conduit&amp;#8217;s dataproviders come from very different places, such as Flickr, Google, Banshee, iPods, etc. Loading all these modules is very consuming, there are a lot of dependencies and very different requirements for each dataprovider. However, most of the time dataproviders are only used to show their icons in the left-pane, so we dont actually have to load them at all before we really need them. That is just what I did. It was quite easy actually, everything seems to be architectured to do that. All information we really need from each dataprovider was already organized in a central location and in order to work with dataproviders that come and go, the entire UI handles module wrappers instead of modules themselfs, so all we need are module wrappers that load the actual modules when needed. Then on the first Conduit run, all necessary information is pickled into a file in the user cache directory (usually under ~&#x2F;.cache&#x2F;conduit). This file is pretty small, so it&amp;#8217;s way faster to load then all the modules together. And if anything changes with the modules, the file is modified.&lt;&#x2F;p&gt;
&lt;p&gt;Of course, some problems arise. We really do need factories loaded all the time, those are the ones that create dataproviders for removeable devices and things that come and go (and very soon for online accounts). So I also had to split factories from their respective dataproviders. Some corner cases are still unresolved, such as our custom version of the Google library not loading everytime or the possibility of the iPod dataprovider not showing if you install the GPod library after starting Conduit for the first time (I could say &amp;#8220;Who would do that?&amp;#8221;, but I do sometimes 😉 I&amp;#8217;ll probably disable the cache on these cases.&lt;&#x2F;p&gt;
&lt;p&gt;Another thing I just implemented is the ability to save and restore everything from GConf, while we only used GConf for window settings before and kept all syncset information in a XML format. This means that settings are kept updated when you make any change, such as creating new conduits and, very soon, configure dataproviders. The XML format will still be around for any DBus application to use or some future backup service.&lt;&#x2F;p&gt;
&lt;p&gt;Now I&amp;#8217;m implementing an entirelly new UI. My idea was to use DBus to communicate with Conduit, making Conduit a true daemon process, but an idea for the new interface just popped into my head and I had to implement before I changed the DBus API for what I needed. It seems the troubles of using DBus for the GUI are greater then I predicted, but with this new UI it might be possible in the future. I&amp;#8217;m insanely hacking at this new UI to have something 100% functional (hopefully very soon) and I&amp;#8217;m really excited by it. I just hope the Conduit maintainers agree with me 🙂&lt;&#x2F;p&gt;</description>
      </item>
      <item>
          <title>Conduit&#x27;s improvements</title>
          <pubDate>Mon, 25 May 2009 00:00:00 +0000</pubDate>
          <author>Alex Rosenfeld</author>
          <link>https://blog.arsfeld.dev/posts/2009/05/25/conduits-improvements/</link>
          <guid>https://blog.arsfeld.dev/posts/2009/05/25/conduits-improvements/</guid>
          <description xml:base="https://blog.arsfeld.dev/posts/2009/05/25/conduits-improvements/">&lt;p&gt;As GSoC is officially starting yesterday and I made a promess to finish what I begun almost six months ago (sorry about taking so long), last week I commited the last parts of my branch. So, in the end I rewrote the configuration system of Conduit, based on a previous limited implementation that I met when working at Summer of Code 2008. If anyone wants to know if Summer of Code works, it does. I would never knew so much about Conduit had I never entered Summer of Code last year.&lt;&#x2F;p&gt;
&lt;p&gt;Why is the configuration system so important? Well, every dataprovider needs to be configured. What we do now, is that we open a configuration dialog for the user, with the standard configuration widgets: text entries, drop-boxes, item-lists, etc, and, of course, an OK and Cancel button. The problem was that every dataprovider had its own Gtk and Glade code, meaning a lot of duplicate code and usually confusing interfaces, not HIG-compliant at all. On top of that, every dataprovider showed their own configuration dialog, so we couldnt do anything else then displaying the dialog. Fixing all of this meant fixing each one of them.&lt;&#x2F;p&gt;
&lt;p&gt;I started with the idea that in order to be more useful to outside applications, we had to be more flexible and we had to fix our problems once and for all in all configuration dialogs. I knew I would have to change every dataprovider we ship, so I wanted to do it only once. In the end I created a wrapper to Gtk, letting dataproviders only deal with what matter to them. Because we have a limited set of widgets, we can have a much more useful API then dealing with Gtk directly. We also get some benefits:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Fixing code in one place fixes it for all dataproviders.&lt;&#x2F;li&gt;
&lt;li&gt;Being HIG compliant is much easier.&lt;&#x2F;li&gt;
&lt;li&gt;We could change from Gtk to something else in the future much more quickly.&lt;&#x2F;li&gt;
&lt;li&gt;We can handle configuration dialogs on clients in a much better way (think about embedding an iPod sync configuration into Rhythmbox).&lt;&#x2F;li&gt;
&lt;li&gt;We can do something else then dialogs for configuration. Think about a sidebar, a la Glade.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;It was explicitely designed for the last two points, because my first work at Conduit was the iPod module and having the same iTunes experience of syncing it was one of the main goals.&lt;&#x2F;p&gt;
&lt;p&gt;So, I wished to show a screenshot of how it looked before, but it seems we fixed so much of it lately (or we screwed up so much before) I can&amp;#8217;t run older version. Anyway, here is a screenshot of the new code:&lt;&#x2F;p&gt;
&lt;figure id=&quot;attachment_24&quot; aria-describedby=&quot;caption-attachment-24&quot; style=&quot;width: 361px&quot; class=&quot;wp-caption aligncenter&quot;&gt;&lt;img decoding=&quot;async&quot; loading=&quot;lazy&quot; class=&quot;size-full wp-image-24&quot; title=&quot;Conduit Picasa Screenshot&quot; src=&quot;__GHOST_URL__&#x2F;content&#x2F;images&#x2F;wordpress&#x2F;2009&#x2F;05&#x2F;screenshot21.png&quot; alt=&quot;Picasa Screenshot&quot; width=&quot;361&quot; height=&quot;370&quot; srcset=&quot;__GHOST_URL__&#x2F;content&#x2F;images&#x2F;wordpress&#x2F;2009&#x2F;05&#x2F;screenshot21.png 361w, __GHOST_URL__&#x2F;content&#x2F;images&#x2F;wordpress&#x2F;2009&#x2F;05&#x2F;screenshot21-293x300.png 293w&quot; sizes=&quot;(max-width: 361px) 100vw, 361px&quot; &#x2F;&gt;&lt;figcaption id=&quot;caption-attachment-24&quot; class=&quot;wp-caption-text&quot;&gt;Picasa Screenshot&lt;&#x2F;figcaption&gt;&lt;&#x2F;figure&gt;
&lt;p&gt;Of course, we changed a lot of code with the last commits and not without bugs. Our UI is still quite bad, but hopefully we can improve it soon.&lt;&#x2F;p&gt;
&lt;p&gt;I should start thinking about this year&amp;#8217;s Summer of Code now. Hopefully we will have something really good at the end of the summer (my winter actually).&lt;&#x2F;p&gt;</description>
      </item>
      <item>
          <title>Introduction</title>
          <pubDate>Thu, 07 May 2009 00:00:00 +0000</pubDate>
          <author>Alex Rosenfeld</author>
          <link>https://blog.arsfeld.dev/posts/2009/05/07/introduction/</link>
          <guid>https://blog.arsfeld.dev/posts/2009/05/07/introduction/</guid>
          <description xml:base="https://blog.arsfeld.dev/posts/2009/05/07/introduction/">&lt;p&gt;Let me introduce myself, I am Alexandre Rosenfeld, a Computer Engineering student from Brazil, at the University of São Paulo. I have participated on the Google Summer of Code 2008, working on Conduit. Since then I&amp;#8217;ve kept working on Conduit and now I&amp;#8217;ve been selected for the Google Summer of Code 2009, to work on making Conduit a background service. Let me just say that since last year Summer of Code I&amp;#8217;m getting involved in the Gnome community and I&amp;#8217;m enjoying it very much.&lt;&#x2F;p&gt;
&lt;p&gt;I&amp;#8217;d like to start talking about why I&amp;#8217;m involved in Conduit and how I think it solves one of my favorite problems. I&amp;#8217;ve always thought the data in my computer to be mine, just as everybody else, I think. But I always wanted to do stuff with my data, making the computer really useful. But I never could really get what I wanted without coding. So some time ago I had an idea about an application that would allow people to drag and drop components that would process their data in new and interesting ideas. This application would be kind of an script intrepeter, but without writing the script. So, as years went by, I never implemented my idea but I always found something that could be solved by my application or something that resembled it. I was very excited with Yahoo Pipes, as it resembled a lot to what I thought about the interface, but it eventually got me down as it is much different from what I had in mind.&lt;&#x2F;p&gt;
&lt;p&gt;But then I met Conduit. I allows me to pass my data from one point to another, while processing it in different ways. It even knows what kind of data it is passing around and it can make decisions based on that. The first thing I did was to try to sync my iPod, which at the time no application could do something as simple as check what songs was already in the iPod and copy songs not in there. To be honest I was creating scripts for that for a while, but running scripts in the terminal is boring. Eventually that led me to improve the Rhythmbox dataprovider, then the iPod and media classes in Conduit as part of Google Summer of Code 2008 and then I couldn&amp;#8217;t stop having ideas for Conduit. Ok, it isn&amp;#8217;t doing what I dreamed about, but it is pretty close and it is interesting as hell (for me at least).&lt;&#x2F;p&gt;
&lt;p&gt;Everywhere I look I see data in my computer not getting their full potential. I want a more dynamic world, a place where my data is recognized the way they are, rich, full of metadata and meaningfull to me, and it should be presented and handled like that. The reason social websites are all the hype is because they recognized that people&amp;#8217;s data are the most precious things. People post their photos on Facebook and Orkut and it gets tagged and commented. People listen to their music on Last.fm and again, it is tagged, commented and discussed. But when you open your file manager, you get a note icon for your music and your photos appears as simple thumbnails.&lt;&#x2F;p&gt;
&lt;p&gt;Imagine if your applications could interact with your data, no matter where they are, in new and interesting ways, using their full potential? That is where I want Conduit to be, pushing data between applications, web services, devices, etc. It already knows how connect to most of them, all it needs is these things talking together. Applications should be able to use it Conduit as a data service.&lt;&#x2F;p&gt;
&lt;p&gt;My proposal is one step in that direction, bringing Conduit closer to applications. Not a big step, but it&amp;#8217;s a start. I know a lot of people (pretty smart people) are thinking about these things, &lt;a title=&quot;SuperDataDaemonThing&quot; href=&quot;http:&#x2F;&#x2F;live.gnome.org&#x2F;SuperDataDaemonThing&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;SuperDataDaemonThing&lt;&#x2F;a&gt;, &lt;a title=&quot;Nepomuk&quot; href=&quot;http:&#x2F;&#x2F;nepomuk.semanticdesktop.org&#x2F;xwiki&#x2F;bin&#x2F;view&#x2F;Main1&#x2F;&quot;&gt;Nepomuk&lt;&#x2F;a&gt;, &lt;a title=&quot;Tracker&quot; href=&quot;http:&#x2F;&#x2F;projects.gnome.org&#x2F;tracker&#x2F;&quot;&gt;Tracker&lt;&#x2F;a&gt;, etc, and this is just my way of helping.&lt;&#x2F;p&gt;</description>
      </item>
      <item>
          <title>Google Summer of Code 2009</title>
          <pubDate>Tue, 21 Apr 2009 00:00:00 +0000</pubDate>
          <author>Alex Rosenfeld</author>
          <link>https://blog.arsfeld.dev/posts/2009/04/21/google-summer-of-code-2009/</link>
          <guid>https://blog.arsfeld.dev/posts/2009/04/21/google-summer-of-code-2009/</guid>
          <description xml:base="https://blog.arsfeld.dev/posts/2009/04/21/google-summer-of-code-2009/">&lt;p&gt;I have just been approved for Google Summer of Code 2009, which means I can once again work on a great project, the &lt;a title=&quot;Conduit&quot; href=&quot;http:&#x2F;&#x2F;www.conduit-project.org&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Conduit&lt;&#x2F;a&gt; application.&lt;&#x2F;p&gt;
&lt;p&gt;This year my project is &amp;#8220;Making Conduit work as a daemon&amp;#8221;, which is by itself a great project and will allow me to implement some ideas I&amp;#8217;m having for about a year (or at least find out why I can&amp;#8217;t implement them 🙂 )&lt;&#x2F;p&gt;
&lt;p&gt;But what is more important is how I think it helps Gnome into better integrating the Gnome desktop with the online world. Conduit working as a daemon is much more then a synchronization application running in the background, it is a service to access any data, both local and online, indenpendently of API, language or binding, because it is available in a DBus service abstracting all the details. What is more important, is that applications dont have to implement support for every website in the planet, because that is done inside the Conduit API as modules (and plugins, as soon as I finish implementing them).&lt;&#x2F;p&gt;
&lt;p&gt;I would like to details more how all of that is possible and how I think my work can help with that, but I&amp;#8217;ll leave that for later. For now I&amp;#8217;m just celebrating being accepted!&lt;&#x2F;p&gt;</description>
      </item>
      <item>
          <title>Something to fix</title>
          <pubDate>Thu, 09 Apr 2009 00:00:00 +0000</pubDate>
          <author>Alex Rosenfeld</author>
          <link>https://blog.arsfeld.dev/posts/2009/04/09/something-to-fix/</link>
          <guid>https://blog.arsfeld.dev/posts/2009/04/09/something-to-fix/</guid>
          <description xml:base="https://blog.arsfeld.dev/posts/2009/04/09/something-to-fix/">&lt;p&gt;So, this is my first post on this blog. I tried blogging before, but I never wrote much. I didnt have much to write about. But now my life is getting quite busy and I&amp;#8217;m quite excited about many things I&amp;#8217;m doing, so why not try to blog again.&lt;&#x2F;p&gt;
&lt;p&gt;Let&amp;#8217;s start about the title. Well, I just wrote in the About page that I now have a focus in my life and that focus is the computer. More specifically, the human-computer interaction. I&amp;#8217;m not only thinking about user interfaces, altough that is one area I am really interested about, but I am talking about everything that involves a computer in our daily activities. Somehow, I think the computer the way we use them now are just getting in the way. We are making people adapt to the computer, instead of the computer adapting to our workflow.&lt;&#x2F;p&gt;
&lt;p&gt;So, no, I dont know how to fix it, I just think it should be fixed. So, my focus? I want to learn as much as I can about our computer usage now. But I am not just standing still, waiting for something better to come along. No, I believe we can fix it from the inside. So I am also learning as much as I can about programming, which is one of my biggest passions, so I can actually try do something useful now.&lt;&#x2F;p&gt;
&lt;p&gt;Later on I&amp;#8217;ll explain about how that translates to my current work.&lt;&#x2F;p&gt;</description>
      </item>
    </channel>
</rss>
