<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Nite</title>
    <link>https://www.nite07.com/en/</link>
    <description>This is Nite&#39;s home!</description>
    <generator>Hugo 0.164.0 &amp; FixIt v1.0.0-alpha</generator>
    <language>en</language>
    <managingEditor>nite@nite07.com (Nite)</managingEditor>
    <webMaster>nite@nite07.com (Nite)</webMaster>
    <lastBuildDate>Tue, 04 Aug 2026 03:10:52 +1000</lastBuildDate>
    <atom:link href="https://www.nite07.com/en/index.xml" rel="self" type="application/rss+xml" /><item>
      <title>Go GMP Model</title>
      <link>https://www.nite07.com/en/posts/go-gmp/</link>
      <pubDate>Tue, 04 Aug 2026 03:10:52 +1000</pubDate><author>nite@nite07.com (Nite)</author>
      <guid>https://www.nite07.com/en/posts/go-gmp/</guid>
      <category domain="https://www.nite07.com/en/categories/note/">Note</category>
      <description>&lt;p&gt;GMP is the trio of core components in the Go scheduler:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;G (Goroutine): the task created by &lt;code&gt;go func()&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;M (Machine): an OS thread.&lt;/li&gt;&#xA;&lt;li&gt;P (Processor): scheduling context that manages a G queue.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;M is an OS thread. The runtime creates it via &lt;code&gt;newm&lt;/code&gt;/&lt;code&gt;newosproc&lt;/code&gt; (the functions that create M and its underlying thread), and the OS then schedules it onto a CPU core. M is the smallest unit that executes Go code; an M without a P can only sleep or handle system calls.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;g-states-and-lifecycle&#34;&gt;&lt;span&gt;G states and lifecycle&lt;/span&gt;&#xA;  &lt;a href=&#34;#g-states-and-lifecycle&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;A G has a well-defined set of runtime states (the enum in &lt;code&gt;runtime2.go&lt;/code&gt;):&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Gidle: just allocated, not yet initialized.&lt;/li&gt;&#xA;&lt;li&gt;Grunnable: queued in a run queue, waiting for an M to pick it up.&lt;/li&gt;&#xA;&lt;li&gt;Grunning: executing user code.&lt;/li&gt;&#xA;&lt;li&gt;Gsyscall: executing a syscall, temporarily detached from scheduling.&lt;/li&gt;&#xA;&lt;li&gt;Gwaiting: parked (suspended), waiting on a channel, lock, or IO.&lt;/li&gt;&#xA;&lt;li&gt;Gdead: finished, sitting in a free list for reuse.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;pre&gt;&lt;code&gt;flowchart LR&#xA;    A[&amp;#34;Gidle&amp;#34;] --&amp;gt; B[&amp;#34;Grunnable&amp;#34;]&#xA;    B --&amp;gt; C[&amp;#34;Grunning&amp;#34;]&#xA;    C --&amp;gt; D[&amp;#34;Gsyscall&amp;#34;]&#xA;    C --&amp;gt; E[&amp;#34;Gwaiting&amp;#34;]&#xA;    D --&amp;gt; B&#xA;    E --&amp;gt; B&#xA;    C --&amp;gt; F[&amp;#34;Gdead&amp;#34;]&#xA;    F --&amp;gt; B&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A finished G is not destroyed; it goes into a free-G cache for reuse when a new G is created. The cache has two levels: each P keeps a local cache (&lt;code&gt;pp.gFree&lt;/code&gt;), and there is a shared global pool (&lt;code&gt;sched.gFree&lt;/code&gt;, lock-protected). On return, a G goes into the current P&amp;rsquo;s local cache first; on creation, a new G is taken from the local cache first, and only when the local cache is empty are &lt;strong&gt;32 Gs fetched from the global pool&lt;/strong&gt; at once. Fetching in bulk minimizes global-lock contention: the next 32 creations all hit the lock-free local cache.&lt;/p&gt;&#xA;&lt;p&gt;Stack reuse is best-effort. A G&amp;rsquo;s stack grows dynamically and may end up far larger than the initial size. If a G&amp;rsquo;s stack size doesn&amp;rsquo;t match the standard initial stack size, the stack is freed on return; the same happens when a reused G turns out to have a mismatched stack. Only a G whose stack size matches exactly can be reused together with its stack.&lt;/p&gt;&#xA;&lt;p&gt;A goroutine stack starts at only 2KB (&lt;code&gt;stackMin&lt;/code&gt;), far smaller than a thread&amp;rsquo;s default stack (megabytes). That is why G is lightweight and why millions of goroutines are feasible. The stack grows on demand, up to 1GB on 64-bit platforms.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;queue-structure&#34;&gt;&lt;span&gt;Queue structure&lt;/span&gt;&#xA;  &lt;a href=&#34;#queue-structure&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Each P has a local G queue, and there is one shared global G queue. The global queue is lock-protected (&lt;code&gt;sched.lock&lt;/code&gt;, the scheduler lock); the local queue is lock-free. This is the key to scheduling performance.&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;flowchart TB&#xA;    GQ[&amp;#34;Global G queue&amp;lt;br/&amp;gt;lock-protected&amp;#34;]&#xA;    P1[&amp;#34;P1&amp;#34;]&#xA;    P2[&amp;#34;P2&amp;#34;]&#xA;    P3[&amp;#34;P3&amp;#34;]&#xA;    Q1[&amp;#34;Local queue&amp;lt;br/&amp;gt;runnext &amp;#43; runq[256]&amp;#34;]&#xA;    Q2[&amp;#34;Local queue&amp;#34;]&#xA;    Q3[&amp;#34;Local queue&amp;#34;]&#xA;    M1[&amp;#34;M1 OS thread&amp;#34;]&#xA;    M2[&amp;#34;M2 OS thread&amp;#34;]&#xA;    M3[&amp;#34;M3 OS thread (idle)&amp;#34;]&#xA;    CPU[&amp;#34;CPU core&amp;lt;br/&amp;gt;scheduled by OS&amp;#34;]&#xA;    GQ --&amp;gt;|take G| Q1&#xA;    GQ --&amp;gt;|take G| Q2&#xA;    GQ --&amp;gt;|take G| Q3&#xA;    Q1 --- P1&#xA;    Q2 --- P2&#xA;    Q3 --- P3&#xA;    P1 --- M1&#xA;    P2 --- M2&#xA;    P3 -. idle .-&amp;gt; M3&#xA;    M1 --&amp;gt; CPU&#xA;    M2 --&amp;gt; CPU&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A new G goes into the current P&amp;rsquo;s local queue, most preferably into the &lt;code&gt;runnext&lt;/code&gt; slot (the next-to-run position; each P has exactly one, and it may be empty). If &lt;code&gt;runnext&lt;/code&gt; is empty, the new G takes it directly; if it is occupied, the old G is pushed out into the &lt;code&gt;runq&lt;/code&gt; (local run queue) ring buffer. &lt;code&gt;runq&lt;/code&gt; is a fixed-size array of 256, so together with &lt;code&gt;runnext&lt;/code&gt; the P holds 257 Gs.&lt;/p&gt;&#xA;&lt;p&gt;What goes into &lt;code&gt;runnext&lt;/code&gt;: a newly created G (&lt;code&gt;go func()&lt;/code&gt;), a woken G that needs to run soon (&lt;code&gt;ready&lt;/code&gt; called with next=true, e.g. the finalizer G), and a preempted G — so it can resume immediately after the STW ends.&lt;/p&gt;&#xA;&lt;p&gt;This is the locality principle: the new G was just created by the current G, and running it on the same P means its data is likely still in the CPU cache, maximizing hit rate when it runs immediately.&lt;/p&gt;&#xA;&lt;p&gt;When the local queue can&amp;rsquo;t take more (runnext occupied &lt;strong&gt;and&lt;/strong&gt; runq full at 256), &lt;code&gt;runqputslow&lt;/code&gt; (the function that moves half the queue to the global queue) moves &lt;strong&gt;the front half&lt;/strong&gt; (the 128 earliest Gs) together with the displaced old G (the one that was sitting in &lt;code&gt;runnext&lt;/code&gt;, not yet run) to the global queue. Old Gs go, the new G stays: the new G must run soon, so it remains in runnext; the front-half Gs have waited the longest, and once in the global queue they can be picked up by other idle Ps, which doubles as load balancing. So the &amp;ldquo;maximum length of a P&amp;rsquo;s G queue&amp;rdquo; is 256 + 1, not unbounded.&lt;/p&gt;&#xA;&lt;h4 class=&#34;heading-element&#34; id=&#34;why-move-half-at-once-instead-of-just-the-displaced-g&#34;&gt;&lt;span&gt;Why move half at once instead of just the displaced G?&lt;/span&gt;&#xA;  &lt;a href=&#34;#why-move-half-at-once-instead-of-just-the-displaced-g&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h4&gt;&lt;p&gt;Question: the new G takes runnext, so why not just throw the displaced old G into the global queue instead of moving 128 Gs along with it?&lt;/p&gt;&#xA;&lt;p&gt;The answer has two layers:&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;The old G needs a place to go, so runq must free up space.&lt;/strong&gt; The displaced G can&amp;rsquo;t re-enter runnext (the new G holds it) and can only go into runq, which is already full at 256. Making room in runq is a hard requirement, so front-half Gs must move.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Moving in bulk minimizes global-lock contention.&lt;/strong&gt; The global queue has a lock (&lt;code&gt;sched.lock&lt;/code&gt;). If we freed just one slot each time: create a G → runq full again → free one more → take the global lock again. Goroutine creation is a hot path; taking a global lock on every creation is unacceptable. Moving 128 at once takes the lock once, leaving runq half-empty (128 free slots); many subsequent enqueues then go through the lock-free local path until the queue fills up again. Moving half also leaves headroom: runq drops from 256 to 128, so new Gs don&amp;rsquo;t immediately hit the ceiling.&lt;/p&gt;&#xA;&lt;p&gt;Question: idle Ps will steal from a full queue anyway via work stealing — why move anything at all?&lt;/p&gt;&#xA;&lt;p&gt;Stealing has its own cost: when several idle Ps target the same full queue, they contend, and the losers retry, burning CPU. Once half the queue is in the global pool, idle Ps can just call &lt;code&gt;globrunqgetbatch&lt;/code&gt; and grab a batch with one lock acquisition, no fighting. But work stealing is only a fallback — a full queue means production outpaces consumption, and relying on others to steal doesn&amp;rsquo;t unblock this P&amp;rsquo;s enqueue; making room is mandatory.&lt;/p&gt;&#xA;&lt;p&gt;So the core reasons for moving half are &amp;ldquo;no room for the old G&amp;rdquo; plus &amp;ldquo;can&amp;rsquo;t take a global lock on every creation&amp;rdquo;; load balancing is a side benefit.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;number-of-ps-and-ms&#34;&gt;&lt;span&gt;Number of Ps and Ms&lt;/span&gt;&#xA;  &lt;a href=&#34;#number-of-ps-and-ms&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;The number of Ps equals GOMAXPROCS (maximum processors), which defaults to the number of logical CPU cores. It can be changed via the &lt;code&gt;GOMAXPROCS&lt;/code&gt; environment variable or &lt;code&gt;runtime.GOMAXPROCS(n)&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;The number of Ms changes dynamically: a new M is created when there is work to do but not enough idle Ms; idle Ms park in a sleep list (&lt;code&gt;sched.midle&lt;/code&gt;, the idle-M list) waiting to be reused, and ones idle for too long are destroyed. The default cap is 10000, adjustable via &lt;code&gt;runtime/debug.SetMaxThreads&lt;/code&gt; (set max threads).&lt;/p&gt;&#xA;&lt;p&gt;Each M is bound to 0 or 1 P at any moment: only an M holding a P can execute Go code. An M obtains Gs through its bound P, in this order: &lt;code&gt;runnext&lt;/code&gt; → local queue → global queue → steal from other Ps.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;thread-reuse&#34;&gt;&lt;span&gt;Thread reuse&lt;/span&gt;&#xA;  &lt;a href=&#34;#thread-reuse&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;h4 class=&#34;heading-element&#34; id=&#34;work-stealing&#34;&gt;&lt;span&gt;work stealing&lt;/span&gt;&#xA;  &lt;a href=&#34;#work-stealing&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h4&gt;&lt;p&gt;When a P&amp;rsquo;s local queue is empty, the order for getting Gs is: global queue first (one batch at a time via &lt;code&gt;globrunqgetbatch&lt;/code&gt;, sized &lt;code&gt;min(128, globalLen, globalLen/GOMAXPROCS+1)&lt;/code&gt;, where globalLen is the number of Gs currently queued in the global queue; dividing by GOMAXPROCS splits the batch fairly among all Ps, and 128 is the ceiling because the local queue can&amp;rsquo;t hold more), then netpoll (picking up Gs whose IO is ready), and only then stealing from other Ps.&lt;/p&gt;&#xA;&lt;p&gt;When stealing, the thief takes &lt;strong&gt;the front half&lt;/strong&gt; of the target P&amp;rsquo;s &lt;code&gt;runq&lt;/code&gt; (&lt;code&gt;runqgrab&lt;/code&gt; does &lt;code&gt;n = n - n/2&lt;/code&gt;, counting n from head), i.e. the half that entered the queue earliest. If nothing can be stolen and the global queue is empty too, the P joins the idle list and waits. The number of Ps is fixed — Ps are never destroyed; it&amp;rsquo;s Ms that sleep or get destroyed.&lt;/p&gt;&#xA;&lt;p&gt;There&amp;rsquo;s also a fairness guard: every 61 scheduling ticks (&lt;code&gt;schedtick%61 == 0&lt;/code&gt;) the scheduler forces a check of the global queue and takes from it first if non-empty. This prevents two Gs endlessly spawning new Gs in a local queue and starving the Gs in the global queue.&lt;/p&gt;&#xA;&lt;h4 class=&#34;heading-element&#34; id=&#34;hand-off&#34;&gt;&lt;span&gt;hand off&lt;/span&gt;&#xA;  &lt;a href=&#34;#hand-off&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h4&gt;&lt;p&gt;When a G blocks, the handling differs by case:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Ordinary blocking (channel, lock, sleep): the G is parked (suspended until its condition is met), the M stays put and keeps executing the next G from the P&amp;rsquo;s queue. No new M is needed.&lt;/li&gt;&#xA;&lt;li&gt;Syscall blocking: the M enters the syscall together with the G, and the P is released (&lt;code&gt;handoffp&lt;/code&gt;, the hand-off function); a sleeping M is woken to take over the P, or a new one is created if none exists.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;When the syscall returns, the M first tries to get a P back: with an idle P available it just resumes the G; without one, the G goes into the global queue (&lt;code&gt;globrunqput&lt;/code&gt;) and the M sleeps (&lt;code&gt;stopm&lt;/code&gt;) until woken again.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;load-balancing&#34;&gt;&lt;span&gt;Load balancing&lt;/span&gt;&#xA;  &lt;a href=&#34;#load-balancing&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Every new G creation (&lt;code&gt;newproc&lt;/code&gt;, the function that creates a G) calls &lt;code&gt;wakep&lt;/code&gt; (the M-waker): if there is no spinning M right now and an idle P exists, it wakes a sleeping M or creates a new one, binds it to that P, and sends it into spinning mode to hunt for Gs.&lt;/p&gt;&#xA;&lt;p&gt;A spinning M&amp;rsquo;s order for getting Gs: local queue (the idle P it just picked up has an empty queue anyway) → global queue → steal from other Ps.&lt;/p&gt;&#xA;&lt;p&gt;Spinning Ms don&amp;rsquo;t burn CPU forever:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Their count is capped at half the number of busy Ps (&lt;code&gt;2 × spinningMs &amp;lt; GOMAXPROCS - idlePs&lt;/code&gt; must hold for a new one).&lt;/li&gt;&#xA;&lt;li&gt;When neither the global queue nor any P has work, the spinning M returns its P to the idle list and sleeps itself (&lt;code&gt;stopm&lt;/code&gt; → &lt;code&gt;mPark&lt;/code&gt;, thread park) until the next &lt;code&gt;wakep&lt;/code&gt; wakes it.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;sysmon-and-preemption&#34;&gt;&lt;span&gt;sysmon and preemption&lt;/span&gt;&#xA;  &lt;a href=&#34;#sysmon-and-preemption&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;This is not &amp;ldquo;force-kill after timeout&amp;rdquo;. sysmon (the system monitor thread) is a dedicated thread created at runtime startup (&lt;code&gt;newm&lt;/code&gt; runs the &lt;code&gt;sysmon&lt;/code&gt; function), responsible for several things:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Preemption&lt;/strong&gt;: at most every 10ms it checks whether a G has been running on the same P for over 10ms (&lt;code&gt;forcePreemptNS&lt;/code&gt;, the preemption time threshold); if so, it asynchronously preempts via a SIGURG signal (a Unix signal used for preemption, Go 1.14+). The G yields the CPU, goes back to the queue, and will be scheduled again later.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Syscall takeover&lt;/strong&gt;: if a P has been stuck in a syscall for over ~20us, sysmon takes the P back and hands it to another M (working with hand off).&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Netpoll fallback&lt;/strong&gt;: if nobody has polled network events for over 10ms, sysmon polls epoll itself and wakes ready Gs.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Periodic GC&lt;/strong&gt;: if no GC has run within &lt;code&gt;forcegcperiod&lt;/code&gt; (2 minutes), sysmon forces one.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;network-io-and-netpoll&#34;&gt;&lt;span&gt;Network IO and netpoll&lt;/span&gt;&#xA;  &lt;a href=&#34;#network-io-and-netpoll&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Network IO (socket reads/writes) is non-blocking by default in Go, built on IO multiplexing like epoll (Linux) / kqueue (macOS). When a G&amp;rsquo;s read/write isn&amp;rsquo;t ready, &lt;code&gt;netpollblock&lt;/code&gt; calls &lt;code&gt;gopark&lt;/code&gt; to suspend it (waitReasonIOWait), &lt;strong&gt;without occupying an M&lt;/strong&gt;. Ready events are collected by netpoll, which wakes the corresponding Gs and pushes them back into queues.&lt;/p&gt;&#xA;&lt;p&gt;So when many goroutines do network IO at once, Ms are never blocked by IO. This is the core of GMP&amp;rsquo;s support for high-concurrency network services: no matter how many connections, the number of active threads stays around GOMAXPROCS.&lt;/p&gt;&#xA;&lt;p&gt;Note the distinction: file IO and syscall blocking take the other path (hand off, occupying an M), while network IO goes through netpoll and occupies no M.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;scheduling-flow&#34;&gt;&lt;span&gt;Scheduling flow&lt;/span&gt;&#xA;  &lt;a href=&#34;#scheduling-flow&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;ol&gt;&#xA;&lt;li&gt;&lt;code&gt;go func(){...}()&lt;/code&gt;: creates a G, which goes into the current P&amp;rsquo;s &lt;code&gt;runnext&lt;/code&gt; first, then the local queue; when the local queue is full at 256, the front half of Gs plus the displaced old G move to the global queue.&lt;/li&gt;&#xA;&lt;li&gt;The M takes a G from its bound P: &lt;code&gt;runnext&lt;/code&gt; → local queue → global queue → steal from other Ps.&lt;/li&gt;&#xA;&lt;li&gt;G blocks: on ordinary blocking the M keeps executing the next G; on syscall blocking it hands the P off to another M.&lt;/li&gt;&#xA;&lt;li&gt;Syscall returns: the G re-enters a queue, and the M grabs a P or sleeps.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;pre&gt;&lt;code&gt;flowchart LR&#xA;    A[&amp;#34;go func()&amp;#34;] --&amp;gt; B[&amp;#34;create G&amp;#34;]&#xA;    B --&amp;gt; C{&amp;#34;P local queue full?&amp;#34;}&#xA;    C -- no --&amp;gt; D[&amp;#34;put in runnext / local queue&amp;#34;]&#xA;    C -- yes --&amp;gt; E[&amp;#34;front half Gs &amp;#43; displaced old G to global queue&amp;#34;]&#xA;    D --&amp;gt; F[&amp;#34;M takes G from P and runs it&amp;#34;]&#xA;    E --&amp;gt; F&#xA;    F --&amp;gt; G{&amp;#34;G blocks?&amp;#34;}&#xA;    G -- ordinary --&amp;gt; H[&amp;#34;park G, M keeps running next G&amp;#34;]&#xA;    G -- syscall --&amp;gt; I[&amp;#34;hand off P to another M&amp;#34;]&#xA;    H --&amp;gt; F&#xA;    I --&amp;gt; F&lt;/code&gt;&lt;/pre&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;p-states&#34;&gt;&lt;span&gt;P states&lt;/span&gt;&#xA;  &lt;a href=&#34;#p-states&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;A P has four states (&lt;code&gt;runtime2.go&lt;/code&gt;):&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Pidle: in the idle-P list, queue empty, waiting to be picked up by an M.&lt;/li&gt;&#xA;&lt;li&gt;Prunning: held by an M, executing Gs.&lt;/li&gt;&#xA;&lt;li&gt;Pgcstop: GC&amp;rsquo;s STW (stop the world, all threads paused) phase, all Ps stop here.&lt;/li&gt;&#xA;&lt;li&gt;Pdead: destroyed when GOMAXPROCS shrinks; reusable if GOMAXPROCS grows again.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;GC starts with STW: all Ms reach a safe point, Ps transition to Pgcstop, and scheduling pauses. The marking phase then proceeds concurrently and Ps resume running user code. For the full GC picture, see &lt;a href=&#34;https://www.nite07.com/posts/go-gc/&#34;&gt;Go GC mechanism&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;m0-and-g0&#34;&gt;&lt;span&gt;M0 and G0&lt;/span&gt;&#xA;  &lt;a href=&#34;#m0-and-g0&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;M0 is the first M created at program startup (the main thread). It handles initialization (installing signal handlers, creating the sysmon thread, etc.) and never exits — in &lt;code&gt;mexit&lt;/code&gt; (the thread-exit path) m0 is wedged in place until the process ends.&lt;/p&gt;&#xA;&lt;p&gt;G0 is each M&amp;rsquo;s dedicated scheduling goroutine. Every M creation (&lt;code&gt;allocm&lt;/code&gt;, the function that creates an M) also creates its g0, stored in the M&amp;rsquo;s &lt;code&gt;g0&lt;/code&gt; field. g0 enters no queue and occupies no P; it only runs scheduler code (schedule, stack growth, signal handling, etc.). It is independent of P and tied only to its M: when the M blocks, g0 goes with it — there&amp;rsquo;s no &amp;ldquo;where does g0 live&amp;rdquo; problem.&lt;/p&gt;&#xA;&lt;p&gt;Nor is there any &amp;ldquo;m0 and g0 unbinding&amp;rdquo;: g0 always belongs to m0, and m0 enters the scheduling loop through g0.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;hello-world-execution-flow&#34;&gt;&lt;span&gt;Hello world execution flow&lt;/span&gt;&#xA;  &lt;a href=&#34;#hello-world-execution-flow&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;ol&gt;&#xA;&lt;li&gt;Program starts; m0 and its g0 are created.&lt;/li&gt;&#xA;&lt;li&gt;g0 runs &lt;code&gt;schedinit&lt;/code&gt; (scheduler initialization): creates GOMAXPROCS Ps, initializes the global queue.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;newproc&lt;/code&gt; creates the main goroutine and puts it in some P&amp;rsquo;s local queue.&lt;/li&gt;&#xA;&lt;li&gt;m0&amp;rsquo;s g0 enters the scheduling loop, picks up the main goroutine, and m0 switches to running it.&lt;/li&gt;&#xA;&lt;li&gt;main finishes; the program exits.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;runtimetrace&#34;&gt;&lt;span&gt;runtime/trace&lt;/span&gt;&#xA;  &lt;a href=&#34;#runtimetrace&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;import &amp;#34;runtime/trace&amp;#34;&#xA;&#xA;f, _ := os.Create(&amp;#34;trace.out&amp;#34;)&#xA;trace.Start(f)&#xA;defer trace.Stop()&#xA;&#xA;// your code&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After generating the file, run &lt;code&gt;go tool trace trace.out&lt;/code&gt; to open the Web UI, which shows G/P/M scheduling, blocking, syscalls, GC, and other events.&lt;/p&gt;&#xA;</description>
    </item><item>
      <title>Mitigation measures for self-hosted Gitea instances overwhelmed by crawler traffic</title>
      <link>https://www.nite07.com/en/posts/gitea-scraper-mitigation/</link>
      <pubDate>Sun, 02 Aug 2026 18:19:54 +1000</pubDate><author>nite@nite07.com (Nite)</author>
      <guid>https://www.nite07.com/en/posts/gitea-scraper-mitigation/</guid>
      <category domain="https://www.nite07.com/en/categories/tutorials/">Tutorials</category>
      <description>&lt;h3 class=&#34;heading-element&#34; id=&#34;symptoms&#34;&gt;&lt;span&gt;Symptoms&lt;/span&gt;&#xA;  &lt;a href=&#34;#symptoms&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;A self-hosted Gitea started consuming abnormal bandwidth. The provider panel showed the traffic allowance draining at an alarming rate, with the monthly quota running out fast. Aggregating access logs by response size, the git domain accounted for the vast majority of traffic, dominated by &lt;code&gt;/owner/repo/compare/&lt;/code&gt; version-comparison pages with single responses up to 10MB.&lt;/p&gt;&#xA;&lt;p&gt;The crawler pattern was obvious: Amazonbot, Lightpanda, and a rotating pool of IPs faking old Chrome/Firefox user agents, repeatedly fetching blame and compare pages. Most unique IPs made only one or two requests — classic distributed scraping.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;causes&#34;&gt;&lt;span&gt;Causes&lt;/span&gt;&#xA;  &lt;a href=&#34;#causes&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;ol&gt;&#xA;&lt;li&gt;Crawler user agents vary widely, from well-known bots (Amazonbot, Lightpanda) to rotating IPs faking browser UAs.&lt;/li&gt;&#xA;&lt;li&gt;Heavy pages are large — compare pages exceed 10MB uncompressed — so every crawl burns real bandwidth.&lt;/li&gt;&#xA;&lt;li&gt;Per-IP rate limiting does nothing against a rotating IP pool, where most IPs make one or two requests and move on.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;fixes&#34;&gt;&lt;span&gt;Fixes&lt;/span&gt;&#xA;  &lt;a href=&#34;#fixes&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;h4 class=&#34;heading-element&#34; id=&#34;1-caddy-config&#34;&gt;&lt;span&gt;1. Caddy config&lt;/span&gt;&#xA;  &lt;a href=&#34;#1-caddy-config&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h4&gt;&lt;p&gt;The UA blacklist (&lt;code&gt;@block_bots&lt;/code&gt; returns 403 on match) and the heavy-page rate limit (&lt;code&gt;@heavy&lt;/code&gt;, 2 requests per IP per 10s) live in the same site block and run in order. The UA list covers common crawlers and scanners; extend or trim it as needed:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;git.example.com {&#xA;&#x9;# Logging: per-domain access log&#xA;&#x9;import log git.example.com&#xA;&#xA;&#x9;# 1. UA blacklist: known crawlers get 403&#xA;&#x9;@block_bots header_regexp User-Agent &amp;#34;(?i)(SemrushBot|Lightpanda|GPTBot|ClaudeBot|Bytespider|AhrefsBot|MJ12bot|DataForSeoBot|Amazonbot|DotBot|CCBot|cohere-ai|Diffbot|Google-Extended|Meta-ExternalAgent|Meta-ExternalFetcher|Applebot-Extended|Timpibot|ImagesiftBot|Crawl4AI|Scrapy|ChatGLM-Spider|DeepSeekBot|cohere-training-data-crawler|AI2Bot|TikTokSpider|omgili|BLEXBot|Exabot|360Spider|80legs|MegaIndex|Barkrowler|DataCha0s|Nikto|Nmap|Nessus|OpenVAS|Sqlmap|WPScan|Nuclei|Masscan|Shodan|Acunetix|Dirbuster|Whatweb|Havij|Fimap|Jbrofuzz|Zgrab|Censys)&amp;#34;&#xA;&#x9;handle @block_bots {&#xA;&#x9;&#x9;respond 403&#xA;&#x9;}&#xA;&#xA;&#x9;# 2. Rate limit heavy pages (compare/blame etc.) per IP&#xA;&#x9;@heavy path_regexp heavy_path ^/[^/]&amp;#43;/[^/]&amp;#43;/(?:compare|blame|commit|commits|archive|pulls|issues)(?:/|$)&#xA;&#x9;handle @heavy {&#xA;&#x9;&#x9;rate_limit {&#xA;&#x9;&#x9;&#x9;zone dynamic_zone {&#xA;&#x9;&#x9;&#x9;&#x9;key {client_ip}&#xA;&#x9;&#x9;&#x9;&#x9;events 2&#xA;&#x9;&#x9;&#x9;&#x9;window 10s&#xA;&#x9;&#x9;&#x9;}&#xA;&#x9;&#x9;&#x9;log_key&#xA;&#x9;&#x9;}&#xA;&#x9;&#x9;reverse_proxy 100.64.0.10:3000&#xA;&#x9;}&#xA;&#xA;&#x9;# 3. robots.txt: cooperative crawlers read the rules first&#xA;&#x9;handle /robots.txt {&#xA;&#x9;&#x9;respond `User-agent: *&#xA;Disallow: /blame/&#xA;Disallow: /compare/&#xA;Disallow: /commit/&#xA;Disallow: /commits/&#xA;Disallow: /archive/&#xA;Disallow: /pulls/&#xA;Disallow: /issues/`&#xA;&#x9;}&#xA;&#xA;&#x9;# 4. Everything else goes to Gitea&#xA;&#x9;reverse_proxy 100.64.0.10:3000&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;@heavy&lt;/code&gt; path pattern covers compare, blame, commit, and other heavy page types; an IP that exceeds 2 requests in 10 seconds gets a 429. With &lt;code&gt;log_key&lt;/code&gt; enabled, rate-limited requests are logged with their IP, which helps tune the threshold.&lt;/p&gt;&#xA;&lt;h4 class=&#34;heading-element&#34; id=&#34;2-enable-origin-gzip-in-gitea&#34;&gt;&lt;span&gt;2. Enable origin gzip in Gitea&lt;/span&gt;&#xA;  &lt;a href=&#34;#2-enable-origin-gzip-in-gitea&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h4&gt;&lt;p&gt;Add &lt;code&gt;ENABLE_GZIP = true&lt;/code&gt; to the &lt;code&gt;[server]&lt;/code&gt; section of &lt;code&gt;app.ini&lt;/code&gt;, then restart Gitea:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;[server]&#xA;ENABLE_GZIP = true&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;ENABLE_GZIP&lt;/code&gt; compresses runtime-generated content only; static assets are unaffected. Enable it at the origin rather than at the reverse proxy because the path is origin → proxy → visitor, and under bidirectional billing origin compression saves on both legs, while proxy-only compression saves on one. Caddy&amp;rsquo;s &lt;code&gt;reverse_proxy&lt;/code&gt; adds &lt;code&gt;Accept-Encoding: gzip&lt;/code&gt; for clients that omit it and passes the upstream&amp;rsquo;s compressed response through untouched. No &lt;code&gt;encode&lt;/code&gt; directive is needed on the Caddy side.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;references&#34;&gt;&lt;span&gt;References&lt;/span&gt;&#xA;  &lt;a href=&#34;#references&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://github.com/mholt/caddy-ratelimit&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;caddy-ratelimit&lt;i class=&#34;fa-solid fa-external-link-alt fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://docs.gitea.com/administration/config-cheat-sheet&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Gitea config cheat sheet (ENABLE_GZIP)&lt;i class=&#34;fa-solid fa-external-link-alt fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://caddyserver.com/docs/caddyfile/directives/handle&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Caddy handle directive&lt;i class=&#34;fa-solid fa-external-link-alt fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;</description>
    </item><item>
      <title>Gitea custom template inline scripts blocked by CSP nonce</title>
      <link>https://www.nite07.com/en/posts/gitea-csp-nonce-inline-script/</link>
      <pubDate>Sun, 02 Aug 2026 02:03:49 +1000</pubDate><author>nite@nite07.com (Nite)</author>
      <guid>https://www.nite07.com/en/posts/gitea-csp-nonce-inline-script/</guid>
      <category domain="https://www.nite07.com/en/categories/tutorials/">Tutorials</category>
      <description>&lt;h3 class=&#34;heading-element&#34; id=&#34;symptom&#34;&gt;&lt;span&gt;Symptom&lt;/span&gt;&#xA;  &lt;a href=&#34;#symptom&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;After putting the Matomo tracking script into &lt;code&gt;$GITEA_CUSTOM/templates/custom/header.tmpl&lt;/code&gt; (rendered before the closing &lt;code&gt;&amp;lt;/head&amp;gt;&lt;/code&gt; tag), no statistics show up on the site. The browser console reports:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;Executing inline script violates the following Content Security Policy directive &amp;#39;script-src * &amp;#39;nonce-&amp;lt;random&amp;gt;&amp;#39;&amp;#39;. Either the &amp;#39;unsafe-inline&amp;#39; keyword, a hash (&amp;#39;sha256-...&amp;#39;), or a nonce (&amp;#39;nonce-...&amp;#39;) is required to enable inline execution. The action has been blocked.&lt;/code&gt;&lt;/pre&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;cause&#34;&gt;&lt;span&gt;Cause&lt;/span&gt;&#xA;  &lt;a href=&#34;#cause&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Gitea 1.27.0 switched its CSP inline-script policy to a per-request nonce (&lt;a href=&#34;https://github.com/go-gitea/gitea/pull/37232&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;PR #37232&lt;i class=&#34;fa-solid fa-external-link-alt fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;): a random nonce is generated per response and &lt;code&gt;script-src&lt;/code&gt; becomes &lt;code&gt;* &#39;nonce-xxx&#39;&lt;/code&gt;: external scripts still load, but inline &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags only execute when they carry that nonce. Inline scripts in custom templates have no nonce attribute, so the browser blocks them. The &lt;a href=&#34;https://blog.gitea.com/release-of-1.27.0/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;1.27.0 release notes&lt;i class=&#34;fa-solid fa-external-link-alt fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt; mention this too: inline &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags injected by custom templates stop executing until they are updated to carry the nonce.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;fix&#34;&gt;&lt;span&gt;Fix&lt;/span&gt;&#xA;  &lt;a href=&#34;#fix&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Add a nonce attribute to the script tag, using the template variable &lt;code&gt;{{ctx.CspScriptNonce}}&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;&amp;lt;script nonce=&amp;#34;{{ctx.CspScriptNonce}}&amp;#34;&amp;gt;&#xA;  // your inline script, e.g. Matomo tracking code&#xA;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After saving, restart Gitea (just restart the container for container deployments) and refresh the page: the console no longer reports CSP blocking errors, and visitors show up in the statistics dashboard.&lt;/p&gt;&#xA;</description>
    </item><item>
      <title>Patching a Steam Game with Goldberg Steam Emulator</title>
      <link>https://www.nite07.com/en/posts/gse-patching-guide/</link>
      <pubDate>Sun, 26 Jul 2026 12:30:00 +1000</pubDate><author>nite@nite07.com (Nite)</author>
      <guid>https://www.nite07.com/en/posts/gse-patching-guide/</guid>
      <category domain="https://www.nite07.com/en/categories/tutorials/">Tutorials</category>
      <description>&lt;p&gt;Goldberg Steam Emulator (GSE) replaces a game&amp;rsquo;s &lt;code&gt;steam_api(64).dll&lt;/code&gt; so it can run without the Steam client. The game calls the emulator instead of Steam, and the emulator fakes the responses.&lt;/p&gt;&#xA;&lt;p&gt;The original project is no longer actively maintained. The community now uses GBE Fork, an actively developed fork. You also need Steamless to strip the DRM wrapper from the exe.&lt;/p&gt;&#xA;&lt;p&gt;Relevant projects:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://github.com/Detanup01/gbe_fork&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;GBE Fork&lt;i class=&#34;fa-solid fa-external-link-alt fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt; — the emulator this guide uses&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://github.com/atom0s/Steamless&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Steamless&lt;i class=&#34;fa-solid fa-external-link-alt fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt; — SteamDRM wrapper removal tool&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;1-run-steamless-on-the-exe&#34;&gt;&lt;span&gt;1. Run Steamless on the exe&lt;/span&gt;&#xA;  &lt;a href=&#34;#1-run-steamless-on-the-exe&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Steamless removes the SteamDRM Wrapper (Steam Stub) from the exe. You can run it on any exe — if there&amp;rsquo;s no DRM, it skips safely without damaging the file.&lt;/p&gt;&#xA;&lt;p&gt;Download Steamless, open &lt;code&gt;Steamless.exe&lt;/code&gt;, pick the game&amp;rsquo;s main exe, and click unpack. A &lt;code&gt;.unbind&lt;/code&gt; file will appear in the same directory. Back up the original exe, then rename the &lt;code&gt;.unbind&lt;/code&gt; file to replace it.&lt;/p&gt;&#xA;&lt;p&gt;Large exes (over 100MB) can take a few minutes.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;2-find-steam_api64dll&#34;&gt;&lt;span&gt;2. Find steam_api(64).dll&lt;/span&gt;&#xA;  &lt;a href=&#34;#2-find-steam_api64dll&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Search &lt;code&gt;steam_api*.dll&lt;/code&gt; in the game folder.&lt;/p&gt;&#xA;&lt;p&gt;There are two possible scenarios.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Only one DLL (generic / Unity engine):&lt;/strong&gt; replace that DLL. Put &lt;code&gt;steam_settings&lt;/code&gt; next to it.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Two DLLs (Unreal Engine game):&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;&amp;lt;GameDir&amp;gt;\Engine\Binaries\ThirdParty\Steamworks\Steamv157\Win64\steam_api64.dll&#xA;&amp;lt;GameDir&amp;gt;\&amp;lt;GameName&amp;gt;\Binaries\Win64\steam_api64.dll&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;UE loads &lt;code&gt;steam_api64.dll&lt;/code&gt; from the Engine directory at startup, not from the game&amp;rsquo;s binary directory. Replace the Engine one, put &lt;code&gt;steam_settings&lt;/code&gt; in the Engine directory as well. Leave the game&amp;rsquo;s main binary directory DLL as-is.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;3-back-up-the-original-dll&#34;&gt;&lt;span&gt;3. Back up the original DLL&lt;/span&gt;&#xA;  &lt;a href=&#34;#3-back-up-the-original-dll&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Copy the DLL you&amp;rsquo;re about to replace and rename it to &lt;code&gt;steam_api64.dll.old&lt;/code&gt;. You&amp;rsquo;ll need this backup later to generate the interfaces file.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;4-replace-with-the-emulator-dll&#34;&gt;&lt;span&gt;4. Replace with the emulator DLL&lt;/span&gt;&#xA;  &lt;a href=&#34;#4-replace-with-the-emulator-dll&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;From the GBE Fork release package, copy the correct architecture DLL over the original:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;64-bit game: &lt;code&gt;regular\x64\steam_api64.dll&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;32-bit game: &lt;code&gt;regular\x86\steam_api.dll&lt;/code&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;The emulator DLL is about 7–11MB, the original one about 200–400KB. The size difference is obvious.&lt;/p&gt;&#xA;&lt;p&gt;For UE games, leave the game&amp;rsquo;s main directory DLL unchanged.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;5-create-the-steam_settings-directory&#34;&gt;&lt;span&gt;5. Create the steam_settings directory&lt;/span&gt;&#xA;  &lt;a href=&#34;#5-create-the-steam_settings-directory&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Create a &lt;code&gt;steam_settings&lt;/code&gt; folder next to the emulator DLL. UE engine: put it in the Engine directory. Generic engine: next to the game exe.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;6-add-steam_appidtxt&#34;&gt;&lt;span&gt;6. Add steam_appid.txt&lt;/span&gt;&#xA;  &lt;a href=&#34;#6-add-steam_appidtxt&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Find the AppID from the Steam store URL: &lt;code&gt;store.steampowered.com/app/&amp;lt;number&amp;gt;/&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Create &lt;code&gt;steam_settings\steam_appid.txt&lt;/code&gt; containing only the numeric AppID. For example &lt;code&gt;3683770&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;7-generate-steam_interfacestxt-recommended&#34;&gt;&lt;span&gt;7. Generate steam_interfaces.txt (recommended)&lt;/span&gt;&#xA;  &lt;a href=&#34;#7-generate-steam_interfacestxt-recommended&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Mismatched interface versions can cause crashes or missing functionality. Run &lt;code&gt;generate_interfaces_x64.exe&lt;/code&gt; (or the x86 version) from the GBE Fork tools against the backed-up original DLL.&lt;/p&gt;&#xA;&lt;p&gt;Drag &lt;code&gt;steam_api64.dll.old&lt;/code&gt; into the tool window. The tool creates &lt;code&gt;steam_interfaces.txt&lt;/code&gt; in the current directory. Move it into &lt;code&gt;steam_settings\&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Always run this against the original DLL, never the emulator DLL.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;8-optional-configure-ini-files&#34;&gt;&lt;span&gt;8. Optional: configure INI files&lt;/span&gt;&#xA;  &lt;a href=&#34;#8-optional-configure-ini-files&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;The &lt;code&gt;steam_settings.EXAMPLE\&lt;/code&gt; folder has templates. Copy them to &lt;code&gt;steam_settings\&lt;/code&gt; and remove the &lt;code&gt;.EXAMPLE&lt;/code&gt; infix.&lt;/p&gt;&#xA;&lt;p&gt;Key settings:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;configs.user.ini&lt;/code&gt;: &lt;code&gt;local_save_path=./path/relative/to/dll&lt;/code&gt; — saves go to the game directory instead of &lt;code&gt;%appdata%&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;configs.user.ini&lt;/code&gt;: &lt;code&gt;account_name=Player&lt;/code&gt; — in-game username&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;configs.user.ini&lt;/code&gt;: &lt;code&gt;language=english&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;configs.main.ini&lt;/code&gt;: &lt;code&gt;offline=0&lt;/code&gt; — offline mode&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;configs.app.ini&lt;/code&gt;: &lt;code&gt;unlock_all=1&lt;/code&gt; — unlock all DLCs&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;You don&amp;rsquo;t need any INI files for the emulator to work — defaults are sensible.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;common-issues&#34;&gt;&lt;span&gt;Common issues&lt;/span&gt;&#xA;  &lt;a href=&#34;#common-issues&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Game crashes or won&amp;rsquo;t start.&lt;/strong&gt; Check in this order: did Steamless run on the exe, is the DLL in the right place (UE engine), is &lt;code&gt;steam_settings&lt;/code&gt; next to the DLL, was &lt;code&gt;steam_interfaces.txt&lt;/code&gt; generated from the original DLL.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Default saves&lt;/strong&gt; live in &lt;code&gt;%appdata%\GSE Saves\&lt;/code&gt;. Set &lt;code&gt;local_save_path&lt;/code&gt; in &lt;code&gt;configs.user.ini&lt;/code&gt; for per-game portable saves.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;UE engine games:&lt;/strong&gt; replace only the Engine directory DLL, leave the game&amp;rsquo;s main directory DLL alone. Put &lt;code&gt;steam_settings&lt;/code&gt; in the Engine directory too.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Restore original state:&lt;/strong&gt; rename &lt;code&gt;steam_api64.dll.old&lt;/code&gt; back to &lt;code&gt;steam_api64.dll&lt;/code&gt;, delete &lt;code&gt;steam_settings&lt;/code&gt; and &lt;code&gt;steam_appid.txt&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;steam_interfaces.txt&lt;/strong&gt; is created in the tool&amp;rsquo;s current working directory, not the tool&amp;rsquo;s own location. cd to the emulator DLL directory first, then run the tool, then move the file to &lt;code&gt;steam_settings\&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;automation-tools&#34;&gt;&lt;span&gt;Automation tools&lt;/span&gt;&#xA;  &lt;a href=&#34;#automation-tools&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Two community tools if you prefer not to do it manually:&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;ARMGDDN Autocracker&lt;/strong&gt; runs on GBE Fork. Install it, right-click a game folder, and it does everything automatically. Repo: &lt;a href=&#34;https://github.com/KaladinDMP/ARMGDDN-Autocracker&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;https://github.com/KaladinDMP/ARMGDDN-Autocracker&lt;i class=&#34;fa-solid fa-external-link-alt fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;. The author&amp;rsquo;s GitHub was suspended at one point; the latest version may be on Telegram.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;SteamAutoCracker&lt;/strong&gt; wraps Steamless and Goldberg into a single script — one-click DRM removal + DLL replacement. Repo: &lt;a href=&#34;https://github.com/BigBoiCJ/SteamAutoCracker&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;https://github.com/BigBoiCJ/SteamAutoCracker&lt;i class=&#34;fa-solid fa-external-link-alt fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;.&lt;/p&gt;&#xA;</description>
    </item><item>
      <title>Continuously monitor Linux traffic with vnStat and picosnitch</title>
      <link>https://www.nite07.com/en/posts/linux-traffic-monitoring-vnstat-picosnitch/</link>
      <pubDate>Thu, 23 Jul 2026 11:00:00 +1000</pubDate><author>nite@nite07.com (Nite)</author>
      <guid>https://www.nite07.com/en/posts/linux-traffic-monitoring-vnstat-picosnitch/</guid>
      <category domain="https://www.nite07.com/en/categories/tutorials/">Tutorials</category>
      <description>&lt;p&gt;When traffic on a VPS looks abnormal, there are usually two questions: which network interface transferred how much data and when, and which service was actually responsible.&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://humdi.net/vnstat/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;vnStat&lt;i class=&#34;fa-solid fa-external-link-alt fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt; records sent and received traffic per network interface. It is useful for total egress and hourly, daily, or monthly trends. &lt;a href=&#34;https://github.com/elesiuta/picosnitch&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;picosnitch&lt;i class=&#34;fa-solid fa-external-link-alt fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt; records connections per executable, letting you continue down to processes, domains, ports, and byte counts. Both keep collecting data in the background, but at different levels, so they complement each other well.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;vnstat-per-interface-traffic&#34;&gt;&lt;span&gt;vnStat: per-interface traffic&lt;/span&gt;&#xA;  &lt;a href=&#34;#vnstat-per-interface-traffic&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;vnStat is not a packet sniffer. It reads the network-interface counters exposed by the kernel, while &lt;code&gt;vnstatd&lt;/code&gt; periodically writes the data to its database. Resource usage stays low. It retains traffic records at five-minute, hourly, daily, monthly, and yearly resolutions.&lt;/p&gt;&#xA;&lt;p&gt;Install it and start the service on Arch Linux:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;sudo pacman -S vnstat&#xA;sudo systemctl enable --now vnstat&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;On its first start, the service creates database entries for available interfaces. List the ones already being recorded:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;vnstat --dbiflist&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;An interface added later can be explicitly added, for example after Tailscale creates &lt;code&gt;tailscale0&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;sudo vnstat --add -i tailscale0&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Common overview queries:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;# Default summary: today, current month, and all time&#xA;vnstat -i wlan0&#xA;&#xA;# Short summary&#xA;vnstat -i wlan0 -s&#xA;&#xA;# Single parseable line for scripts&#xA;vnstat --oneline -i wlan0&#xA;&#xA;# Last 7 days, 12 hours, and recent months&#xA;vnstat -i wlan0 -d 7&#xA;vnstat -i wlan0 -h 12&#xA;vnstat -i wlan0 -m&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;For the time range of an anomaly, combine &lt;code&gt;--begin&lt;/code&gt; and &lt;code&gt;--end&lt;/code&gt; with daily, hourly, or five-minute lists. With &lt;code&gt;--end&lt;/code&gt;, vnStat adds a &lt;code&gt;sum&lt;/code&gt; line at the end: the total received and transmitted traffic for that range.&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;# Daily records and the range total&#xA;vnstat -i wlan0 -d --begin 2026-07-20 --end 2026-07-31&#xA;&#xA;# Hourly records within a day, precise to the minute&#xA;vnstat -i wlan0 -h --begin &amp;#34;2026-07-28 08:00&amp;#34; --end &amp;#34;2026-07-28 20:00&amp;#34;&#xA;&#xA;# Five-minute records during a peak period&#xA;vnstat -i wlan0 -5 --begin &amp;#34;2026-07-28 12:00&amp;#34; --end &amp;#34;2026-07-28 14:00&amp;#34;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;--begin&lt;/code&gt; accepts &lt;code&gt;YYYY-MM-DD HH:MM&lt;/code&gt;, &lt;code&gt;YYYY-MM-DD&lt;/code&gt;, and &lt;code&gt;today&lt;/code&gt;; &lt;code&gt;--end&lt;/code&gt; accepts the first two formats. They are only available with list output, JSON, or XML.&lt;/p&gt;&#xA;&lt;p&gt;Use JSON when a script needs raw byte counts:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;vnstat -i wlan0 -d --begin 2026-07-28 --end 2026-07-30 --json \&#xA;  | jq &amp;#39;.interfaces[0].traffic.days&amp;#39;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;For a real-time rate or graphs:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;# Press Ctrl&amp;#43;C to show statistics for this run&#xA;vnstat --live -i wlan0&#xA;&#xA;# Terminal bar graph for the past 24 hours&#xA;vnstat -i wlan0 -hg&#xA;&#xA;# Render daily data to a PNG&#xA;vnstati -i wlan0 -d -o ~/wlan0.png&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Five-minute data is retained for only 48 hours by default. To keep fine-grained records of peak periods longer, change &lt;code&gt;5MinuteHours&lt;/code&gt; in &lt;code&gt;/etc/vnstat.conf&lt;/code&gt;, then restart the service:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;5MinuteHours 336&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;sudo systemctl restart vnstat&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;vnStat only records traffic from the point &lt;code&gt;vnstatd&lt;/code&gt; starts running; it cannot recover traffic that passed through an interface earlier.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;picosnitch-per-process-traffic&#34;&gt;&lt;span&gt;picosnitch: per-process traffic&lt;/span&gt;&#xA;  &lt;a href=&#34;#picosnitch-per-process-traffic&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;vnStat can reveal when total interface traffic grows, but it cannot say which service generated it. picosnitch is a userspace daemon using BPF and fanotify. It records network connections by executable and stores them in SQLite. Its records include the executable path and hash, parent process, domain, port, user, and bytes sent and received. It can identify applications running in containers too.&lt;/p&gt;&#xA;&lt;p&gt;The picosnitch upstream project recommends a system-wide pipx installation. On Arch Linux:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;sudo pacman -S python-pipx&#xA;sudo pipx install picosnitch --global&#xA;sudo picosnitch systemd&#xA;sudo systemctl enable --now picosnitch&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It requires Python 3.12 or newer and a Linux kernel capable of running modern libbpf CO-RE programs.&lt;/p&gt;&#xA;&lt;p&gt;There are three ways to view the data:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;# Terminal UI for historical connections&#xA;picosnitch tui&#xA;&#xA;# Web UI, listening on http://localhost:5100 by default&#xA;picosnitch webui&#xA;&#xA;# Live event stream&#xA;sudo picosnitch top&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Configuration is at &lt;code&gt;/etc/picosnitch/config.toml&lt;/code&gt;. The local SQLite database defaults to &lt;code&gt;/var/lib/picosnitch/picosnitch.db&lt;/code&gt; and retains 30 days of history by default. To retain more history, change &lt;code&gt;retention_days&lt;/code&gt; and restart the service:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;[database]&#xA;retention_days = 90&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;sudo systemctl restart picosnitch&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The Web UI listens locally by default. To make it reachable from a LAN or reverse proxy, set its address and port before launching it. Apply appropriate access control before exposing it publicly:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;PICOSNITCH_HOST=0.0.0.0 PICOSNITCH_PORT=5100 picosnitch webui&lt;/code&gt;&lt;/pre&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;investigate-anomalies-with-both-tools&#34;&gt;&lt;span&gt;Investigate anomalies with both tools&lt;/span&gt;&#xA;  &lt;a href=&#34;#investigate-anomalies-with-both-tools&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Start with vnStat to see the traffic that actually passed through an interface, then identify the day or hour when it grew. With that time window narrowed, open picosnitch&amp;rsquo;s TUI or Web UI and inspect the executables, domains, and ports for the same period.&lt;/p&gt;&#xA;&lt;p&gt;For example, if vnStat shows a sharp increase in transmitted traffic through &lt;code&gt;wlan0&lt;/code&gt; across two hours at noon, check that range in picosnitch next. If the traffic is concentrated in &lt;code&gt;caddy&lt;/code&gt;, inspect the clients it connected to; if it is concentrated in a backup job or downloader, continue with that service&amp;rsquo;s logs and configuration. Interface accounting finds the problem. Process accounting identifies its source.&lt;/p&gt;&#xA;</description>
    </item><item>
      <title>Fixing Podman build newuidmap errors on WSL2 Arch Linux</title>
      <link>https://www.nite07.com/en/posts/wsl-rootless-podman-newuidmap/</link>
      <pubDate>Sat, 11 Jul 2026 20:23:42 +1000</pubDate><author>nite@nite07.com (Nite)</author>
      <guid>https://www.nite07.com/en/posts/wsl-rootless-podman-newuidmap/</guid>
      <category domain="https://www.nite07.com/en/categories/tutorials/">Tutorials</category>
      <description>&lt;p&gt;When running rootless Podman inside Arch Linux on WSL2, an image build may fail before it even starts processing the Dockerfile:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;podman build . -t game-crawler:latest&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The error looks like this:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;WARN[0000] &amp;#34;/&amp;#34; is not a shared mount, this could cause issues or missing mounts with rootless containers&#xA;ERRO[0000] running `/usr/sbin/newuidmap 61029 0 1000 1 1 100000 65536`: newuidmap: Could not set caps&#xA;Error: cannot set up namespace using &amp;#34;/usr/sbin/newuidmap&amp;#34;: should have setuid or have filecaps setuid: exit status 1&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;&amp;quot;/&amp;quot; is not a shared mount&lt;/code&gt; line is only a warning. The build fails because of &lt;code&gt;newuidmap: Could not set caps&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;when-this-happens&#34;&gt;&lt;span&gt;When this happens&lt;/span&gt;&#xA;  &lt;a href=&#34;#when-this-happens&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;This is a rootless Podman setup problem, not a Dockerfile problem.&lt;/p&gt;&#xA;&lt;p&gt;Podman&amp;rsquo;s official &lt;a href=&#34;https://github.com/containers/podman/blob/main/docs/tutorials/rootless_tutorial.md&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;rootless tutorial&lt;i class=&#34;fa-solid fa-external-link-alt fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt; explains that rootless Podman needs subordinate UID/GID ranges in &lt;code&gt;/etc/subuid&lt;/code&gt; and &lt;code&gt;/etc/subgid&lt;/code&gt;. The &lt;code&gt;newuidmap(1)&lt;/code&gt; manual says that &lt;code&gt;newuidmap&lt;/code&gt; sets the UID mapping of a user namespace and checks whether the caller is allowed to use those ranges through &lt;code&gt;/etc/subuid&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Rootless Podman therefore needs these helper binaries when creating the container namespace:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;/usr/bin/newuidmap&#xA;/usr/bin/newgidmap&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;On Arch Linux, they come from the &lt;code&gt;shadow&lt;/code&gt; package. If they have neither the setuid bit nor the required file capabilities, an unprivileged user cannot complete the UID/GID mapping step, and &lt;code&gt;podman build&lt;/code&gt; fails while setting up the namespace.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;check-the-current-state&#34;&gt;&lt;span&gt;Check the current state&lt;/span&gt;&#xA;  &lt;a href=&#34;#check-the-current-state&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Check permissions and file capabilities first:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;stat -c &amp;#39;%A %U:%G %a %n&amp;#39; /usr/bin/newuidmap /usr/bin/newgidmap&#xA;getcap /usr/bin/newuidmap /usr/bin/newgidmap&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A broken setup may look like this:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;-rwxr-xr-x root:root 755 /usr/bin/newuidmap&#xA;-rwxr-xr-x root:root 755 /usr/bin/newgidmap&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;getcap&lt;/code&gt; prints nothing.&lt;/p&gt;&#xA;&lt;p&gt;Then check whether the current user has subordinate IDs:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;grep &amp;#34;^$USER:&amp;#34; /etc/subuid /etc/subgid&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A valid setup has entries like these:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;/etc/subuid:nite:100000:65536&#xA;/etc/subgid:nite:100000:65536&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If the user has no entries there, add the subordinate ID ranges first. If the entries already exist and &lt;code&gt;newuidmap/newgidmap&lt;/code&gt; still have no setuid bit or file capabilities, fix the helper permissions.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;fix-it-with-file-capabilities&#34;&gt;&lt;span&gt;Fix it with file capabilities&lt;/span&gt;&#xA;  &lt;a href=&#34;#fix-it-with-file-capabilities&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Give each helper the minimal capability it needs:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;sudo setcap cap_setuid=ep /usr/bin/newuidmap&#xA;sudo setcap cap_setgid=ep /usr/bin/newgidmap&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Verify it:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;getcap /usr/bin/newuidmap /usr/bin/newgidmap&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Expected output:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;/usr/bin/newuidmap cap_setuid=ep&#xA;/usr/bin/newgidmap cap_setgid=ep&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;On Arch Linux, &lt;code&gt;/usr/sbin&lt;/code&gt; is usually a symlink to &lt;code&gt;bin&lt;/code&gt;, so an error mentioning &lt;code&gt;/usr/sbin/newuidmap&lt;/code&gt; is still fixed by changing &lt;code&gt;/usr/bin/newuidmap&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;verify-the-rootless-namespace&#34;&gt;&lt;span&gt;Verify the rootless namespace&lt;/span&gt;&#xA;  &lt;a href=&#34;#verify-the-rootless-namespace&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;There is no need to run a full image build just to test the fix. Use &lt;code&gt;podman unshare&lt;/code&gt; first:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;podman unshare sh -c &amp;#39;echo uid_map:; cat /proc/self/uid_map; echo gid_map:; cat /proc/self/gid_map&amp;#39;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After the fix, the output should look like this:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;uid_map:&#xA;         0       1000          1&#xA;         1     100000      65536&#xA;gid_map:&#xA;         0       1000          1&#xA;         1     100000      65536&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here &lt;code&gt;1000&lt;/code&gt; is the WSL user&amp;rsquo;s UID, and &lt;code&gt;100000 65536&lt;/code&gt; comes from &lt;code&gt;/etc/subuid&lt;/code&gt; and &lt;code&gt;/etc/subgid&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Then retry the build:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;podman build . -t game-crawler:latest&lt;/code&gt;&lt;/pre&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;if-setcap-is-missing&#34;&gt;&lt;span&gt;If setcap is missing&lt;/span&gt;&#xA;  &lt;a href=&#34;#if-setcap-is-missing&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Check whether &lt;code&gt;setcap/getcap&lt;/code&gt; are installed:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;command -v setcap getcap&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If not, install the package that provides them:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;sudo pacman -S libcap&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The traditional fallback is to set the setuid bit on both helpers:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;sudo chmod u&amp;#43;s /usr/bin/newuidmap /usr/bin/newgidmap&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Check it with:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;stat -c &amp;#39;%A %U:%G %n&amp;#39; /usr/bin/newuidmap /usr/bin/newgidmap&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The mode should show &lt;code&gt;-rwsr-xr-x&lt;/code&gt;. If file capabilities work, prefer &lt;code&gt;setcap&lt;/code&gt; because it grants a narrower privilege.&lt;/p&gt;&#xA;</description>
    </item><item>
      <title>Fixing Windows Interop in Arch Linux WSL2</title>
      <link>https://www.nite07.com/en/posts/wsl-arch-interop/</link>
      <pubDate>Thu, 09 Jul 2026 16:33:30 +0800</pubDate><author>nite@nite07.com (Nite)</author>
      <guid>https://www.nite07.com/en/posts/wsl-arch-interop/</guid>
      <category domain="https://www.nite07.com/en/categories/tutorials/">Tutorials</category>
      <description>&lt;h3 class=&#34;heading-element&#34; id=&#34;what-is-wsl-interop&#34;&gt;&lt;span&gt;What Is WSL Interop&lt;/span&gt;&#xA;  &lt;a href=&#34;#what-is-wsl-interop&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;WSL Interop is a core feature of WSL2 that lets you invoke Windows executables directly from within a WSL terminal. For example:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;powershell.exe -NoProfile -Command &amp;#34;Get-Process | Select-Object -First 5&amp;#34;&#xA;winget.exe search nushell&#xA;explorer.exe .&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The Windows filesystem is also accessible through &lt;code&gt;/mnt/c/&lt;/code&gt;, &lt;code&gt;/mnt/d/&lt;/code&gt;, and so on.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;the-problem&#34;&gt;&lt;span&gt;The Problem&lt;/span&gt;&#xA;  &lt;a href=&#34;#the-problem&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;On Arch Linux WSL2, you may find that all of the above commands fail with &amp;ldquo;exec format error&amp;rdquo; or &amp;ldquo;command not found.&amp;rdquo; This happens because WSL Interop registration depends on &lt;code&gt;systemd-binfmt.service&lt;/code&gt;, which gets skipped on Arch Linux due to unmet condition checks — Arch doesn&amp;rsquo;t ship pre-existing binfmt.d configuration files like Ubuntu does.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;fix&#34;&gt;&lt;span&gt;Fix&lt;/span&gt;&#xA;  &lt;a href=&#34;#fix&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Run the following in your WSL terminal:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;sudo mkdir -p /etc/binfmt.d&#xA;sudo tee /etc/binfmt.d/wsl-interop.conf &amp;gt; /dev/null &amp;lt;&amp;lt; &amp;#39;EOF&amp;#39;&#xA;:WSLInterop:M::MZ::/init:P&#xA;EOF&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then restart the service:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;sudo systemctl restart systemd-binfmt&lt;/code&gt;&lt;/pre&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;verification&#34;&gt;&lt;span&gt;Verification&lt;/span&gt;&#xA;  &lt;a href=&#34;#verification&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;# Check service status — should be active (exited), not skipped&#xA;systemctl status systemd-binfmt&#xA;&#xA;# Check that WSLInterop is registered&#xA;cat /proc/sys/fs/binfmt_misc/WSLInterop&#xA;&#xA;# Test invoking a Windows program&#xA;powershell.exe -NoProfile -Command &amp;#34;Write-Output &amp;#39;interop OK&amp;#39;&amp;#34;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The configuration file is persistent and takes effect automatically every time WSL starts — no need to repeat this step.&lt;/p&gt;&#xA;</description>
    </item><item>
      <title>Configuring Fail2Ban for a Podman Mail Server with UFW Bans</title>
      <link>https://www.nite07.com/en/posts/fail2ban-postfix-podman/</link>
      <pubDate>Thu, 09 Jul 2026 15:30:00 +0800</pubDate><author>nite@nite07.com (Nite)</author>
      <guid>https://www.nite07.com/en/posts/fail2ban-postfix-podman/</guid>
      <category domain="https://www.nite07.com/en/categories/tutorials/">Tutorials</category>
      <description>&lt;p&gt;My self-hosted mail server, running Postfix and Dovecot, lives in a rootless Podman container on a netcup VPS. A Postfix log summary showed 2,477 rejected messages in one day, with 2,471 coming from the same IP. There were also 152 failed SASL LOGIN attempts, along with scanning, relay abuse, and TLS probes.&lt;/p&gt;&#xA;&lt;p&gt;Postfix can reject spam itself, but SASL password guessing is a separate problem. Fail2Ban identifies sources with repeated failures in the logs and tells the firewall to ban them. Since the server already uses UFW to manage inbound rules, Fail2Ban uses its built-in &lt;code&gt;ufw&lt;/code&gt; action instead of writing directly to iptables.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;environment&#34;&gt;&lt;span&gt;Environment&lt;/span&gt;&#xA;  &lt;a href=&#34;#environment&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;table&gt;&#xA;&#x9;&lt;thead&gt;&#xA;&#x9;&#x9;&#x9;&lt;tr&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;th&gt;Item&lt;/th&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;th&gt;Value&lt;/th&gt;&#xA;&#x9;&#x9;&#x9;&lt;/tr&gt;&#xA;&#x9;&lt;/thead&gt;&#xA;&#x9;&lt;tbody&gt;&#xA;&#x9;&#x9;&#x9;&lt;tr&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;Host OS&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;Arch Linux&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&lt;/tr&gt;&#xA;&#x9;&#x9;&#x9;&lt;tr&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;Container runtime&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;Rootless Podman&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&lt;/tr&gt;&#xA;&#x9;&#x9;&#x9;&lt;tr&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;Mail server image&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;code&gt;ghcr.io/docker-mailserver/docker-mailserver:latest&lt;/code&gt;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&lt;/tr&gt;&#xA;&#x9;&#x9;&#x9;&lt;tr&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;Mail server service&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;code&gt;systemd-mailserver&lt;/code&gt;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&lt;/tr&gt;&#xA;&#x9;&#x9;&#x9;&lt;tr&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;Quadlet file&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;code&gt;~/pod/docker-mailserver/mailserver.container&lt;/code&gt;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&lt;/tr&gt;&#xA;&#x9;&#x9;&#x9;&lt;tr&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;Host mail log&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;code&gt;~/pod/docker-mailserver/logs/mail.log&lt;/code&gt;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&lt;/tr&gt;&#xA;&#x9;&#x9;&#x9;&lt;tr&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;Container mail log&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;&lt;code&gt;/var/log/mail/mail.log&lt;/code&gt;&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&lt;/tr&gt;&#xA;&#x9;&#x9;&#x9;&lt;tr&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;Fail2Ban&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;1.1.0&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&lt;/tr&gt;&#xA;&#x9;&#x9;&#x9;&lt;tr&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;Firewall&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;UFW&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&lt;/tr&gt;&#xA;&#x9;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;1-install-and-inspect-the-current-state&#34;&gt;&lt;span&gt;1. Install and inspect the current state&lt;/span&gt;&#xA;  &lt;a href=&#34;#1-install-and-inspect-the-current-state&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Install and start Fail2Ban:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;sudo pacman -S fail2ban&#xA;sudo systemctl enable --now fail2ban&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Confirm that UFW is enabled, Fail2Ban is running, and see which jails are active:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;sudo ufw status verbose&#xA;sudo systemctl is-active fail2ban&#xA;sudo fail2ban-client status&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;UFW should report &lt;code&gt;Status: active&lt;/code&gt;. Once this configuration is complete, the jail list includes:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;Status&#xA;|- Number of jail:  3&#xA;`- Jail list:       postfix, postfix-sasl, sshd&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;backend&lt;/code&gt; and &lt;code&gt;banaction&lt;/code&gt; are separate settings:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;backend&lt;/code&gt; determines where Fail2Ban reads failure records, such as journald or a regular log file.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;banaction&lt;/code&gt; determines how an IP is banned once it reaches the threshold.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;The mail container writes logs to a host-mounted file, so the mail jails need &lt;code&gt;backend = polling&lt;/code&gt;. Switching to UFW does not change log collection.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;2-make-the-postfix-jails-read-the-container-log&#34;&gt;&lt;span&gt;2. Make the Postfix jails read the container log&lt;/span&gt;&#xA;  &lt;a href=&#34;#2-make-the-postfix-jails-read-the-container-log&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;A traditional Postfix installation usually writes logs to &lt;code&gt;/var/log/mail.log&lt;/code&gt;. This path inside the container is not visible to Fail2Ban on the host, so use the actual host-side volume path instead.&lt;/p&gt;&#xA;&lt;p&gt;Fail2Ban on Arch selects the systemd log backend for Postfix through &lt;code&gt;paths-arch.conf&lt;/code&gt; by default. The logs here are files, so explicitly configure &lt;code&gt;polling&lt;/code&gt; in a local override.&lt;/p&gt;&#xA;&lt;p&gt;Create or edit &lt;code&gt;/etc/fail2ban/jail.d/mailserver.local&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;[DEFAULT]&#xA;postfix_backend = polling&#xA;postfix_log = /home/nite/pod/docker-mailserver/logs/mail.log&#xA;&#xA;[postfix-sasl]&#xA;enabled   = true&#xA;mode      = auth&#xA;port      = smtp,465,submission,imap,imaps,pop3,pop3s&#xA;logpath   = /home/nite/pod/docker-mailserver/logs/mail.log&#xA;backend   = polling&#xA;maxretry  = 5&#xA;findtime  = 10m&#xA;bantime   = 1h&#xA;banaction = ufw&#xA;&#xA;[postfix]&#xA;enabled   = true&#xA;mode      = more&#xA;port      = smtp,465,submission&#xA;logpath   = /home/nite/pod/docker-mailserver/logs/mail.log&#xA;backend   = polling&#xA;maxretry  = 5&#xA;findtime  = 10m&#xA;bantime   = 1h&#xA;banaction = ufw&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Use the &lt;code&gt;.local&lt;/code&gt; suffix. Fail2Ban reads &lt;code&gt;jail.conf&lt;/code&gt;, &lt;code&gt;jail.d/*.conf&lt;/code&gt;, &lt;code&gt;jail.local&lt;/code&gt;, and &lt;code&gt;jail.d/*.local&lt;/code&gt; in order, so this local file overrides the defaults last.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;3-change-the-sshd-banning-action-to-ufw&#34;&gt;&lt;span&gt;3. Change the sshd banning action to UFW&lt;/span&gt;&#xA;  &lt;a href=&#34;#3-change-the-sshd-banning-action-to-ufw&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Do not edit &lt;code&gt;/etc/fail2ban/jail.conf&lt;/code&gt;; it is package-provided and may be overwritten during an update.&lt;/p&gt;&#xA;&lt;p&gt;To make UFW the default action for jails, edit &lt;code&gt;/etc/fail2ban/jail.local&lt;/code&gt; and set this in &lt;code&gt;[DEFAULT]&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;[DEFAULT]&#xA;banaction = ufw&#xA;banaction_allports = ufw&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;To switch only the SSH jail, use this instead:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;[sshd]&#xA;banaction = ufw&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Postfix and postfix-sasl already set &lt;code&gt;banaction = ufw&lt;/code&gt; individually in &lt;code&gt;mailserver.local&lt;/code&gt;, so they do not depend on the global setting.&lt;/p&gt;&#xA;&lt;p&gt;Fail2Ban&amp;rsquo;s built-in UFW action calls &lt;code&gt;ufw&lt;/code&gt; to add and remove rules. There is no longer a need to inspect Fail2Ban ban chains with &lt;code&gt;iptables -L&lt;/code&gt;; check UFW directly instead.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;4-validate-the-configuration-then-restart-the-jails&#34;&gt;&lt;span&gt;4. Validate the configuration, then restart the jails&lt;/span&gt;&#xA;  &lt;a href=&#34;#4-validate-the-configuration-then-restart-the-jails&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Parse the configuration after editing:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;sudo fail2ban-client -d &amp;gt;/dev/null&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This command only checks the parsed result; it does not modify the firewall.&lt;/p&gt;&#xA;&lt;p&gt;After changing an action, restart the service completely:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;sudo systemctl restart fail2ban&lt;/code&gt;&lt;/pre&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;5-verify-logs-actions-and-ufw-rules&#34;&gt;&lt;span&gt;5. Verify logs, actions, and UFW rules&lt;/span&gt;&#xA;  &lt;a href=&#34;#5-verify-logs-actions-and-ufw-rules&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Confirm that the mail jails are reading the host log file:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;sudo fail2ban-client status postfix-sasl&#xA;sudo fail2ban-client status postfix&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;File list&lt;/code&gt; output should be:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;/home/nite/pod/docker-mailserver/logs/mail.log&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;To test whether the SASL filter matches existing log entries without issuing a ban:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;sudo fail2ban-regex /home/nite/pod/docker-mailserver/logs/mail.log &amp;#34;postfix[mode=auth]&amp;#34;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After restarting the service, inspect the runtime actions instead of relying only on the configuration files:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;sudo systemctl is-active fail2ban&#xA;sudo fail2ban-client get sshd actions&#xA;sudo fail2ban-client get postfix actions&#xA;sudo fail2ban-client get postfix-sasl actions&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The actual output in this case was:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;active&#xA;The jail sshd has the following actions:&#xA;ufw&#xA;The jail postfix has the following actions:&#xA;ufw&#xA;The jail postfix-sasl has the following actions:&#xA;ufw&lt;/code&gt;&lt;/pre&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;6-run-an-end-to-end-test&#34;&gt;&lt;span&gt;6. Run an end-to-end test&lt;/span&gt;&#xA;  &lt;a href=&#34;#6-run-an-end-to-end-test&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;pre&gt;&lt;code&gt;sudo fail2ban-client set sshd banip 192.0.2.1&#xA;sudo ufw status numbered | grep -F 192.0.2.1&#xA;sudo fail2ban-client set sshd unbanip 192.0.2.1&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The second command should show a UFW rule with a &lt;code&gt;by Fail2Ban&lt;/code&gt; comment. The third command removes the test ban immediately.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;references&#34;&gt;&lt;span&gt;References&lt;/span&gt;&#xA;  &lt;a href=&#34;#references&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://github.com/fail2ban/fail2ban/blob/1.1.0/config/action.d/ufw.conf&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Fail2Ban 1.1.0 UFW action&lt;i class=&#34;fa-solid fa-external-link-alt fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://github.com/fail2ban/fail2ban/blob/1.1.0/config/jail.conf&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Fail2Ban 1.1.0 default jail configuration&lt;i class=&#34;fa-solid fa-external-link-alt fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;</description>
    </item><item>
      <title>Hide the Console Window When Auto-Starting WSL2</title>
      <link>https://www.nite07.com/en/posts/wsl-autostart-hidden/</link>
      <pubDate>Thu, 09 Jul 2026 02:00:00 +0800</pubDate><author>nite@nite07.com (Nite)</author>
      <guid>https://www.nite07.com/en/posts/wsl-autostart-hidden/</guid>
      <category domain="https://www.nite07.com/en/categories/tutorials/">Tutorials</category>
      <description>&lt;p&gt;WSL2 automatically stops the instance when all terminal windows are closed. While you can mitigate this by setting &lt;code&gt;vmIdleTimeout=-1&lt;/code&gt; and &lt;code&gt;instanceIdleTimeout=-1&lt;/code&gt; in &lt;code&gt;.wslconfig&lt;/code&gt;, some versions still terminate the instance. A reliable approach is to launch a persistent process at login to keep the instance alive, but running &lt;code&gt;wsl.exe&lt;/code&gt; directly through Task Scheduler pops up a black console window. This post covers wrapping it in a VBS script to hide that window.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;create-the-vbs-launch-script&#34;&gt;&lt;span&gt;Create the VBS Launch Script&lt;/span&gt;&#xA;  &lt;a href=&#34;#create-the-vbs-launch-script&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Create a VBS script file on Windows, for example &lt;code&gt;C:\Users\YourUsername\start-wsl.vbs&lt;/code&gt;, with the following content:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;Set WshShell = CreateObject(&amp;#34;WScript.Shell&amp;#34;)&#xA;WshShell.Run &amp;#34;wsl.exe -d archlinux -u root /bin/bash -c &amp;#34;&amp;#34;sleep infinity&amp;#34;&amp;#34;&amp;#34;, 0, False&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Replace &lt;code&gt;archlinux&lt;/code&gt; with your WSL distribution name (run &lt;code&gt;wsl -l&lt;/code&gt; in PowerShell to check).&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;WshShell.Run&lt;/code&gt; parameter explanation:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Second parameter &lt;code&gt;0&lt;/code&gt; — hide the window, no UI shown&lt;/li&gt;&#xA;&lt;li&gt;Third parameter &lt;code&gt;False&lt;/code&gt; — do not wait for the child process to finish, the script exits immediately&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;create-the-scheduled-task&#34;&gt;&lt;span&gt;Create the Scheduled Task&lt;/span&gt;&#xA;  &lt;a href=&#34;#create-the-scheduled-task&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Run the following commands in PowerShell to create a scheduled task that runs at logon:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;$action = New-ScheduledTaskAction -Execute &amp;#34;wscript.exe&amp;#34; -Argument &amp;#39;&amp;#34;C:\Users\YourUsername\start-wsl.vbs&amp;#34;&amp;#39;&#xA;$trigger = New-ScheduledTaskTrigger -AtLogOn&#xA;$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit (New-TimeSpan -Seconds 0)&#xA;Register-ScheduledTask -TaskName &amp;#34;WSL2 AutoStart&amp;#34; -Action $action -Trigger $trigger -Settings $settings&lt;/code&gt;&lt;/pre&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;verification&#34;&gt;&lt;span&gt;Verification&lt;/span&gt;&#xA;  &lt;a href=&#34;#verification&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Log off and back on (or reboot), then run &lt;code&gt;wsl -l --running&lt;/code&gt; in PowerShell to confirm the WSL instance is running. No console window will appear during the process.&lt;/p&gt;&#xA;</description>
    </item><item>
      <title>mihomo TUN Mode Breaks Networking: ip_forward, UFW, and auto-redirect</title>
      <link>https://www.nite07.com/en/posts/mihomo-tun-no-network/</link>
      <pubDate>Tue, 30 Jun 2026 03:15:00 +1000</pubDate><author>nite@nite07.com (Nite)</author>
      <guid>https://www.nite07.com/en/posts/mihomo-tun-no-network/</guid>
      <category domain="https://www.nite07.com/en/categories/tutorials/">Tutorials</category>
      <description>&lt;p&gt;When using mihomo in TUN mode (&lt;code&gt;stack: system&lt;/code&gt;, &lt;code&gt;auto-route: true&lt;/code&gt;), enabling it causes a complete network outage — SSH drops too. The problem isn&amp;rsquo;t in the mihomo config itself, but in system network forwarding settings that aren&amp;rsquo;t configured.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;cause&#34;&gt;&lt;span&gt;Cause&lt;/span&gt;&#xA;  &lt;a href=&#34;#cause&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;stack: system&lt;/code&gt; makes mihomo rely on the kernel networking stack to forward TUN traffic. &lt;code&gt;auto-route: true&lt;/code&gt; takes over the default route and funnels all traffic into the TUN interface. But the kernel needs three conditions to forward:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;code&gt;net.ipv4.ip_forward&lt;/code&gt; must be &lt;code&gt;1&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;The firewall forward chain must not DROP&lt;/li&gt;&#xA;&lt;li&gt;The kernel must not drop TCP packets returning from the TUN interface due to rp_filter&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;Arch Linux defaults to &lt;code&gt;ip_forward=0&lt;/code&gt;, and UFW defaults to &lt;code&gt;DEFAULT_FORWARD_POLICY=&amp;quot;DROP&amp;quot;&lt;/code&gt;. Both block forwarding. Even after fixing those two, on kernel 6.x ping works but curl times out — TCP handshake packets returning from the TUN interface are dropped by &lt;code&gt;rp_filter=1&lt;/code&gt;. The mihomo log shows &lt;code&gt;[TCP] ... using DIRECT&lt;/code&gt;, but the connection just times out.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;fix&#34;&gt;&lt;span&gt;Fix&lt;/span&gt;&#xA;  &lt;a href=&#34;#fix&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;h4 class=&#34;heading-element&#34; id=&#34;1-enable-ip_forward&#34;&gt;&lt;span&gt;1. Enable ip_forward&lt;/span&gt;&#xA;  &lt;a href=&#34;#1-enable-ip_forward&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h4&gt;&lt;pre&gt;&lt;code&gt;echo &amp;#39;net.ipv4.ip_forward = 1&amp;#39; | sudo tee /etc/sysctl.d/99-mihomo.conf&#xA;sudo sysctl -p /etc/sysctl.d/99-mihomo.conf&lt;/code&gt;&lt;/pre&gt;&lt;h4 class=&#34;heading-element&#34; id=&#34;2-change-ufw-forward-policy-to-accept&#34;&gt;&lt;span&gt;2. Change UFW forward policy to ACCEPT&lt;/span&gt;&#xA;  &lt;a href=&#34;#2-change-ufw-forward-policy-to-accept&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h4&gt;&lt;pre&gt;&lt;code&gt;sudo sed -i &amp;#39;s/DEFAULT_FORWARD_POLICY=&amp;#34;DROP&amp;#34;/DEFAULT_FORWARD_POLICY=&amp;#34;ACCEPT&amp;#34;/&amp;#39; /etc/default/ufw&#xA;sudo ufw reload&lt;/code&gt;&lt;/pre&gt;&lt;h4 class=&#34;heading-element&#34; id=&#34;3-enable-auto-redirect&#34;&gt;&lt;span&gt;3. Enable auto-redirect&lt;/span&gt;&#xA;  &lt;a href=&#34;#3-enable-auto-redirect&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h4&gt;&lt;p&gt;Add &lt;code&gt;auto-redirect: true&lt;/code&gt; to the &lt;code&gt;tun&lt;/code&gt; section in config.yaml:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;tun:&#xA;  enable: true&#xA;  stack: system&#xA;  auto-route: true&#xA;  auto-redirect: true&#xA;  auto-detect-interface: true&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;auto-redirect&lt;/code&gt; automatically configures nftables rules to redirect TCP connections, bypassing rp_filter&amp;rsquo;s filtering of return packets from the TUN interface. Restart mihomo after changing.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;alternative&#34;&gt;&lt;span&gt;Alternative&lt;/span&gt;&#xA;  &lt;a href=&#34;#alternative&#34; class=&#34;heading-mark&#34;&gt;&lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;If you don&amp;rsquo;t want to change system forwarding settings, switch the TUN stack:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;mixed&lt;/code&gt;: TCP uses the system stack, UDP uses gvisor — better compatibility than system, better performance than pure gvisor&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;gvisor&lt;/code&gt;: handles TCP/IP entirely in userspace without relying on kernel forwarding — best compatibility but slightly lower performance&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Change &lt;code&gt;tun.stack&lt;/code&gt; in config.yaml from &lt;code&gt;system&lt;/code&gt; to &lt;code&gt;mixed&lt;/code&gt; or &lt;code&gt;gvisor&lt;/code&gt;. No system configuration changes needed.&lt;/p&gt;&#xA;</description>
    </item>
  </channel>
</rss>
