<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Code is cheap, let&#39;s talk</title>
    <link>https://blog.ferstar.org/en/</link>
    <description>Code is cheap, let&#39;s talk</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en</language>
    <copyright>Copyright 2026 ferstar</copyright>
    <lastBuildDate>Sat, 09 May 2026 14:38:00 +0800</lastBuildDate>
    <ttl>60</ttl><atom:link href="https://blog.ferstar.org/en/index.xml" rel="self" type="application/rss+xml" /><image>
      <url>https://blog.ferstar.org/site-logo.png</url>
      <title>Code is cheap, let&#39;s talk</title>
      <link>https://blog.ferstar.org/</link>
    </image>
    
    <item>
      <title>Putting Semantic Search into an AI Coding Harness: Notes on Open-Sourcing ace-wrapper</title>
      <link>https://blog.ferstar.org/en/posts/ace-wrapper-semantic-search-ai-coding-harness/</link>
      <pubDate>Sat, 09 May 2026 14:38:00 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/en/posts/ace-wrapper-semantic-search-ai-coding-harness/</guid>
      <description>Long AI coding tasks often fail because the agent reads the wrong files; use ace-wrapper to put semantic retrieval into Read -&gt; Search -&gt; Change -&gt; Verify; let agents find candidate files first, then verify evidence to reduce blind edits and wasted context.</description><content:encoded><![CDATA[<blockquote><p>I am not a native English speaker; this article was translated by AI.</p>
</blockquote><p>In the <a href="/en/posts/ai-coding-harness-engineering-workflow/" >previous post</a> about Harness Engineering, I compressed my default AI coding workflow into a few steps:</p>
<ol>
<li>Read</li>
<li>Search</li>
<li>Change</li>
<li>Verify</li>
<li>Record</li>
</ol>
<p>The easiest one to underestimate is <code>Search</code>.</p>
<p>Many agents fail not because they cannot edit code, but because they read the wrong place first. The user describes a behavior, a bug, or a cross-layer workflow, while the code may not contain a function with the same name. Running <code>rg login</code>, <code>rg upload</code>, or <code>rg session</code> is fast, but it only works when the keyword is already known. If the keyword is unknown, speed just helps the agent drift faster.</p>
<p>So I open-sourced a small layer I have been using recently:</p>
<p><a href="https://github.com/ferstar/ace-wrapper"  target="_blank" rel="noreferrer">ferstar/ace-wrapper</a></p>
<p>It does one narrow thing: wrap Augment Context Engine’s filesystem context search as an <code>ace</code> command, so coding agents can run semantic retrieval from the shell before editing.</p>

<h3 class="relative group">Why this layer exists
    <div id="why-this-layer-exists" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#why-this-layer-exists" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>The target is concrete: make the search action part of the harness.</p>
<p>I used to see this path often:</p>
<pre class="not-prose mermaid">
flowchart LR
  A[User describes behavior] --> B[Agent guesses keywords]
  B --> C[Reads nearby files]
  C --> D[Edits plausible code]
  D --> E[Verification fails]
  E --> B
</pre>

<p>The problem with this loop is that, after failure, the agent often keeps circling around the same wrong files. It can edit code; what it needs is a better entry point into candidate files. Put less politely, it is working hard after entering the wrong door.</p>
<p><code>ace-wrapper</code> is meant to patch this part:</p>
<pre class="not-prose mermaid">
flowchart LR
  A[User describes behavior] --> B[ace semantic retrieval]
  B --> C[Candidate files]
  C --> D[Read returned files]
  D --> E[rg / tests confirm evidence]
  E --> F[Small patch]
  F --> G[Verify]
</pre>

<p>The important part is the order: <code>ace</code> only finds candidate files. Conclusions still require reading files, exact search, and tests. It is not an answer generator; it just helps the agent waste fewer steps.</p>

<h3 class="relative group">Usage is short
    <div id="usage-is-short" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#usage-is-short" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>Install it:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">uv tool install ace-wrapper</span></span></code></pre></div></div>
<p>Install a local development checkout:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">uv tool install /path/to/ace-wrapper</span></span></code></pre></div></div>
<p>Search for a workflow when the exact keyword is unknown:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">timeout 60s ace <span class="s2">"user uploads an unsupported file and should see skipped-file feedback"</span> -w /repo
</span></span><span class="line"><span class="cl">rg -n <span class="s2">"unsupported|skipped|upload|file"</span> /repo</span></span></code></pre></div></div>
<p>The first command answers “which files may be relevant.” The second command confirms “which identifiers, events, copy, or tests actually exist in the code.”</p>
<p>I usually put this rule into a project’s <code>AGENTS.md</code>:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Use `timeout 60s ace "<query>" -w <repo-root>` for semantic codebase discovery.
</span></span><span class="line"><span class="cl">Treat `ace` results as candidate files.
</span></span><span class="line"><span class="cl">After it returns results, read the relevant files and use exact search before using them as evidence.</span></span></code></pre></div></div>
<p>These lines work better than “read more context,” because they give the agent a concrete action and a boundary against false conclusions.</p>

<h3 class="relative group">How it works with rg
    <div id="how-it-works-with-rg" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#how-it-works-with-rg" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p><code>ace</code> and <code>rg</code> work better as consecutive steps.</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Scenario</th>
          <th style="text-align: left">Use first</th>
          <th style="text-align: left">Why</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">You know the behavior but not the implementation location</td>
          <td style="text-align: left"><code>ace</code></td>
          <td style="text-align: left">Behavior descriptions can find candidate entry points across files and naming styles</td>
      </tr>
      <tr>
          <td style="text-align: left">You know the function name, event name, or error text</td>
          <td style="text-align: left"><code>rg</code></td>
          <td style="text-align: left">It is exact, complete, and enumerable</td>
      </tr>
      <tr>
          <td style="text-align: left">You need a structural refactor</td>
          <td style="text-align: left"><code>ast-grep</code></td>
          <td style="text-align: left">AST-level matching is needed; textual proximity falls short</td>
      </tr>
      <tr>
          <td style="text-align: left">You need to confirm whether a feature exists</td>
          <td style="text-align: left"><code>ace</code> + read files + <code>rg</code></td>
          <td style="text-align: left">A semantic hit cannot prove the feature exists</td>
      </tr>
  </tbody>
</table>
<p>I intentionally wrote this boundary into the README: ACE returns candidate files, while evidence still has to come from code and tests. That boundary matters.</p>
<p>Semantic retrieval returns “nearby” things. If you ask about a feature that does not exist, it may still find files that look related. If an agent treats “there are results” as “the feature exists,” it starts inventing a story. A conclusion is only defensible after reading an implementation, test, route, config, or call site.</p>

<h3 class="relative group">Where it fits in Harness Engineering
    <div id="where-it-fits-in-harness-engineering" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#where-it-fits-in-harness-engineering" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p><code>ace-wrapper</code> is small, and I want it to stay that way. It is closer to a small gear in the harness: it turns open-ended code discovery into a repeatable, constrained command.</p>
<p>I now prefer this project rule:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Read -> Search -> Change -> Verify</span></span></code></pre></div></div>
<p>Here, <code>Search</code> means choosing the tool by problem type:</p>
<ul>
<li>Open-ended behavior and cross-layer workflows: use <code>ace</code> first</li>
<li>Exact identifiers, errors, routes, and config keys: use <code>rg</code></li>
<li>Structural replacements: use <code>ast-grep</code></li>
<li>External strategy and industry practice: use web research</li>
<li>Old decisions and repeated lessons: use memory</li>
</ul>
<p>The useful part of this split is reduced agent randomness. The agent first uses semantic retrieval to narrow the reading surface, then uses deterministic tools to confirm facts, and only then changes code. The order is a little more verbose, but it is much cheaper than confidently editing the wrong file.</p>

<h3 class="relative group">The prompt matters most
    <div id="the-prompt-matters-most" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#the-prompt-matters-most" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>A good <code>ace</code> query describes behavior and avoids keyword piles:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">timeout 60s ace <span class="s2">"frontend sends requestId to backend and starts a processing job"</span> -w /repo
</span></span><span class="line"><span class="cl">timeout 60s ace <span class="s2">"用户拖入不支持的文件后应该显示跳过文件提示"</span> -w /repo
</span></span><span class="line"><span class="cl">timeout 60s ace <span class="s2">"how provider config is persisted and restored after app restart"</span> -w /repo</span></span></code></pre></div></div>
<p>I try to include four kinds of information:</p>
<ul>
<li>User action: click, drag, upload, stop generation</li>
<li>Runtime boundary: frontend to backend, CLI handler to core service</li>
<li>Expected effect: persist config, abort loop, show skipped-file feedback</li>
<li>Known fields: <code>sessionId</code>, <code>requestId</code>, <code>files</code>, <code>workspace</code></li>
</ul>
<p>This is much more stable than only searching <code>upload</code> or <code>provider</code>. It lets the retrieval system look for behavior and data flow, and it reminds the agent that this step is still semantic retrieval, not evidence by itself.</p>

<h3 class="relative group">Why I open-sourced it
    <div id="why-i-open-sourced-it" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#why-i-open-sourced-it" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p><code>ace-wrapper</code> has very little code. The core is just <code>FileSystemContext.create(str(workspace))</code> plus <code>context.search(args.query)</code>. I wanted to preserve the workflow constraints around those few lines:</p>
<ol>
<li>If the keyword is unknown, start with semantic retrieval</li>
<li>Ask one workflow per query</li>
<li>Treat results as candidate files</li>
<li>Read the files, then use <code>rg</code> to confirm exact evidence</li>
<li>Do not conclude without evidence</li>
</ol>
<p>Once these rules live in the tool README, skill, and agent prompt, they become much more likely to stick. Otherwise every session depends on a human reminding the agent again, which gets old fast.</p>
<p>The previous post said Harness Engineering means putting an engineering track around AI. <code>ace-wrapper</code> is one small piece of that track: it does not make the agent better at writing code; it just helps the agent read the right place first.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>From Vibe Coding to Harness Engineering: How My AI Coding Workflow Changed</title>
      <link>https://blog.ferstar.org/en/posts/ai-coding-harness-engineering-workflow/</link>
      <pubDate>Sat, 09 May 2026 14:19:00 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/en/posts/ai-coding-harness-engineering-workflow/</guid>
      <description>AI coding can generate code but long-running delivery drifts easily; use Harness Engineering to control tasks, context, verification, and recovery; turn AI output into an executable, verifiable, reviewable engineering workflow.</description><content:encoded><![CDATA[<blockquote><p>I am not a native English speaker; this article was translated by AI.</p>
</blockquote><p>This is the written version of an internal team sharing session. The slides are here:</p>
<p><a href="/slides/harness-engineering-ai-coding/" >From Vibe Coding to Harness Engineering</a></p>
<div style="position:relative;width:100%;aspect-ratio:16/9;margin:1.5rem 0 2rem;border:1px solid rgba(127,127,127,.25);overflow:hidden;">
  <iframe src="/slides/harness-engineering-ai-coding/" title="From Vibe Coding to Harness Engineering" style="position:absolute;inset:0;width:100%;height:100%;border:0;" loading="lazy" allowfullscreen></iframe>
</div>
<p>For a while I kept looking at one question: can AI really take over most of the coding work?</p>
<p>The answer is mostly settled now. When the project context, quality gates, and verification flow are in place, AI-generated code can enter the engineering workflow reliably. Human time moves from “typing the code” to “holding the line”: breaking down requirements, judging architecture, arranging context, checking boundaries, and handling failures.</p>
<p>Recent practice pushed this one step further. The question is no longer how to make the prompt prettier. It is whether the whole workflow can survive long-running tasks. I have stepped on this rake a few times, especially when I open the laptop in the morning, see that the agent ran all night, and still cannot tell which diff should be kept.</p>

<h3 class="relative group">What changed
    <div id="what-changed" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#what-changed" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>Early Vibe Coding solved the entry problem: describe the requirement clearly, put project rules into <code>AGENTS.md</code> / <code>CLAUDE.md</code>, and let tests, lint, and review catch the model output.</p>
<p>That setup is still useful, but it is closer to single-task engineering. Once a task gets longer, a few problems start showing up:</p>
<ul>
<li>Context keeps growing until the model loses the important part</li>
<li>Repeated retries can push the fix further away from the real issue</li>
<li>Without external references, strategy becomes guesswork</li>
<li>After many rounds, it is hard to tell which changes should be kept</li>
<li>User rejection, permission blocks, and empty output need explicit stop semantics</li>
</ul>
<p>So I now prefer calling this layer Harness Engineering: put an engineering track around AI so tasks are executable, results are verifiable, and failures are recoverable. The name sounds a bit grand. In practice, it just means trusting “it will figure it out” a little less and adding a few guardrails.</p>
<pre class="not-prose mermaid">
flowchart LR
  A[Task scope] --> B[Context route]
  B --> C[Agent loop]
  C --> D[Verification gate]
  D --> E[Recovery / memory]
  D -->|failed| F[Patch harness]
  F --> C
</pre>


<h3 class="relative group">The four things I manage first
    <div id="the-four-things-i-manage-first" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#the-four-things-i-manage-first" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>The first thing is task boundaries.</p>
<p>Before a medium-sized task starts, I want at least <code>done when</code>, <code>out of scope</code>, the change surface, and the verification command. This does not need to be a long document. Five lines are often enough. The point is to let the executor know when to stop, instead of drifting into “while I am here” changes.</p>
<p>The second thing is context routing.</p>
<p><code>AGENTS.md</code> should not become an encyclopedia. It works better as an index: project rules, entry points, verification commands, things that must not be touched, and where to read the next layer of docs. Long context should be opened on demand, not dumped into the session. When the context gets too full, the model behaves a bit like me with too many browser tabs open: it looks busy, but the focus is gone.</p>
<p>The third thing is the verification loop.</p>
<p>My default order is now:</p>
<ol>
<li>Read: read README, AGENTS, older notes, and key implementation files</li>
<li>Search: use <code>ace</code>, <code>rg</code>, <code>ast-grep</code>, <code>nmem</code>, and Exa to find evidence</li>
<li>Change: apply a small patch and avoid drive-by refactors</li>
<li>Verify: run narrow checks first, then expand by risk</li>
<li>Record: write repeated lessons back into rules, tests, or memory</li>
</ol>
<p>This order is boring in a good way. Reading and searching first reduce model guesswork. Narrow verification avoids one giant change where nobody knows which step broke.</p>
<p>The fourth thing is failure handling.</p>
<p>After a failure, I classify it first: stop, retry, patch the harness, or record it.</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Type</th>
          <th style="text-align: left">When to use it</th>
          <th style="text-align: left">Handling</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">Stop</td>
          <td style="text-align: left">User rejection, permission block, side effect risk, repeated spinning</td>
          <td style="text-align: left">Break the loop and return control</td>
      </tr>
      <tr>
          <td style="text-align: left">Retry</td>
          <td style="text-align: left">Network jitter, fixable parameter, read failure without side effects</td>
          <td style="text-align: left">Retry in small steps and keep logs</td>
      </tr>
      <tr>
          <td style="text-align: left">Patch</td>
          <td style="text-align: left">Same class of error appears twice</td>
          <td style="text-align: left">Add tests, rules, scripts, or logs</td>
      </tr>
      <tr>
          <td style="text-align: left">Record</td>
          <td style="text-align: left">The case will likely happen again</td>
          <td style="text-align: left">Save trigger conditions, verification commands, and evidence entry points</td>
      </tr>
  </tbody>
</table>
<p>I used to treat many failures as “try again.” Now I am more careful: only retry failures that are actually retryable, and stop when the situation says stop. Letting an agent push forward from a wrong premise usually just creates more diff for a human to clean up.</p>

<h3 class="relative group">Where external research fits
    <div id="where-external-research-fits" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#where-external-research-fits" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>In this workflow, Exa or similar web search tools also have a clearer place.</p>
<p>I usually do not search for broad trends. I search for concrete engineering questions:</p>
<ul>
<li>What timeout should be used?</li>
<li>Should this failure be retried?</li>
<li>How should the default strategy be split?</li>
<li>What boundaries do mainstream tools provide?</li>
<li>What failure samples show up in real issues?</li>
</ul>
<p>I still do not copy external answers directly. External material gives me a reference frame, and the final decision has to fit the current repo. Useful conclusions should land in specs, project rules, tests, or scripts. Otherwise I will search for the same thing again next time, which is a very small but reliable way to waste time.</p>

<h3 class="relative group">Autoresearch and Ralph Loop
    <div id="autoresearch-and-ralph-loop" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#autoresearch-and-ralph-loop" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>Autoresearch works best for long loops with a clear metric. Give the agent a goal, a guard, and a verification command first. Each round should allow only one rollback-friendly change. If it drifts, the damage is still contained.</p>
<p>I currently treat Ralph Loop as persistent single-owner execution. The same owner keeps driving the work. PRD and test spec come first, then the agent runs the long task. It cares more about preserving context, judgment, and verification clues than about adding more agents early. Fewer people in the loop can sometimes make ownership much clearer.</p>
<p>Both patterns share the same idea: define the track before letting the agent run. The track needs metrics, boundaries, verification, and rules for what to keep or discard.</p>

<h3 class="relative group">Three steps worth copying first
    <div id="three-steps-worth-copying-first" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#three-steps-worth-copying-first" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>If this needs to move into a team workflow, I would not start with platform work. Three steps are enough to copy tomorrow:</p>
<ol>
<li>Write <code>done when</code> and <code>out of scope</code> for every medium-sized task</li>
<li>Ask the agent to list files, evidence, and the change surface before allowing edits</li>
<li>After one failure, patch tests, rules, or scripts before letting the agent continue</li>
</ol>
<p>Once these three steps are in place, AI coding moves a bit from “it can produce output” toward “it can be shipped.” Autoresearch, Ralph Loop, team workers, and memory become easier to reason about after that.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Who would have thought: still using crontab &#43; HTTP Header for time sync in 6202</title>
      <link>https://blog.ferstar.org/en/posts/crontab-http-header-time-sync/</link>
      <pubDate>Thu, 22 Jan 2026 14:30:00 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/en/posts/crontab-http-header-time-sync/</guid>
      <description>An isolated network blocked UDP, NTP/chrony could not sync time, Prometheus reported a 38-second drift, and Teleport handshakes failed; HTTP Date Header plus crontab pulled the nodes back into a usable range.</description><content:encoded><![CDATA[<blockquote><p>I am not a native English speaker; this article was translated by AI.</p>
</blockquote>
<h2 class="relative group">What happened
    <div id="what-happened" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#what-happened" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>On January 21, 2026, the target cluster started alerting.</p>
<p>Prometheus first showed a clock drift warning: <code>Warning: Error fetching server time: Detected 38.116000175476074 seconds time difference between your browser and the server.</code></p>
<p>38 seconds does not sound like much. For Prometheus queries, it is enough to break charts. For something like Teleport, with security checks in the handshake path, it is even worse. target02 was already unreachable at that point: Teleport handshake failed, and SSH was gone with it.</p>

<h2 class="relative group">Checking the network first
    <div id="checking-the-network-first" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#checking-the-network-first" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>The first suspect was still the network. I tested reachability from target01 to the jump host:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># TCP scan, OK</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> port in <span class="m">22</span> <span class="m">80</span> 8888<span class="p">;</span> <span class="k">do</span> nc -zv -w <span class="m">2</span> 100.64.0.5 <span class="nv">$port</span><span class="p">;</span> <span class="k">done</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># UDP scan, looks OK at first glance, but no packets come back</span>
</span></span><span class="line"><span class="cl">nc -uvz -w <span class="m">2</span> 100.64.0.5 <span class="m">8888</span></span></span></code></pre></div></div>
<p>TCP was fine. UDP looked suspicious. <code>nc -u</code> can easily make this look “connected” when nothing useful actually reached the other side, so I had to capture packets.</p>
<p>I started tcpdump on the jump host and sent one UDP packet from target01:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># jump-host</span>
</span></span><span class="line"><span class="cl">sudo tcpdump -i any udp port <span class="m">8888</span> -n
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># target01</span>
</span></span><span class="line"><span class="cl">nc -u 100.64.0.5 <span class="m">8888</span> <span class="o"><<<</span> <span class="s2">"test"</span></span></span></code></pre></div></div>
<p>The jump host saw nothing. That was enough to confirm UDP was blocked somewhere in the isolated network.</p>
<p>I also checked port 8888 while I was there:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -v http://100.64.0.5:8888
</span></span><span class="line"><span class="cl"><span class="c1"># < Proxy-Agent: gost/2.12.0</span></span></span></code></pre></div></div>
<p>That port was a gost proxy, mostly for TCP tunnels. UDP was not wired through it, so it was not going to keep NTP alive.</p>

<h2 class="relative group">It came down to clock drift
    <div id="it-came-down-to-clock-drift" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#it-came-down-to-clock-drift" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>After all that, the actual issue was still clock drift:</p>
<ul>
<li>target01 was about 38 seconds behind, enough to make Prometheus queries and charts go weird.</li>
<li>target02 had drifted further, and Teleport rejected the handshake outright.</li>
</ul>
<p>NTP could not get out of the isolated environment. chrony was running, but only in the sense that it was repeatedly failing. The normal path was dead, so I needed something reachable inside the environment that could still act as a time reference.</p>
<p>The jump host’s port 80 was reachable, and HTTP responses have a <code>Date</code> header.</p>

<h2 class="relative group">Using HTTP Date to recover first
    <div id="using-http-date-to-recover-first" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#using-http-date-to-recover-first" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>I stopped chrony first so it would not keep fighting the manual correction:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">systemctl stop chronyd
</span></span><span class="line"><span class="cl">systemctl disable chronyd</span></span></code></pre></div></div>
<p>Then I pulled <code>Date</code> from the jump host’s HTTP header and fed it straight into <code>date -s</code>:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">HTTP_DATE</span><span class="o">=</span><span class="k">$(</span>curl -sI http://100.64.0.5 <span class="p">|</span> grep -i <span class="s2">"^Date:"</span> <span class="p">|</span> cut -d<span class="s2">" "</span> -f2-<span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span> -n <span class="s2">"</span><span class="nv">$HTTP_DATE</span><span class="s2">"</span> <span class="o">]</span> <span class="o">&&</span> date -s <span class="s2">"</span><span class="nv">$HTTP_DATE</span><span class="s2">"</span></span></span></code></pre></div></div>
<p>It is crude. Precision is only seconds. But the goal at that moment was not elegance; it was getting Teleport back within handshake tolerance. After running it, the Prometheus clock drift warning cleared, and target02 became reachable again.</p>
<p>Yes, it is 2026 and I am still syncing clocks with <code>curl</code> + <code>date -s</code>. The last time I remember doing this was probably on OpenWrt.</p>

<h2 class="relative group">Making it stick with crontab
    <div id="making-it-stick-with-crontab" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#making-it-stick-with-crontab" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>After the quick recovery, I still needed to stop the nodes from drifting again. I put the same sync into crontab and ran it hourly:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="o">(</span>crontab -l 2>/dev/null<span class="p">;</span> <span class="nb">echo</span> <span class="s1">'0 * * * * HTTP_DATE=$(curl -sI http://100.64.0.5 | grep -i "^Date:" | cut -d" " -f2-) && [ -n "$HTTP_DATE" ] && date -s "$HTTP_DATE"'</span><span class="o">)</span> <span class="p">|</span> crontab -</span></span></code></pre></div></div>
<p>It is not pretty, but it works well enough in this environment. It only needs HTTP. As long as the jump host is reachable, the nodes stay within an acceptable time window.</p>

<h2 class="relative group">Notes for next time
    <div id="notes-for-next-time" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#notes-for-next-time" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>A few things worth writing down:</p>
<ol>
<li>Do not assume NTP works in isolated networks. Once UDP 123 is blocked, chrony can look alive while syncing nothing.</li>
<li>Do not trust <code>nc -u</code> too much. UDP has no handshake, so a success message does not mean the remote side received anything.</li>
<li>Prometheus and Teleport are both sensitive to clock drift. A few dozen seconds can easily create symptoms that look like network failures.</li>
</ol>
<p>If I deploy into a similar environment again, I will probably turn this HTTP Date sync into a small systemd timer. crontab is fine for firefighting; long term, it deserves something slightly less feral.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>KVM GPU Passthrough and Single-Node K8s Monitoring</title>
      <link>https://blog.ferstar.org/en/posts/kvm-gpu-passthrough-k8s-monitoring/</link>
      <pubDate>Thu, 22 Jan 2026 10:00:00 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/en/posts/kvm-gpu-passthrough-k8s-monitoring/</guid>
      <description>GPU passthrough caused black screen and missing GPUs + tuned OVMF/PCI hole64/NUMA and wired monitoring + 8 GPUs with NVSwitch and metrics stable</description><content:encoded><![CDATA[<blockquote><p>I am not a native English speaker; this article was translated by AI.</p>
</blockquote><p>This round was about a KVM VM on a GPU host: pass through 8 H800 GPUs and 4 NVSwitches, then run single-node K8s, GPU Operator, and Prometheus inside the VM.</p>
<p>The process was not fancy, but the traps were very familiar: black screen, not enough PCI resources, DHCP lease mismatch, and a ServiceMonitor label that did not line up. I am writing the path down in the order I debugged it, mostly so I do not have to dig through logs from scratch next time.</p>

<h2 class="relative group">Environment and goal
    <div id="environment-and-goal" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#environment-and-goal" aria-label="Anchor">#</a>
    </span>
    
</h2>
<ul>
<li>Host: GPU node</li>
<li>VM: <code>ubuntu_gpu</code> (KVM/libvirt, UEFI/OVMF)</li>
<li>Goal: 8x H800 + 4x NVSwitch passthrough, GPU scheduling in single-node K8s, and DCGM metrics collected by Prometheus</li>
</ul>

<h2 class="relative group">Rough troubleshooting path
    <div id="rough-troubleshooting-path" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#rough-troubleshooting-path" aria-label="Anchor">#</a>
    </span>
    
</h2>
<pre class="not-prose mermaid">
flowchart TD
  A[VM boot + GPU passthrough] --> B[Black screen / some GPUs fail to init]
  B --> C[Switch to non-Secure Boot OVMF]
  C --> D[Increase PCI hole64]
  D --> E[NUMA + vCPU pinning]
  E --> F[8 GPUs + NVSwitch OK]
  F --> G[K8s + GPU Operator + Prometheus]
  G --> H[DCGM metrics verified]
</pre>


<h2 class="relative group">1. Black screen and PCI resource shortage
    <div id="1-black-screen-and-pci-resource-shortage" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#1-black-screen-and-pci-resource-shortage" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>The first symptom looked scary: after GPU passthrough, VNC showed nothing, and the driver logs contained <code>PCI I/O region invalid</code>. For this kind of issue, I would not blame the driver too early. The 64-bit PCI hole on Q35 can be too small, especially with multiple GPUs plus NVSwitch.</p>
<p>I changed two things:</p>
<ul>
<li>switched OVMF to the non-secure version, to remove Secure Boot from the equation</li>
<li>increased <code>pci-hole64-size</code> to 2048G</li>
</ul>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="cl"><span class="c"><!-- OVMF non-secure --></span>
</span></span><span class="line"><span class="cl"><span class="nt"><loader</span> <span class="na">readonly=</span><span class="s">'yes'</span> <span class="na">type=</span><span class="s">'pflash'</span><span class="nt">></span>/usr/share/edk2/ovmf/OVMF_CODE.cc.fd<span class="nt"></loader></span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c"><!-- Q35 PCIe 64-bit hole --></span>
</span></span><span class="line"><span class="cl"><span class="nt"><qemu:commandline></span>
</span></span><span class="line"><span class="cl">  <span class="nt"><qemu:arg</span> <span class="na">value=</span><span class="s">'-global'</span><span class="nt">/></span>
</span></span><span class="line"><span class="cl">  <span class="nt"><qemu:arg</span> <span class="na">value=</span><span class="s">'q35-pcihost.pci-hole64-size=2048G'</span><span class="nt">/></span>
</span></span><span class="line"><span class="cl"><span class="nt"></qemu:commandline></span></span></span></code></pre></div></div>
<p>After that, GPU initialization and topology detection became normal. This option is easy to forget because most VMs never need it, but for multi-GPU passthrough it is one of the first places worth checking.</p>

<h2 class="relative group">2. IP unreachable: just DHCP binding
    <div id="2-ip-unreachable-just-dhcp-binding" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#2-ip-unreachable-just-dhcp-binding" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>There was also a very boring, very time-wasting network issue: the VM IP was unreachable. In the end, the NIC MAC had changed, so the DHCP binding no longer matched.</p>
<p>Restoring the old MAC brought the address back to <code>192.168.122.146</code>.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># old MAC</span>
</span></span><span class="line"><span class="cl">52:54:00:a9:a2:11</span></span></code></pre></div></div>
<p>Logs are not always generous for this kind of problem. I had to compare the libvirt XML, DHCP lease, and routes by hand.</p>

<h2 class="relative group">3. Memory, NUMA, and vCPU pinning
    <div id="3-memory-numa-and-vcpu-pinning" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#3-memory-numa-and-vcpu-pinning" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>The VM memory was set to 256GB:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="cl"><span class="nt"><memory</span> <span class="na">unit=</span><span class="s">'KiB'</span><span class="nt">></span>268435456<span class="nt"></memory></span>
</span></span><span class="line"><span class="cl"><span class="nt"><currentMemory</span> <span class="na">unit=</span><span class="s">'KiB'</span><span class="nt">></span>268435456<span class="nt"></currentMemory></span></span></span></code></pre></div></div>
<p>vCPUs were split into two groups and pinned to matching NUMA nodes. GPUs were placed according to NUMA locality as well. This does not always decide whether the VM can boot, but it does matter once real workloads start running, so I prefer to get it right early.</p>

<h2 class="relative group">4. Verifying GPU, K8s, and monitoring
    <div id="4-verifying-gpu-k8s-and-monitoring" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#4-verifying-gpu-k8s-and-monitoring" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>On the GPU side, check the devices, topology, and NVLink state:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">nvidia-smi -L
</span></span><span class="line"><span class="cl">nvidia-smi topo -m
</span></span><span class="line"><span class="cl">nvidia-smi nvlink -s</span></span></code></pre></div></div>
<p>On the K8s side, check the node and pods:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl get nodes -o wide
</span></span><span class="line"><span class="cl">kubectl get pods -A</span></span></code></pre></div></div>
<p>For Prometheus, query one DCGM metric directly:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl http://127.0.0.1:9090/api/v1/query?query<span class="o">=</span>DCGM_FI_DEV_SM_CLOCK</span></span></code></pre></div></div>
<p>If data comes back, the DCGM Exporter to Prometheus path is basically working.</p>

<h2 class="relative group">5. Key config snippets
    <div id="5-key-config-snippets" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#5-key-config-snippets" aria-label="Anchor">#</a>
    </span>
    
</h2>

<h3 class="relative group">containerd proxy
    <div id="containerd-proxy" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#containerd-proxy" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>This machine cannot reach the public internet directly, so containerd needs a proxy for image pulls:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># /etc/systemd/system/containerd.service.d/http-proxy.conf</span>
</span></span><span class="line"><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">"HTTP_PROXY=http://100.64.0.5:8888"</span>
</span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">"HTTPS_PROXY=http://100.64.0.5:8888"</span>
</span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">"NO_PROXY=127.0.0.1,localhost,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,100.64.0.0/10"</span></span></span></code></pre></div></div>

<h3 class="relative group">GPU Operator and Prometheus
    <div id="gpu-operator-and-prometheus" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#gpu-operator-and-prometheus" aria-label="Anchor">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">helm upgrade --install gpu-operator nvidia/gpu-operator <span class="se">\
</span></span></span><span class="line"><span class="cl">  -n gpu-operator --create-namespace <span class="se">\
</span></span></span><span class="line"><span class="cl">  --set driver.enabled<span class="o">=</span><span class="nb">false</span> --set dcgmExporter.enabled<span class="o">=</span><span class="nb">true</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">helm upgrade --install kube-prometheus-stack prometheus-community/kube-prometheus-stack <span class="se">\
</span></span></span><span class="line"><span class="cl">  -n monitoring --create-namespace</span></span></code></pre></div></div>

<h3 class="relative group">DCGM metrics into Prometheus
    <div id="dcgm-metrics-into-prometheus" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#dcgm-metrics-into-prometheus" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>The easy-to-miss bit is the <code>ServiceMonitor</code> label. Prometheus usually only selects objects with the matching release label, so add it explicitly:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># ServiceMonitor must match Prometheus release label</span>
</span></span><span class="line"><span class="cl">kubectl -n gpu-operator label servicemonitor nvidia-dcgm-exporter <span class="nv">release</span><span class="o">=</span>kube-prometheus-stack --overwrite</span></span></code></pre></div></div>

<h2 class="relative group">Notes for next time
    <div id="notes-for-next-time" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#notes-for-next-time" aria-label="Anchor">#</a>
    </span>
    
</h2>
<ul>
<li>For multi-GPU passthrough failures, check OVMF and <code>pci-hole64-size</code> early</li>
<li>Turn off Secure Boot while validating, otherwise there are too many variables</li>
<li>If DHCP suddenly behaves strangely, verify whether libvirt changed the MAC</li>
<li>Having DCGM Exporter running is not enough; the Prometheus <code>release</code> label must match</li>
</ul>
<p>Most of the issues here were the annoying kind where one small config mismatch makes the whole thing look broken. Once fixed, the 8 GPUs, NVSwitches, single-node K8s, and monitoring path all stayed stable.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>VPS Migration and Performance Squeezing Guide (Debian 13 &#43; XanMod &#43; Port 443 Multiplexing)</title>
      <link>https://blog.ferstar.org/en/posts/vps-migration-and-optimization-guide/</link>
      <pubDate>Tue, 20 Jan 2026 10:00:00 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/en/posts/vps-migration-and-optimization-guide/</guid>
      <description>A low-memory VPS struggles after migration; use XanMod, kernel/memory tuning, and port 443 multiplexing to keep services stable on a smaller plan.</description><content:encoded><![CDATA[<blockquote><p>I am not a native English speaker; this article was translated by AI.</p>
</blockquote><p>This is a plain migration note: moving from an old DigitalOcean host (Ubuntu 20.04) to a new one (Debian 13 + XanMod).</p>
<p>The reason was also plain. My old $6/mo instance (1GB RAM / 20GB DISK / 1TB BW) was idle most of the time, and the blog had already moved to Cloudflare Pages, the reliable “cyber bodhisattva” in the corner. So I downgraded the Droplet to the $4/mo plan (512MB RAM / 10GB DISK / 500GB BW). Saving two dollars a month is not life-changing, but for this kind of tinkering, saving anything still feels like a win.</p>
<p>After the downgrade, this little VPS mainly runs a backup proxy and a half-asleep WeChat public account backend. 512MB RAM is not generous, so I also cleaned up the kernel, memory settings, Nginx port multiplexing, and certificate renewal while migrating.</p>

<h2 class="relative group">1. Basic environment
    <div id="1-basic-environment" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#1-basic-environment" aria-label="Anchor">#</a>
    </span>
    
</h2>
<ul>
<li><strong>Source Host</strong>: DigitalOcean Ubuntu 20.04 (IP hidden)</li>
<li><strong>Target Host</strong>: DigitalOcean Debian 13 Trixie (IP hidden)</li>
<li><strong>Reserved IP</strong>: Attached to the new host for DNS resolution.</li>
<li><strong>Hostname / PTR</strong>: <code>ferstar.org</code> (automatically triggered by renaming the DigitalOcean Droplet).</li>
</ul>

<h2 class="relative group">2. Kernel and memory tuning (Kernel 6.18+)
    <div id="2-kernel-and-memory-tuning-kernel-618" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#2-kernel-and-memory-tuning-kernel-618" aria-label="Anchor">#</a>
    </span>
    
</h2>

<h3 class="relative group">2.1 Upgrade to XanMod Edge
    <div id="21-upgrade-to-xanmod-edge" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#21-upgrade-to-xanmod-edge" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>For BBRv3 and newer scheduler features, I went straight to XanMod Edge:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">wget -qO - https://dl.xanmod.org/archive.key <span class="p">|</span> gpg --dearmor <span class="p">|</span> tee /usr/share/keyrings/xanmod-archive-keyring.gpg > /dev/null
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s1">'deb [signed-by=/usr/share/keyrings/xanmod-archive-keyring.gpg] http://deb.xanmod.org releases main'</span> <span class="p">|</span> tee /etc/apt/sources.list.d/xanmod-kernel.list
</span></span><span class="line"><span class="cl">apt update <span class="o">&&</span> apt install linux-xanmod-edge-x64v3 -y</span></span></code></pre></div></div>
<p><code>linux-xanmod-edge-x64v3</code> requires x86-64-v3 CPU support. If the VPS does not support it, use <code>linux-xanmod-edge-x64v2</code> or <code>linux-xanmod-edge-x64</code> instead. No need to be heroic here.</p>

<h3 class="relative group">2.2 The small-memory set: zswap + MGLRU + KSM
    <div id="22-the-small-memory-set-zswap--mglru--ksm" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#22-the-small-memory-set-zswap--mglru--ksm" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>With only 512MB RAM, there is not much headroom, so the kernel needs a little help. I enabled MGLRU, KSM, and the zswap shrinker. Some of these parameters are awkward to persist through <code>sysctl</code>, so I put them in <code>crontab</code> with <code>@reboot</code>. Crude, but easy to inspect later.</p>
<p><strong>Persistence commands (<code>crontab -e</code>):</strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Enable all MGLRU optimization tiers to significantly reduce OOM risk under low memory</span>
</span></span><span class="line"><span class="cl">@reboot <span class="nb">echo</span> <span class="m">7</span> > /sys/kernel/mm/lru_gen/enabled
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Enable KSM memory page merging to reduce duplicate memory usage between Docker containers</span>
</span></span><span class="line"><span class="cl">@reboot <span class="nb">echo</span> <span class="m">1</span> > /sys/kernel/mm/ksm/run
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Enable zswap shrinker to allow the kernel to balance data more aggressively between the compressed area and physical Swap</span>
</span></span><span class="line"><span class="cl">@reboot <span class="nb">echo</span> Y > /sys/module/zswap/parameters/shrinker_enabled</span></span></code></pre></div></div>
<p>Other settings:</p>
<ul>
<li><strong>zswap</strong>: Enabled memory compression cache, currently using the <strong>lzo</strong> algorithm.</li>
<li><strong>Swap</strong>: 1GB physical file as a fallback.</li>
</ul>

<h2 class="relative group">3. Port 443 multiplexing (SNI proxy)
    <div id="3-port-443-multiplexing-sni-proxy" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#3-port-443-multiplexing-sni-proxy" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>Fewer exposed ports usually means less daily noise. Here I use the <code>stream</code> module from Nginx 1.29.4 (Mainline) to route traffic by SNI. Unknown traffic falls back to SSH.</p>

<h3 class="relative group">3.1 Nginx global config (<code>/etc/nginx/nginx.conf</code>)
    <div id="31-nginx-global-config-etcnginxnginxconf" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#31-nginx-global-config-etcnginxnginxconf" aria-label="Anchor">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl"><span class="k">stream</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kn">map</span> <span class="nv">$ssl_preread_server_name</span> <span class="nv">$stream_map</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kn">api.ferstar.org</span> <span class="s">api</span><span class="p">;</span>   <span class="c1"># WeChat Bot -> Forward to local 8444
</span></span></span><span class="line"><span class="cl">        <span class="kn">fm.ferstar.org</span>  <span class="s">fm</span><span class="p">;</span>    <span class="c1"># File Server -> Forward to local 8445
</span></span></span><span class="line"><span class="cl">        <span class="kn">default</span>         <span class="s">ssh</span><span class="p">;</span>   <span class="c1"># Non-SSL or unknown domain defaults to local 22 (SSH)
</span></span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="kn">upstream</span> <span class="s">ssh</span>  <span class="p">{</span> <span class="kn">server</span> <span class="n">127.0.0.1</span><span class="p">:</span><span class="mi">22</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="kn">upstream</span> <span class="s">api</span>  <span class="p">{</span> <span class="kn">server</span> <span class="n">127.0.0.1</span><span class="p">:</span><span class="mi">8444</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="kn">upstream</span> <span class="s">fm</span>   <span class="p">{</span> <span class="kn">server</span> <span class="n">127.0.0.1</span><span class="p">:</span><span class="mi">8445</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="kn">server</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kn">listen</span> <span class="mi">443</span> <span class="s">reuseport</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kn">proxy_pass</span> <span class="nv">$stream_map</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kn">ssl_preread</span> <span class="no">on</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>The benefits are straightforward:</p>
<ul>
<li>the firewall only needs 80/443 exposed, so the rules stay clean</li>
<li>SSH is not directly exposed on port 22, which cuts down scan noise</li>
<li>HTTPS, SSH, and proxy services can share one entry point, which is handy on awkward networks</li>
</ul>

<h3 class="relative group">3.2 Async IO tuning
    <div id="32-async-io-tuning" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#32-async-io-tuning" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>File serving still runs on this box, so I enabled thread-pool async IO in the <code>http</code> block to keep large file reads and writes from blocking workers:</p>
<ul>
<li><code>aio threads;</code></li>
<li><code>thread_pool default threads=32 max_queue=65536;</code></li>
<li><code>directio 4m;</code></li>
</ul>

<h2 class="relative group">4. Firewall config (UFW)
    <div id="4-firewall-config-ufw" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#4-firewall-config-ufw" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>Inbound 22 is closed and handled through the 443 stream fallback. The rules are intentionally boring:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ufw reset
</span></span><span class="line"><span class="cl">ufw default deny incoming
</span></span><span class="line"><span class="cl">ufw default allow outgoing
</span></span><span class="line"><span class="cl">ufw allow 80/tcp
</span></span><span class="line"><span class="cl">ufw allow 443/tcp
</span></span><span class="line"><span class="cl">ufw allow 443/udp
</span></span><span class="line"><span class="cl">ufw allow 18443:18445/udp  <span class="c1"># For proxy services</span>
</span></span><span class="line"><span class="cl">ufw enable</span></span></code></pre></div></div>

<h2 class="relative group">5. Let’s Encrypt wildcard certificate
    <div id="5-lets-encrypt-wildcard-certificate" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#5-lets-encrypt-wildcard-certificate" aria-label="Anchor">#</a>
    </span>
    
</h2>

<h3 class="relative group">5.1 DNS validation (Cloudflare)
    <div id="51-dns-validation-cloudflare" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#51-dns-validation-cloudflare" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>The wildcard certificate (<code>*.ferstar.org</code>) is renewed through the <code>dns-cloudflare</code> plugin.</p>
<ul>
<li><strong>Credential File</strong>: <code>/root/certbot-creds.ini</code> (contains CF API Token).</li>
<li><strong>Plugin Installation</strong>: <code>apt install python3-certbot-dns-cloudflare -y</code>.</li>
</ul>

<h3 class="relative group">5.2 Post-renewal hook
    <div id="52-post-renewal-hook" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#52-post-renewal-hook" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>The renewal config lives at <code>/etc/letsencrypt/renewal/ferstar.org.conf</code>. After a certificate update, reload Nginx and restart the containers that use the certificate:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">post_hook</span> <span class="o">=</span> systemctl reload nginx <span class="o">&&</span> docker restart hysteria hysteria2 tuic-server</span></span></code></pre></div></div>

<h2 class="relative group">6. Application config templates
    <div id="6-application-config-templates" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#6-application-config-templates" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>These templates are not trying to be a clever abstraction. They are here so the next migration has something concrete to compare against.</p>

<h3 class="relative group">6.1 Hysteria v1
    <div id="61-hysteria-v1" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#61-hysteria-v1" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p><strong><code>docker-compose.yml</code></strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">hysteria</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">tobyxdd/hysteria:v1.3.5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">hysteria</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">logging</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">driver</span><span class="p">:</span><span class="w"> </span><span class="s2">"json-file"</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">options</span><span class="p">:</span><span class="w"> </span>{<span class="nt">max-size</span><span class="p">:</span><span class="w"> </span><span class="s2">"10m"</span><span class="nt">, max-file</span><span class="p">:</span><span class="w"> </span><span class="s2">"3"</span>}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"-config"</span><span class="p">,</span><span class="w"> </span><span class="s2">"/etc/config.json"</span><span class="p">,</span><span class="w"> </span><span class="s2">"server"</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./config.json:/etc/config.json</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/etc/letsencrypt:/etc/letsencrypt:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"18443:443/udp"</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">sysctls</span><span class="p">:</span><span class="w"> </span>{<span class="nt">net.ipv4.tcp_congestion_control</span><span class="p">:</span><span class="w"> </span><span class="l">bbr}</span></span></span></code></pre></div></div>
<p><strong><code>config.json</code></strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">"listen"</span><span class="p">:</span> <span class="s2">":443"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">"cert"</span><span class="p">:</span> <span class="s2">"/etc/letsencrypt/live/DOMAIN/fullchain.pem"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">"key"</span><span class="p">:</span> <span class="s2">"/etc/letsencrypt/live/DOMAIN/privkey.pem"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">"auth"</span><span class="p">:</span> <span class="p">{</span> <span class="nt">"mode"</span><span class="p">:</span> <span class="s2">"passwords"</span><span class="p">,</span> <span class="nt">"config"</span><span class="p">:</span> <span class="p">[</span><span class="s2">"PASSWORD"</span><span class="p">]</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">"up_mbps"</span><span class="p">:</span> <span class="mi">1000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">"down_mbps"</span><span class="p">:</span> <span class="mi">1000</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>

<h3 class="relative group">6.2 Hysteria v2
    <div id="62-hysteria-v2" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#62-hysteria-v2" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p><strong><code>docker-compose.yml</code></strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">hysteria2</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">tobyxdd/hysteria:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">hysteria2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"server"</span><span class="p">,</span><span class="w"> </span><span class="s2">"-c"</span><span class="p">,</span><span class="w"> </span><span class="s2">"/etc/hysteria/config.yaml"</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">logging</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">driver</span><span class="p">:</span><span class="w"> </span><span class="s2">"json-file"</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">options</span><span class="p">:</span><span class="w"> </span>{<span class="nt">max-size</span><span class="p">:</span><span class="w"> </span><span class="s2">"10m"</span><span class="nt">, max-file</span><span class="p">:</span><span class="w"> </span><span class="s2">"3"</span>}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./config.yaml:/etc/hysteria/config.yaml:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/etc/letsencrypt:/etc/letsencrypt:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"18445:443/udp"</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">sysctls</span><span class="p">:</span><span class="w"> </span>{<span class="nt">net.ipv4.tcp_congestion_control</span><span class="p">:</span><span class="w"> </span><span class="l">bbr}</span></span></span></code></pre></div></div>
<p><strong><code>config.yaml</code></strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">listen</span><span class="p">:</span><span class="w"> </span><span class="p">:</span><span class="m">443</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">tls</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">cert</span><span class="p">:</span><span class="w"> </span><span class="l">/etc/letsencrypt/live/DOMAIN/fullchain.pem</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">/etc/letsencrypt/live/DOMAIN/privkey.pem</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">auth</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">password</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">password</span><span class="p">:</span><span class="w"> </span><span class="s2">"PASSWORD"</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">bandwidth</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">up</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w"> </span><span class="l">gbps</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">down</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w"> </span><span class="l">gbps</span></span></span></code></pre></div></div>

<h3 class="relative group">6.3 TUIC v5
    <div id="63-tuic-v5" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#63-tuic-v5" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p><strong><code>docker-compose.yml</code></strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">tuic</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">ghcr.io/itsusinn/tuic-server:1.4.5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">tuic-server</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">logging</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">driver</span><span class="p">:</span><span class="w"> </span><span class="s2">"json-file"</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">options</span><span class="p">:</span><span class="w"> </span>{<span class="nt">max-size</span><span class="p">:</span><span class="w"> </span><span class="s2">"10m"</span><span class="nt">, max-file</span><span class="p">:</span><span class="w"> </span><span class="s2">"3"</span>}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">always</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"18444:443/udp"</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./config.json:/etc/tuic/config.json:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/etc/letsencrypt:/etc/letsencrypt:ro</span></span></span></code></pre></div></div>
<p><strong><code>config.json</code></strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">"server"</span><span class="p">:</span> <span class="s2">"[::]:443"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">"users"</span><span class="p">:</span> <span class="p">{</span> <span class="nt">"UUID"</span><span class="p">:</span> <span class="s2">"PASSWORD"</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">"certificate"</span><span class="p">:</span> <span class="s2">"/etc/letsencrypt/live/DOMAIN/fullchain.pem"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">"private_key"</span><span class="p">:</span> <span class="s2">"/etc/letsencrypt/live/DOMAIN/privkey.pem"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">"alpn"</span><span class="p">:</span> <span class="p">[</span><span class="s2">"h3"</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="nt">"udp_relay_ipv6"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">"zero_rtt_handshake"</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">"dual_stack"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">"log_level"</span><span class="p">:</span> <span class="s2">"warn"</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>

<h3 class="relative group">6.4 Filebrowser
    <div id="64-filebrowser" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#64-filebrowser" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p><strong><code>docker-compose.yml</code></strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">filebrowser</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">filebrowser/filebrowser:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">filebrowser</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">logging</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">driver</span><span class="p">:</span><span class="w"> </span><span class="s2">"json-file"</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">options</span><span class="p">:</span><span class="w"> </span>{<span class="nt">max-size</span><span class="p">:</span><span class="w"> </span><span class="s2">"10m"</span><span class="nt">, max-file</span><span class="p">:</span><span class="w"> </span><span class="s2">"3"</span>}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">user</span><span class="p">:</span><span class="w"> </span><span class="m">0</span><span class="p">:</span><span class="m">0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"127.0.0.1:1122:80"</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/root/fm/filebrowser/srv:/srv</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/root/fm/filebrowser/database.db:/database.db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"--address"</span><span class="p">,</span><span class="w"> </span><span class="s2">"0.0.0.0"</span><span class="p">,</span><span class="w"> </span><span class="s2">"--port"</span><span class="p">,</span><span class="w"> </span><span class="s2">"80"</span><span class="p">,</span><span class="w"> </span><span class="s2">"--database"</span><span class="p">,</span><span class="w"> </span><span class="s2">"/database.db"</span><span class="p">,</span><span class="w"> </span><span class="s2">"--root"</span><span class="p">,</span><span class="w"> </span><span class="s2">"/srv"</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span></span></span></code></pre></div></div>

<h2 class="relative group">Closing notes
    <div id="closing-notes" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#closing-notes" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>There is nothing deep about this migration. The main idea was to rethink what the old VPS still needed to do: let Cloudflare Pages handle the blog, keep only the small services on the VPS, expose fewer ports, and squeeze memory where it is safe to do so. 512MB is still tight, but for these lightweight jobs it is enough.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>AI-First Blog Cleanup Log: SEO, Redirects, and a Bilingual Setup</title>
      <link>https://blog.ferstar.org/en/posts/blog-seo-multilingual-ai-optimization/</link>
      <pubDate>Tue, 06 Jan 2026 03:00:00 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/en/posts/blog-seo-multilingual-ai-optimization/</guid>
      <description>A cleanup log: fixing 300+ legacy redirects, tweaking titles/descriptions with GSC data, and shipping a high-fidelity zh/en structure.</description><content:encoded><![CDATA[<blockquote><p>I am not a native English speaker; this article was translated by AI.</p>
</blockquote><p>This post is basically a renovation note for the blog. A few hours earlier, the site was still Chinese-only: <code>static/_redirects</code> had a pile of old rules from several migrations, and many posts with decent traffic still had no description.</p>
<p>I first used Google Search Console (GSC) to find where the pain was, then cleaned up redirects, and finally added the bilingual structure and writing rules. In this round, AI mostly handled repetitive work and cross-checking. The calls on what to keep and what to delete were still mine. Fair enough, since the model is not going to take the blame if I break the site.</p>
<hr>

<h3 class="relative group">GSC poured some cold water on me first
    <div id="gsc-poured-some-cold-water-on-me-first" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#gsc-poured-some-cold-water-on-me-first" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>The data was simple, but not pleasant:</p>
<ul>
<li><strong>US market</strong>: 13,000+ impressions, only <strong>1.46%</strong> CTR</li>
<li><strong>Chinese market</strong>: CTR stayed above 6%</li>
</ul>
<p>I used to think technical posts would find readers as long as the content was solid. In reality, when a fully Chinese page shows up in search results, many overseas readers will just skip it. Going bilingual is not about looking international; it is about letting people who already found the post actually read it.</p>
<hr>

<h3 class="relative group">Redirects look small, but they can quietly kill indexed URLs
    <div id="redirects-look-small-but-they-can-quietly-kill-indexed-urls" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#redirects-look-small-but-they-can-quietly-kill-indexed-urls" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>After moving the blog from Farbox to Bitcron and then to Hugo, <code>static/_redirects</code> was not pretty. My first lazy idea was to replace spaces with <code>-</code>, collapse repeated dashes, and call it done.</p>
<p>Hugo corrected me pretty quickly. Filenames with <code>&</code> can generate slugs like <code>google-search-tips--tricks</code>, with two dashes in the middle. If a script helpfully collapses <code>--</code> into <code>-</code>, already-indexed Google URLs go straight to 404.</p>
<p>I ended up using a dumber but safer approach:</p>
<ul>
<li>Keep both the single-dash normalized path and the original multi-dash variants (<code>--</code>, <code>---</code>, etc.), all pointing exactly to the new slug</li>
<li>Replace fuzzy wildcards (like <code>/post/*</code>) with explicit mappings, so debugging later does not become guesswork</li>
</ul>
<p>This pass cleaned up 300+ legacy redirects into something I am comfortable keeping long-term. The file is still not short, but at least every rule has a reason to exist.</p>
<hr>

<h3 class="relative group">Bilingual does not mean appending a short English summary
    <div id="bilingual-does-not-mean-appending-a-short-english-summary" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#bilingual-does-not-mean-appending-a-short-english-summary" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>I did not want a half-finished setup with the Chinese post plus a tiny English abstract, so I set a few rules for translations:</p>
<ol>
<li><strong>Code parity</strong>: shell commands, Java hooks, and kernel configs must match character-for-character</li>
<li><strong>Diagram parity</strong>: Mermaid diagrams must render on the English pages too</li>
<li><strong>Do not sand off the details</strong>: traps, trade-offs, and performance numbers should stay, not turn into lukewarm prose</li>
</ol>
<p>The workflow was: AI drafted, then AI cross-checked the Chinese and English versions; I edited paragraph by paragraph and cut or rewrote anything that sounded too templated. The first batch covered 10+ high-traffic posts. The older debt can be paid down slowly.</p>
<hr>

<h3 class="relative group">Put the rules in AGENTS.md, because memory is unreliable
    <div id="put-the-rules-in-agentsmd-because-memory-is-unreliable" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#put-the-rules-in-agentsmd-because-memory-is-unreliable" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>After the cleanup, my biggest worry was not that the code would break. It was that I would forget why I made these choices a few months later. So I added <code>AGENTS.md</code> to the repo and wrote down the ground rules:</p>
<ul>
<li><strong>Bilingual alignment</strong>: after a Chinese update, the English version must be updated with high fidelity</li>
<li><strong>SEO description</strong>: descriptions follow <code>[Pain Point] + [Solution] + [Result]</code></li>
<li><strong>Directory convention</strong>: keep <code>content/</code> and <code>content.en/</code> separate, and handle links as a multilingual site</li>
<li><strong>No redirect rollback</strong>: <code>static/_redirects</code> should use explicit rules only, no lazy <code>/post/*</code></li>
</ul>
<p>It is not complicated, but it helps. Next time, whether I come back myself or ask an agent to continue, there is a runnable set of rules instead of “probably close enough”.</p>
<hr>

<h3 class="relative group">Closing
    <div id="closing" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#closing" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>I write blog posts to preserve how problems were solved. SEO and bilingual pages just smooth the path to the door, making those notes easier to find and easier to finish reading.</p>
<p>For me, AI is more like a power tool: checklists, bulk edits, cross-checks — it is fast at those. But the final trade-offs, tone, and responsibility are still mine. That is probably for the best. If something breaks later, I know exactly who to blame.</p>
<hr>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Practical Guide: GPU Cluster Access Control and Auditing with Teleport &#43; Tailscale</title>
      <link>https://blog.ferstar.org/en/posts/teleport-tailscale-gpu-access/</link>
      <pubDate>Sat, 03 Jan 2026 03:58:20 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/en/posts/teleport-tailscale-gpu-access/</guid>
      <description>Vendors need access while audits must stay silent; use Teleport + Tailscale with multiplexing and bypass channels; achieve secure, low-friction GPU cluster access.</description><content:encoded><![CDATA[<blockquote><p>I am not a native English speaker; this article was translated by AI.</p>
</blockquote><p>There are several DGX H800 nodes in a private network. They are not public-facing, but vendors occasionally need remote access for troubleshooting. The requirements sound simple, until network paths and auditing get involved:</p>
<ol>
<li>Vendors need to connect and do their work.</li>
<li>Sessions must be fully recorded, but the audit entry points should not be exposed to them.</li>
<li>Our own team still needs a direct path, without going through the bastion every single time.</li>
</ol>
<p>The final setup is Teleport for bastion access and auditing, plus Tailscale for internal connectivity and an ops bypass path. These are the bits that tripped me up, recorded here so I do not step on the same rake again.</p>
<hr>

<h3 class="relative group">The rough shape
    <div id="the-rough-shape" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#the-rough-shape" aria-label="Anchor">#</a>
    </span>
    
</h3>
<pre class="not-prose mermaid">
flowchart LR
  subgraph External[Public Users]
    Vendor[Vendor<br/>Web/SSH]
    AppUser[App User<br/>Web]
  end
  subgraph Ops[Internal Ops]
    Admin[Admin<br/>SSH Direct]
  end
  subgraph Edge[Public Edge]
    Nginx[Nginx 443]
    Teleport[Teleport Proxy 3080]
    FW[DOCKER-USER Firewall]
    Logs[(Session Recording/Logs)]
    ExitNode[Tailscale Exit Node]
  end
  subgraph Intranet[GPU Cluster]
    GPU[GPU Nodes]
    AppUI[App UI 32000]
  end

  Vendor -- HTTPS/SSH --> Nginx --> FW --> Teleport --> GPU
  AppUser -- HTTPS --> Nginx --> FW --> Teleport --> AppUI
  Admin -- SSH 22 --> TSUser[Internal Tailscale Node]
  TSUser -- Tailscale Tunnel --> ExitNode --> GPU
  Teleport --> Logs
</pre>

<hr>

<h3 class="relative group">First, collapse the ports
    <div id="first-collapse-the-ports" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#first-collapse-the-ports" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>Teleport uses a range of ports by default, roughly 3022-3080. That is fine in a small test, but once Nginx, Docker, and cloud firewall rules join the party, it becomes annoying fast.</p>
<p>Multiplexing lets HTTPS, SSH, and tunnel traffic share port 3080:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># teleport.yaml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">auth_service</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">proxy_listener_mode</span><span class="p">:</span><span class="w"> </span><span class="l">multiplex</span></span></span></code></pre></div></div>
<p>It uses ALPN to distinguish traffic types. The practical win is boring but useful: only one public-facing port to reason about.</p>
<hr>

<h3 class="relative group">Installing the Agent in an isolated network: put the proxy in the repo file
    <div id="installing-the-agent-in-an-isolated-network-put-the-proxy-in-the-repo-file" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#installing-the-agent-in-an-isolated-network-put-the-proxy-in-the-repo-file" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>The GPU nodes cannot reach the internet directly, so installing the Teleport Agent needs a proxy. I first configured the system proxy and still got certificate errors from <code>yum</code>/<code>dnf</code>. The missing piece was setting the proxy explicitly in the repo file:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># /etc/yum.repos.d/teleport.repo</span>
</span></span><span class="line"><span class="cl"><span class="k">[teleport]</span>
</span></span><span class="line"><span class="cl"><span class="na">proxy</span><span class="o">=</span><span class="s">http://[Bastion_VIP]:8888</span></span></span></code></pre></div></div>
<p>This is not a glamorous problem, but it eats time. The error looks like TLS trouble, while the real issue is that the package manager is not using the proxy path you think it is using.</p>
<hr>

<h3 class="relative group">Web Terminal disconnects immediately? Check Nginx WebSocket handling
    <div id="web-terminal-disconnects-immediately-check-nginx-websocket-handling" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#web-terminal-disconnects-immediately-check-nginx-websocket-handling" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>When Nginx reverse-proxied the Teleport Web Terminal, the page loaded but the terminal disconnected as soon as it opened. These settings ended up being the minimum stable set:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl"><span class="k">proxy_pass</span> <span class="s">https://127.0.0.1:3080</span><span class="p">;</span>  <span class="c1"># MUST use https
</span></span></span><span class="line"><span class="cl"><span class="k">proxy_ssl_verify</span> <span class="no">off</span><span class="p">;</span>                <span class="c1"># For self-signed certs
</span></span></span><span class="line"><span class="cl"><span class="k">proxy_http_version</span> <span class="mi">1</span><span class="s">.1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">proxy_set_header</span> <span class="s">Upgrade</span> <span class="nv">$http_upgrade</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">proxy_set_header</span> <span class="s">Connection</span> <span class="s">"upgrade"</span><span class="p">;</span></span></span></code></pre></div></div>
<p>The <code>https</code> in <code>proxy_pass</code> is easy to overlook. The UI loading successfully does not mean the terminal channel is healthy.</p>
<hr>

<h3 class="relative group">Docker mapped ports may skip the INPUT chain you trusted
    <div id="docker-mapped-ports-may-skip-the-input-chain-you-trusted" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#docker-mapped-ports-may-skip-the-input-chain-you-trusted" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>This was the scary one. I had written restrictions in the <code>INPUT</code> chain and assumed 3080 was internal-only. Docker-published ports enter through <code>DOCKER-USER</code>, so those old rules were mostly useless. Port 3080 was still exposed to the public internet.</p>
<p>Note: <code>-F</code> below flushes the <code>DOCKER-USER</code> chain. Do not blindly paste this into production. Back it up first, or use <code>-I</code> to insert rules in the right place.</p>
<p>My fixed version looked like this:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">iptables -F DOCKER-USER
</span></span><span class="line"><span class="cl"><span class="c1"># Allow only localhost and Tailscale network</span>
</span></span><span class="line"><span class="cl">iptables -A DOCKER-USER -s 127.0.0.1 -p tcp --dport <span class="m">3080</span> -j ACCEPT
</span></span><span class="line"><span class="cl">iptables -A DOCKER-USER -s 100.64.0.0/10 -p tcp --dport <span class="m">3080</span> -j ACCEPT
</span></span><span class="line"><span class="cl">iptables -A DOCKER-USER -p tcp --dport <span class="m">3080</span> -j DROP</span></span></code></pre></div></div>
<hr>

<h3 class="relative group">Do not point Nginx at the VPN IP unless you enjoy circular failures
    <div id="do-not-point-nginx-at-the-vpn-ip-unless-you-enjoy-circular-failures" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#do-not-point-nginx-at-the-vpn-ip-unless-you-enjoy-circular-failures" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>At first I set the Nginx upstream to the Tailscale VPN IP. It looked tidy, but it created a circular dependency: if the VPN blipped, the management page went down too.</p>
<p>Changing it to <code>127.0.0.1</code> fixed that. Even if Tailscale is down, the public management entry still works, which is exactly what you want when you need to repair Tailscale.</p>
<hr>

<h3 class="relative group">Permission split
    <div id="permission-split" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#permission-split" aria-label="Anchor">#</a>
    </span>
    
</h3>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Role</th>
          <th style="text-align: left">Access Method</th>
          <th style="text-align: left">Auditing</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">Admin</td>
          <td style="text-align: left">SSH Port 22 + Ed25519 Key</td>
          <td style="text-align: left">None</td>
      </tr>
      <tr>
          <td style="text-align: left">Vendor</td>
          <td style="text-align: left">Web Terminal</td>
          <td style="text-align: left">Full recording, recordings hidden from vendor</td>
      </tr>
  </tbody>
</table>
<p>The RBAC trick is to remove <code>audit</code> from the vendor’s <code>restricted-dev</code> role, so they cannot see recordings or log pages.</p>
<p>One caveat: removing <code>audit</code> only changes visibility of recordings/logs. It does not guarantee that every recording banner disappears. Teleport behavior depends on version and configuration, so test this yourself before relying on the word “silent”.</p>
<p>Two extra constraints are worth adding:</p>
<ol>
<li>Vendors access only through Teleport Web/SSH; they do not get Tailscale.</li>
<li>Vendor roles restrict login users and visible nodes, with labels for isolation.</li>
</ol>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># Example: node labels (vendor name redacted)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">vendor</span><span class="p">:</span><span class="w"> </span><span class="l">vendor-x</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">env</span><span class="p">:</span><span class="w"> </span><span class="l">prod</span></span></span></code></pre></div></div>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># Example: role constraints (vendor login + visible nodes)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">role</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">allow</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">logins</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"vendor-user"</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">node_labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">vendor</span><span class="p">:</span><span class="w"> </span><span class="s2">"vendor-x"</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">deny</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">logins</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"root"</span><span class="p">]</span></span></span></code></pre></div></div>
<hr>

<h3 class="relative group">Core config snippets
    <div id="core-config-snippets" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#core-config-snippets" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p><strong>docker-compose.yml</strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">teleport</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s1">'3080:3080'</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s1">'127.0.0.1:3025:3025'</span><span class="w">  </span><span class="c"># Management API local only</span></span></span></code></pre></div></div>
<p><strong>teleport.yaml</strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">proxy_service</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">public_addr</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">teleport.example.com:443, 100.64.0.x:3080]</span></span></span></code></pre></div></div>
<hr>

<h3 class="relative group">App Access came later
    <div id="app-access-came-later" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#app-access-came-later" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p><strong>Expose apps (example)</strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">app_service</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="s2">"yes"</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">apps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">app-ui</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">uri</span><span class="p">:</span><span class="w"> </span><span class="l">http://10.120.0.0:32000/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">public_addr</span><span class="p">:</span><span class="w"> </span><span class="l">app-ui.example.com</span></span></span></code></pre></div></div>
<p><strong>Proxy environment causing 503</strong></p>
<p>If the node has <code>HTTP_PROXY</code>, Teleport may try to reach internal apps through that proxy and return 503. Adding <code>NO_PROXY</code> in systemd is safer:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1"># /etc/systemd/system/teleport.service</span>
</span></span><span class="line"><span class="cl"><span class="na">Environment</span><span class="o">=</span><span class="s">"NO_PROXY=localhost,127.0.0.1,10.0.0.0/8,100.64.0.0/10"</span></span></span></code></pre></div></div>
<p><strong>Nginx Host passthrough header</strong></p>
<p>For multi-subdomain access, do not forget the Host header:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl"><span class="k">proxy_set_header</span> <span class="s">Host</span> <span class="nv">$host</span><span class="p">;</span></span></span></code></pre></div></div>
<hr>
<p>With this setup, vendors work through the Web UI, internal ops connect directly over Tailscale, session recordings are kept, and the GPU cluster does not need to sit on the public internet.</p>
<p>Not elegant, but steady enough. For this kind of temporary-but-sensitive remote support, steady matters more than pretty.</p>
<hr>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">NOTE</span><span class="o">:</span> <span class="nx">I</span> <span class="nx">am</span> <span class="nx">not</span> <span class="nx">responsible</span> <span class="k">for</span> <span class="nx">any</span> <span class="nx">expired</span> <span class="nx">content</span><span class="p">.</span>
</span></span><span class="line"><span class="cl"><span class="nx">Created</span> <span class="nx">at</span><span class="o">:</span> <span class="mi">2026</span><span class="o">-</span><span class="mi">01</span><span class="o">-</span><span class="mi">03</span><span class="nx">T03</span><span class="o">:</span><span class="mi">58</span><span class="o">:</span><span class="mi">20</span><span class="o">+</span><span class="mi">08</span><span class="o">:</span><span class="mi">00</span>
</span></span><span class="line"><span class="cl"><span class="nx">Updated</span> <span class="nx">at</span><span class="o">:</span> <span class="mi">2026</span><span class="o">-</span><span class="mi">01</span><span class="o">-</span><span class="mi">03</span><span class="nx">T05</span><span class="o">:</span><span class="mi">55</span><span class="o">:</span><span class="mi">29</span><span class="o">+</span><span class="mi">08</span><span class="o">:</span><span class="mi">00</span>
</span></span><span class="line"><span class="cl"><span class="nx">Origin</span> <span class="nx">issue</span><span class="o">:</span> <span class="nx">https</span><span class="o">:</span><span class="c1">//github.com/ferstar/blog/issues/95
</span></span></span></code></pre></div></div>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Vibe Coding in Practice: When Code No Longer Needs to be Written by Hand</title>
      <link>https://blog.ferstar.org/en/posts/vibe-coding-engineering-practice/</link>
      <pubDate>Wed, 31 Dec 2025 21:46:31 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/en/posts/vibe-coding-engineering-practice/</guid>
      <description>AI-generated code is hard to control and expensive to verify; use engineered context and validation to make AI output usable code and ship reliably.</description><content:encoded><![CDATA[<blockquote><p>I am not a native English speaker; this article was translated by AI.</p>
</blockquote><blockquote><p>Based on the Vibe Coding Skeleton engineering practice.
<strong>Note</strong>: For specific implementation details, refer to <code>CLAUDE.md</code>, <code>.pre-commit-config.yaml</code>, <code>ast-grep/</code>, <code>sgconfig.yml</code>, and <code>justfile</code> in this repository.</p>
</blockquote><hr>
<p><strong>TL;DR</strong>: It is not about “letting AI write code,” but “using engineering to ensure AI writes the <em>right</em> code.”</p>
<hr>

<h3 class="relative group">I. A Hard Fact
    <div id="i-a-hard-fact" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#i-a-hard-fact" aria-label="Anchor">#</a>
    </span>
    
</h3>

<h4 class="relative group">1.1 The Data Shock
    <div id="11-the-data-shock" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#11-the-data-shock" aria-label="Anchor">#</a>
    </span>
    
</h4>
<blockquote><p>Consider a real project (anonymized), iterated over ~8 months with 1k+ commits.</p>
<p>Aside from requirement decomposition, business analysis, architectural adjustments, and final acceptance—
<strong>the vast majority of the code was generated by AI, while humans focused on decision-making and verification.</strong></p>
<p>Today, we aren’t debating <em>if</em> AI can write code.
We are discussing: <strong>How to make AI write the code correctly.</strong></p>
</blockquote><p><strong>Project Overview</strong>:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Metric</th>
          <th style="text-align: center">Value (Anonymized)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">Dev Cycle</td>
          <td style="text-align: center">~8 Months</td>
      </tr>
      <tr>
          <td style="text-align: left">Total Commits</td>
          <td style="text-align: center">1k+</td>
      </tr>
      <tr>
          <td style="text-align: left">Active Days</td>
          <td style="text-align: center">200+</td>
      </tr>
      <tr>
          <td style="text-align: left">Avg Commits/Day</td>
          <td style="text-align: center">~5</td>
      </tr>
      <tr>
          <td style="text-align: left">Conventional Commits Compliance</td>
          <td style="text-align: center">>95%</td>
      </tr>
  </tbody>
</table>
<p><strong>Commit Type Distribution</strong>:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Type</th>
          <th style="text-align: center">Share</th>
          <th style="text-align: left">Description</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">feat</td>
          <td style="text-align: center">≈40%</td>
          <td style="text-align: left">New Features</td>
      </tr>
      <tr>
          <td style="text-align: left">fix</td>
          <td style="text-align: center">≈20%</td>
          <td style="text-align: left">Bug Fixes</td>
      </tr>
      <tr>
          <td style="text-align: left">refactor</td>
          <td style="text-align: center">≈20%</td>
          <td style="text-align: left">Refactoring (including AI code optimization)</td>
      </tr>
      <tr>
          <td style="text-align: left">test</td>
          <td style="text-align: center">≈5%</td>
          <td style="text-align: left">Unit/Integration Tests</td>
      </tr>
      <tr>
          <td style="text-align: left">others</td>
          <td style="text-align: center">remainder</td>
          <td style="text-align: left">chore/docs/perf, etc.</td>
      </tr>
  </tbody>
</table>
<p><strong>Development Phase Evolution</strong>:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Phase</th>
          <th style="text-align: left">Characteristics</th>
          <th style="text-align: left">Focus</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">Sprint Phase</td>
          <td style="text-align: left">Higher <code>feat</code> ratio</td>
          <td style="text-align: left">Rapid MVP verification + Quality gates</td>
      </tr>
      <tr>
          <td style="text-align: left">Stability Phase</td>
          <td style="text-align: left">Rising <code>fix/refactor</code></td>
          <td style="text-align: left">Automated regression + Edge case/Perf validation</td>
      </tr>
  </tbody>
</table>

<h4 class="relative group">1.2 The Role Shift
    <div id="12-the-role-shift" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#12-the-role-shift" aria-label="Anchor">#</a>
    </span>
    
</h4>
<p><strong>Traditional Perception</strong> vs. <strong>Reality</strong>:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Traditional Perception</th>
          <th style="text-align: left">Reality</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">AI helps you write <em>some</em> code</td>
          <td style="text-align: left">AI generates <em>most</em> code; humans decide and verify</td>
      </tr>
      <tr>
          <td style="text-align: left">Human + AI collaboration</td>
          <td style="text-align: left">Human commands, AI executes, Human accepts</td>
      </tr>
      <tr>
          <td style="text-align: left">Learning to <em>use</em> AI</td>
          <td style="text-align: left">Learning to <em>verify</em> AI and <em>feed</em> context</td>
      </tr>
  </tbody>
</table>
<p><strong>The New Workflow</strong>:</p>
<pre class="not-prose mermaid">
flowchart TB
    subgraph Human's Work
        A[Decomposition] --> B[Business Analysis]
        B --> C[Architectural Decisions]
        C --> D[Context Preparation]
        D --> E[Verification & Review]
    end

    subgraph AI's Work
        F[Controller]
        G[Service]
        H[Schema]
        I[Test]
        J[Migration]
        K[Config]
        L[Commit Message]
    end

    D --> F & G & H & I & J & K & L
    F & G & H & I & J & K & L --> E
</pre>

<p><strong>Role Definitions</strong>:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Role</th>
          <th style="text-align: left">Responsibility</th>
          <th style="text-align: center">Unreplaceability</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">Product Manager</td>
          <td style="text-align: left">Requirements, Priorities</td>
          <td style="text-align: center">⭐⭐⭐⭐⭐</td>
      </tr>
      <tr>
          <td style="text-align: left">Architect</td>
          <td style="text-align: left">Tech Stack, Module Partitioning</td>
          <td style="text-align: center">⭐⭐⭐⭐⭐</td>
      </tr>
      <tr>
          <td style="text-align: left">QA</td>
          <td style="text-align: left">Validation, Edge Cases</td>
          <td style="text-align: center">⭐⭐⭐⭐</td>
      </tr>
      <tr>
          <td style="text-align: left"><del>Coder</del></td>
          <td style="text-align: left"><del>Writing Code</del></td>
          <td style="text-align: center"><del>Replaced by AI</del></td>
      </tr>
  </tbody>
</table>
<hr>

<h3 class="relative group">II. Why It Works
    <div id="ii-why-it-works" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#ii-why-it-works" aria-label="Anchor">#</a>
    </span>
    
</h3>

<h4 class="relative group">2.1 Context is King
    <div id="21-context-is-king" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#21-context-is-king" aria-label="Anchor">#</a>
    </span>
    
</h4>
<p><strong>The Core Formula</strong>:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">AI Coding Level = Your Context Provision Level</span></span></code></pre></div></div>
<p><strong>The Complete Version</strong>:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">AI Delivery Quality = Context Quality × Automated Verification × Task Alignment</span></span></code></pre></div></div>
<p>Without verification, output is just a draft. Without task alignment, verification costs explode.</p>
<p><strong>The Three-Layer Context Model</strong>:</p>
<pre class="not-prose mermaid">
graph TB
    subgraph Context Pyramid
        P["Project Context<br/>Tech Stack / Directory Structure / Naming Specs / Prohibitions<br/>CLAUDE.md"]
        T["Task Context<br/>Requirements / Boundaries / Acceptance Criteria / Domain Terms<br/>Prompt"]
        S["Session Context<br/>Chat History / Intermediate Results<br/>Auto-accumulated"]
    end

    P --> T --> S

    style P fill:#4ecdc4,stroke:#333,stroke-width:2px
    style T fill:#f38181
    style S fill:#fce38a
</pre>

<p><strong>Investment Return</strong>:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">ROI: Project >> Task >> Session
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Project: 8h investment → Benefits all tasks (Compound Interest)
</span></span><span class="line"><span class="cl">Task: 5-10min → Current task only
</span></span><span class="line"><span class="cl">Session: 0 → Current conversation only</span></span></code></pre></div></div>
<p><strong>Conclusion</strong>: Project Context is an “invest once, benefit continuously” asset. Spending time on a perfect <code>CLAUDE.md</code> is the highest ROI move.</p>

<h4 class="relative group">2.2 The Bottleneck Shift
    <div id="22-the-bottleneck-shift" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#22-the-bottleneck-shift" aria-label="Anchor">#</a>
    </span>
    
</h4>
<p><strong>My Time Allocation</strong>:</p>
<pre class="not-prose mermaid">
pie title Time Allocation (Empirical)
    "Requirement/Arch Design" : 20
    "Preparing Context" : 15
    "Waiting for AI" : 5
    "Verification + Fixes" : 40
    "Paying Technical Debt" : 20
</pre>

<p><strong>Key Insight</strong>: 40% of time is spent “verifying,” not “writing.” The bottleneck has shifted from generation to validation.</p>
<hr>

<h3 class="relative group">III. How It’s Done
    <div id="iii-how-its-done" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#iii-how-its-done" aria-label="Anchor">#</a>
    </span>
    
</h3>

<h4 class="relative group">3.1 Project-level Context: CLAUDE.md
    <div id="31-project-level-context-claudemd" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#31-project-level-context-claudemd" aria-label="Anchor">#</a>
    </span>
    
</h4>
<p>This project uses a ~300-line <code>CLAUDE.md</code> as an “AI Onboarding Manual.”</p>
<blockquote><p><strong>Note</strong>: Different AI tools prefer different filenames (e.g., <code>CLAUDE.md</code>, <code>.cursorrules</code>, <code>AGENTS.md</code>). The essence is the same: translate your project rules into machine-readable specs.</p>
</blockquote><p><strong>Core Structure</strong>:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl"><span class="gh"># CLAUDE.md
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Project Overview        # What the project is for
</span></span></span><span class="line"><span class="cl"><span class="gu">## Technology Stack        # Litestar + PostgreSQL + Redis + OpenDAL + Celery
</span></span></span><span class="line"><span class="cl"><span class="gu">## Basic Rules             # Activate venv first, manage deps with uv
</span></span></span><span class="line"><span class="cl"><span class="gu">## Development Commands    # just lint, just test
</span></span></span><span class="line"><span class="cl"><span class="gu">## Architecture Overview   # DDD layers, business grouping under src/domain/
</span></span></span><span class="line"><span class="cl"><span class="gu">## Coding Style            # ruff formatting, 150 char line width
</span></span></span><span class="line"><span class="cl"><span class="gu">## Testing Guidelines      # pytest (fail-fast, warnings-as-errors)
</span></span></span><span class="line"><span class="cl">## CLI Commands            # litestar database upgrade, litestar run</span></span></code></pre></div></div>

<h5 class="relative group">Case Study: How ast-grep Intercepts Bad AI Habits
    <div id="case-study-how-ast-grep-intercepts-bad-ai-habits" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#case-study-how-ast-grep-intercepts-bad-ai-habits" aria-label="Anchor">#</a>
    </span>
    
</h5>
<p><strong>Requirement</strong>: Write a function to process a temporary PDF file.</p>
<p><strong>Typical AI Output</strong>:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">tempfile</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">process_pdf</span><span class="p">(</span><span class="n">content</span><span class="p">:</span> <span class="nb">bytes</span><span class="p">)</span> <span class="o">-></span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Using delete=False for subsequent processing</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">tempfile</span><span class="o">.</span><span class="n">NamedTemporaryFile</span><span class="p">(</span><span class="n">suffix</span><span class="o">=</span><span class="s2">".pdf"</span><span class="p">,</span> <span class="n">delete</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span> <span class="k">as</span> <span class="n">tmp</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">tmp_path</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="n">tmp</span><span class="o">.</span><span class="n">name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">tmp</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">content</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">result</span> <span class="o">=</span> <span class="n">parse_pdf</span><span class="p">(</span><span class="n">tmp_path</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">result</span>
</span></span><span class="line"><span class="cl">    <span class="k">finally</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">tmp_path</span><span class="o">.</span><span class="n">unlink</span><span class="p">(</span><span class="n">missing_ok</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>  <span class="c1"># Manual cleanup</span></span></span></code></pre></div></div>
<p><strong>The Issues</strong>:</p>
<ul>
<li><code>delete=False</code> forces manual cleanup logic, increasing maintenance cost.</li>
<li>Cleanup logic is easily missed or written incorrectly (e.g., forgetting <code>try/finally</code>), leading to file leaks.</li>
</ul>
<p><strong>ast-grep Interception</strong>:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ git commit -m <span class="s2">"feat: add pdf processor"</span>
</span></span><span class="line"><span class="cl">error<span class="o">[</span>no-tempfile-delete-false<span class="o">]</span>: NamedTemporaryFile with <span class="nv">delete</span><span class="o">=</span>False is forbidden
</span></span><span class="line"><span class="cl">  --> src/utils/pdf.py:6:10
</span></span><span class="line"><span class="cl">   <span class="p">|</span>
</span></span><span class="line"><span class="cl"> <span class="m">6</span> <span class="p">|</span>     with tempfile.NamedTemporaryFile<span class="o">(</span><span class="nv">suffix</span><span class="o">=</span><span class="s2">".pdf"</span>, <span class="nv">delete</span><span class="o">=</span>False<span class="o">)</span> as tmp:
</span></span><span class="line"><span class="cl">   <span class="p">|</span>          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^</span></span></code></pre></div></div>
<p><strong>Fixed Version</strong>:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">process_pdf</span><span class="p">(</span><span class="n">content</span><span class="p">:</span> <span class="nb">bytes</span><span class="p">)</span> <span class="o">-></span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">tempfile</span><span class="o">.</span><span class="n">NamedTemporaryFile</span><span class="p">(</span><span class="n">suffix</span><span class="o">=</span><span class="s2">".pdf"</span><span class="p">)</span> <span class="k">as</span> <span class="n">tmp</span><span class="p">:</span>  <span class="c1"># default delete=True</span>
</span></span><span class="line"><span class="cl">        <span class="n">tmp_path</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="n">tmp</span><span class="o">.</span><span class="n">name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">tmp</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">content</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">tmp</span><span class="o">.</span><span class="n">flush</span><span class="p">()</span>  <span class="c1"># Ensure data is on disk</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">result</span> <span class="o">=</span> <span class="n">parse_pdf</span><span class="p">(</span><span class="n">tmp_path</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">result</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Automatic cleanup on block exit, zero leak risk.</span></span></span></code></pre></div></div>
<p><strong>The Point</strong>: Don’t rely on AI “voluntarily obeying” your rules. Use <code>ast-grep</code> to <strong>force compliance</strong>.</p>

<h4 class="relative group">3.2 Automated Validation Workflow
    <div id="32-automated-validation-workflow" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#32-automated-validation-workflow" aria-label="Anchor">#</a>
    </span>
    
</h4>
<p>AI-generated code must pass through a quality pipeline. If the gate fails, the code doesn’t enter the main branch.</p>
<p><strong>The Pipeline</strong>:</p>
<pre class="not-prose mermaid">
flowchart LR
    A[Code Change] --> B[pre-commit: ruff format]
    B --> C[pre-commit: ruff check]
    C --> D[pre-commit: ast-grep scan]
    D --> E[git commit]
    E --> F[commit-msg: Conventional Commits]
    F --> G[git push]
    G --> H[pre-push: pytest]

    style D fill:#ff6b6b,stroke:#333,stroke-width:2px
    style H fill:#4ecdc4,stroke:#333,stroke-width:2px
</pre>

<p><strong>ast-grep Rules (Selected)</strong>:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Rule ID</th>
          <th style="text-align: left">What it Intercepts</th>
          <th style="text-align: left">Why</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">no-match-case</td>
          <td style="text-align: left"><code>match/case</code> syntax</td>
          <td style="text-align: left">Cython compatibility</td>
      </tr>
      <tr>
          <td style="text-align: left">no-tempfile-delete-false</td>
          <td style="text-align: left"><code>NamedTemporaryFile(delete=False)</code></td>
          <td style="text-align: left">Prevent leaks, reduce boilerplate</td>
      </tr>
      <tr>
          <td style="text-align: left">no-global-src-import-in-cli</td>
          <td style="text-align: left"><code>**/cli.py</code> importing <code>src.*</code></td>
          <td style="text-align: left">Avoid startup side-effects / circular deps</td>
      </tr>
      <tr>
          <td style="text-align: left">no-local-import-in-prod</td>
          <td style="text-align: left">“Local imports” in production code</td>
          <td style="text-align: left">Keep dependencies visible for auditing</td>
      </tr>
  </tbody>
</table>
<hr>

<h3 class="relative group">IV. Boundaries and Stop-Loss
    <div id="iv-boundaries-and-stop-loss" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#iv-boundaries-and-stop-loss" aria-label="Anchor">#</a>
    </span>
    
</h3>

<h4 class="relative group">4.1 Sweet Spot vs. Quagmire
    <div id="41-sweet-spot-vs-quagmire" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#41-sweet-spot-vs-quagmire" aria-label="Anchor">#</a>
    </span>
    
</h4>
<p>It’s not about whether AI <em>can</em> write it—AI can write anything. It’s about whether the <strong>verification cost</strong> is acceptable.</p>
<pre class="not-prose mermaid">
flowchart TB
    subgraph High Verification Cost
        subgraph Q1["❌ Quagmire"]
            Q1A["Complex Business"]
            Q1B["Performance Optimization"]
            Q1C["Algorithm Design"]
        end
    end
    subgraph Low Verification Cost
        subgraph Q3["✅ Sweet Spot"]
            Q3A["CRUD APIs"]
            Q3B["Unit Tests"]
            Q3C["Config Files"]
            Q3D["DB Migrations"]
        end
    end

    style Q3 fill:#4ecdc4,stroke:#333
    style Q1 fill:#ff6b6b,stroke:#333
</pre>


<h5 class="relative group">Case Study: The Correct Way to Optimize Performance
    <div id="case-study-the-correct-way-to-optimize-performance" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#case-study-the-correct-way-to-optimize-performance" aria-label="Anchor">#</a>
    </span>
    
</h5>
<p><strong>Scenario: N+1 Query Optimization</strong></p>
<p><strong>Problem</strong>: A list API response time spikes from 100ms to several seconds.</p>
<p><strong>Wrong Way</strong> — Letting AI “guess” without data:</p>
<blockquote><p>Me: “This API is slow, optimize it.”
AI: “Add caching / Add indexes / Preload…” (Often ineffective shots in the dark).</p>
</blockquote><p><strong>Correct Way</strong> — Feed the AI “Runtime Facts”:</p>
<ol>
<li><strong>Human</strong>: Execute <code>EXPLAIN (ANALYZE, BUFFERS)</code> in test env.</li>
<li><strong>AI (Analytical)</strong>: Locate bottleneck based on the plan (e.g., duplicated scans in a subquery).</li>
<li><strong>Human</strong>: Confirm the rewrite strategy + required migration.</li>
<li><strong>AI (Executive)</strong>: Generate code + migration scripts + tests.</li>
<li><strong>Human</strong>: Run <code>just test</code> and compare load test metrics.</li>
</ol>

<h4 class="relative group">4.2 The Abandonment Threshold
    <div id="42-the-abandonment-threshold" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#42-the-abandonment-threshold" aria-label="Anchor">#</a>
    </span>
    
</h4>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Signal</th>
          <th style="text-align: left">Action</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">Fails 3 times in a row</td>
          <td style="text-align: left">Write it yourself</td>
      </tr>
      <tr>
          <td style="text-align: left">Explaining > 5 mins of domain knowledge</td>
          <td style="text-align: left">Write it yourself</td>
      </tr>
      <tr>
          <td style="text-align: left">Safety-sensitive logic</td>
          <td style="text-align: left">Write it yourself + Peer Review</td>
      </tr>
  </tbody>
</table>
<hr>

<h3 class="relative group">V. The Elephant in the Room
    <div id="v-the-elephant-in-the-room" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#v-the-elephant-in-the-room" aria-label="Anchor">#</a>
    </span>
    
</h3>

<h4 class="relative group">5.1 Is your concern valid?
    <div id="51-is-your-concern-valid" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#51-is-your-concern-valid" aria-label="Anchor">#</a>
    </span>
    
</h4>
<p>“If AI writes the code, what am I?”</p>
<ul>
<li><strong>Is it just “KPI Marketing”?</strong> No. Prompt engineering, requirement decomposition, and verification <em>are</em> programming, just in a different language.</li>
<li><strong>How do Juniors learn?</strong> By reading high-quality open-source code and reviewing AI mistakes, rather than mindlessly typing CRUD.</li>
<li><strong>Reviewing AI is tiring.</strong> Yes. It takes 40% of the time because AI code “looks” correct but may hide subtle bugs.</li>
</ul>

<h4 class="relative group">5.2 A Reality Check
    <div id="52-a-reality-check" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#52-a-reality-check" aria-label="Anchor">#</a>
    </span>
    
</h4>
<p>You aren’t the AI’s secretary; <strong>the AI is your intern</strong>.</p>
<ul>
<li>The intern can work, but you must tell them what to do.</li>
<li>The intern makes mistakes; you must review them.</li>
<li>If the intern does well, the credit is yours.</li>
</ul>
<p>The difference? This intern doesn’t sleep, doesn’t complain, and you can manage ten of them at once.</p>
<p><strong>Has the barrier to entry lowered?</strong>
No. It has <strong>shifted</strong>—from “knowing how to type code” to “knowing how to understand systems + verify them.”</p>
<p><strong>In the AI era, the requirements for engineers are actually HIGHER, not lower.</strong></p>
<hr>

<h3 class="relative group">VI. Action List (TL;DR)
    <div id="vi-action-list-tldr" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#vi-action-list-tldr" aria-label="Anchor">#</a>
    </span>
    
</h3>
<ul>
<li><input disabled="" type="checkbox"> Create <code>CLAUDE.md</code> with your tech stack and rules.</li>
<li><input disabled="" type="checkbox"> Solidify a zero-cost validation loop (e.g., <code>just lint</code>, <code>just test</code>).</li>
<li><input disabled="" type="checkbox"> Configure <code>pre-commit</code> hooks (ruff + ast-grep).</li>
<li><input disabled="" type="checkbox"> Identify “Sweet Spot” tasks for AI delegation.</li>
</ul>
<p><strong>Tools change, but our value in solving problems remains. Be the driver of AI, not the passenger.</strong></p>
<hr>
<p><em>Created: 2025-12-31</em>
*Updated: 2026-01-03"</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Troubleshooting MTU Black Holes in Multi-Layer Tunnels: The Mystery of 30KB/s scp</title>
      <link>https://blog.ferstar.org/en/posts/mtu-black-hole-scp-optimization/</link>
      <pubDate>Wed, 31 Dec 2025 12:09:54 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/en/posts/mtu-black-hole-scp-optimization/</guid>
      <description>ICMP-disabled tunnels can throttle scp to 30KB/s; use MTU probing and TCP MSS clamping; restore normal throughput across multi-layer tunnels.</description><content:encoded><![CDATA[<blockquote><p>I am not a native English speaker; this article was translated by AI.</p>
</blockquote>
<h3 class="relative group">The Problem
    <div id="the-problem" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#the-problem" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>During a cross-regional GPU cluster deployment, two layers of tunnels were established between Site-A and Site-B: <strong>WireGuard + IPsec</strong>. Due to security compliance, <strong>ICMP was disabled site-wide</strong>—<code>ping</code> and <code>traceroute</code> were useless.</p>
<p>Then came a strange symptom:</p>
<ul>
<li><strong>A → B</strong>: 50Mbps, full speed, perfect.</li>
<li><strong>B → A</strong>: SSH connects, but <code>scp</code> of large files drops to exactly <strong>30KB/s</strong>.</li>
</ul>
<p>This is a classic “one-way street” symptom. The link isn’t broken, but a protocol-level ceiling is being hit.</p>
<hr>

<h3 class="relative group">Troubleshooting: How to play without ICMP?
    <div id="troubleshooting-how-to-play-without-icmp" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#troubleshooting-how-to-play-without-icmp" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>Normally, you’d use <code>ping -s <size> -M do</code> to detect Path MTU. But with ICMP blocked, there’s no feedback for “Packet Too Big.”</p>
<p>I had to rely on <strong>TCP behavioral fingerprints</strong>. I conducted experiments with <code>iperf3</code>:</p>

<h4 class="relative group">1) Default TCP Test
    <div id="1-default-tcp-test" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#1-default-tcp-test" aria-label="Anchor">#</a>
    </span>
    
</h4>
<p>Bandwidth locked at ~0.4Mbps with massive retransmissions (<code>Retr</code>).</p>

<h4 class="relative group">2) UDP Control Test
    <div id="2-udp-control-test" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#2-udp-control-test" aria-label="Anchor">#</a>
    </span>
    
</h4>
<p>20Mbps worked normally with controllable loss. The link itself was fine.</p>

<h4 class="relative group">3) TCP MSS Stepping (The Breakthrough)
    <div id="3-tcp-mss-stepping-the-breakthrough" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#3-tcp-mss-stepping-the-breakthrough" aria-label="Anchor">#</a>
    </span>
    
</h4>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">iperf3 -c <SITE_A_IP> -t <span class="m">10</span> -M <span class="m">1400</span>  <span class="c1"># Bandwidth collapses, heavy retrans</span>
</span></span><span class="line"><span class="cl">iperf3 -c <SITE_A_IP> -t <span class="m">10</span> -M <span class="m">1350</span>  <span class="c1"># Instant recovery to ~49Mbps</span></span></span></code></pre></div></div>
<p><strong>Case Closed</strong>: A classic <strong>PMTUD Black Hole</strong> triggered in a “blind” network environment. This is a typical “expert’s blind spot”: you have <code>tcpdump</code> and <code>wireshark</code> running, looking for complex protocol issues, while ignoring the most fundamental link parameters.</p>
<hr>

<h3 class="relative group">The Theory: Who is “Murdering” the Packets?
    <div id="the-theory-who-is-murdering-the-packets" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#the-theory-who-is-murdering-the-packets" aria-label="Anchor">#</a>
    </span>
    
</h3>

<h4 class="relative group">MTU/MSS Breakdown
    <div id="mtumss-breakdown" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#mtumss-breakdown" aria-label="Anchor">#</a>
    </span>
    
</h4>
<p>TCP MSS is the maximum payload of a single TCP segment, limited by the Path MTU.</p>
<ul>
<li>IPv4 approx: <code>MSS ≈ MTU - 20(IP) - 20(TCP) - Options</code></li>
<li>Standard Ethernet MTU 1500 results in ~1460 MSS.</li>
</ul>
<p>In multi-layer tunnels (WireGuard + IPsec), the encapsulation headers continuously “eat” the available MTU. The default large MSS becomes an “overloaded packet.”</p>

<h4 class="relative group">Why the Black Hole?
    <div id="why-the-black-hole" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#why-the-black-hole" aria-label="Anchor">#</a>
    </span>
    
</h4>
<ol>
<li>The sender sends a large packet based on default MSS.</li>
<li>An intermediate node finds the packet too large and tries to return an ICMP (Fragmentation Needed).</li>
<li>But <strong>ICMP is blocked</strong>, so this signal is lost.</li>
<li>The sender never receives the notice, assumes the packet was dropped due to congestion, and retransmits the <em>same oversized packet</em> indefinitely.</li>
<li>Throughput is killed by retransmissions.</li>
</ol>
<hr>

<h3 class="relative group">Solutions
    <div id="solutions" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#solutions" aria-label="Anchor">#</a>
    </span>
    
</h3>

<h4 class="relative group">A. The Cure (Ideal)
    <div id="a-the-cure-ideal" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#a-the-cure-ideal" aria-label="Anchor">#</a>
    </span>
    
</h4>
<p>Allow specifically required ICMP types:</p>
<ul>
<li>IPv4: ICMP Type 3 Code 4 (Fragmentation Needed)</li>
<li>IPv6: ICMPv6 Type 2 (Packet Too Big)</li>
</ul>

<h4 class="relative group">B. Workaround 1: Lower Interface MTU
    <div id="b-workaround-1-lower-interface-mtu" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#b-workaround-1-lower-interface-mtu" aria-label="Anchor">#</a>
    </span>
    
</h4>
<p>Lower the MTU on the WireGuard or IPsec interfaces to stop the bleeding.</p>

<h4 class="relative group">C. Workaround 2: MSS Clamping (My Implementation)
    <div id="c-workaround-2-mss-clamping-my-implementation" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#c-workaround-2-mss-clamping-my-implementation" aria-label="Anchor">#</a>
    </span>
    
</h4>
<p>Since ICMP is unreliable, force the MSS rewrite on the transit gateway:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl"><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># --- Config ---</span>
</span></span><span class="line"><span class="cl"><span class="nv">PHY_IF</span><span class="o">=</span><span class="s2">"ens3"</span>
</span></span><span class="line"><span class="cl"><span class="nv">REMOTE_NET</span><span class="o">=</span><span class="s2">"10.0.0.0/24"</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 1. Physical Layer Optimization (increases CPU overhead, enable as needed)</span>
</span></span><span class="line"><span class="cl">ip link <span class="nb">set</span> dev <span class="s2">"</span><span class="nv">$PHY_IF</span><span class="s2">"</span> mtu <span class="m">1400</span>
</span></span><span class="line"><span class="cl">ethtool -K <span class="s2">"</span><span class="nv">$PHY_IF</span><span class="s2">"</span> tso off gso off gro off
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 2. MSS Clamping</span>
</span></span><span class="line"><span class="cl"><span class="c1"># For Forwarded traffic: 1280 (Conservative)</span>
</span></span><span class="line"><span class="cl">iptables -t mangle -C FORWARD -p tcp -d <span class="s2">"</span><span class="nv">$REMOTE_NET</span><span class="s2">"</span> --tcp-flags SYN,RST SYN -j TCPMSS --set-mss <span class="m">1280</span> 2>/dev/null <span class="o">||</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">iptables -t mangle -I FORWARD -p tcp -d <span class="s2">"</span><span class="nv">$REMOTE_NET</span><span class="s2">"</span> --tcp-flags SYN,RST SYN -j TCPMSS --set-mss <span class="m">1280</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># For Local traffic: 1350 (Aggressive but functional in this case)</span>
</span></span><span class="line"><span class="cl">iptables -t mangle -C OUTPUT -p tcp -d <span class="s2">"</span><span class="nv">$REMOTE_NET</span><span class="s2">"</span> --tcp-flags SYN,RST SYN -j TCPMSS --set-mss <span class="m">1350</span> 2>/dev/null <span class="o">||</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">iptables -t mangle -I OUTPUT -p tcp -d <span class="s2">"</span><span class="nv">$REMOTE_NET</span><span class="s2">"</span> --tcp-flags SYN,RST SYN -j TCPMSS --set-mss <span class="m">1350</span></span></span></code></pre></div></div>
<hr>

<h3 class="relative group">Summary
    <div id="summary" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#summary" aria-label="Anchor">#</a>
    </span>
    
</h3>
<ol>
<li><strong>MTU is the Foundation</strong>: In VPN/Overlay scenarios, the default 1500 is a trap.</li>
<li><strong>Asymmetric Thinking</strong>: One-way speed limits are often MTU issues, not bandwidth caps.</li>
<li><strong>Better Safe than Sorry</strong>: Set a conservative MSS. Sacrificing a bit of payload efficiency is worth a 100x gain in stability.</li>
</ol>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Linux 6.17 Network Tuning: DualPI2 &#43; BBR to Eliminate Bufferbloat on 50Mbps Bandwidth</title>
      <link>https://blog.ferstar.org/en/posts/linux-network-bbr-dualpi2/</link>
      <pubDate>Thu, 18 Dec 2025 06:30:04 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/en/posts/linux-network-bbr-dualpi2/</guid>
      <description>Large downloads spike latency on 50Mbps links; use Linux 6.17 DualPI2 + BBR tuning; keep SSH and interactive traffic responsive under load.</description><content:encoded><![CDATA[<blockquote><p>I am not a native English speaker; this article was translated by AI.</p>
</blockquote>
<h2 class="relative group">Background
    <div id="background" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#background" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>When deploying a VPN relay service on a 50Mbps cloud host, I encountered the classic <strong>Bufferbloat</strong> problem: whenever a large download saturated the bandwidth, SSH latency spiked from 50ms to over 200ms, severely degrading the interactive experience.</p>
<p>The traditional <code>fq</code> + <code>bbr</code> combo has limited effectiveness on narrow bandwidth links because <code>fq</code> only ensures fair queuing—it doesn’t actively control queuing delay.</p>

<h2 class="relative group">The Solution: DualPI2 + BBR
    <div id="the-solution-dualpi2--bbr" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#the-solution-dualpi2--bbr" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>Linux 6.17 introduced the <code>sch_dualpi2</code> scheduler, designed specifically for low latency:</p>
<table>
  <thead>
      <tr>
          <th>Scheduler</th>
          <th>Mechanism</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>fq</code></td>
          <td>Fair Queuing: Ensures each connection gets equal bandwidth but ignores queuing delay.</td>
      </tr>
      <tr>
          <td><code>dualpi2</code></td>
          <td>Active Queue Management: Controls queuing time using ECN marking or packet drops to force senders to slow down, strictly maintaining a latency floor.</td>
      </tr>
  </tbody>
</table>

<h2 class="relative group">Configuration Steps
    <div id="configuration-steps" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#configuration-steps" aria-label="Anchor">#</a>
    </span>
    
</h2>

<h3 class="relative group">1. Requirements
    <div id="1-requirements" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#1-requirements" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>Linux Kernel 6.17 or higher (includes the <code>sch_dualpi2</code> module).</p>

<h3 class="relative group">2. sysctl Configuration
    <div id="2-sysctl-configuration" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#2-sysctl-configuration" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>Edit <code>/etc/sysctl.conf</code>:</p>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-conf" data-lang="conf"># --- Core Scheduling Scheme ---
net.core.default_qdisc = dualpi2
net.ipv4.tcp_congestion_control = bbr

# --- Latency Optimization ---
net.ipv4.tcp_ecn = 1              # Essential for dualpi2
net.ipv4.tcp_fastopen = 3
net.ipv4.tcp_recovery = 1         # RACK for fast reordering fixes
net.ipv4.tcp_notsent_lowat = 8192 # Lower BBR kernel buffering

# --- Buffer Settings (Preventing Bloat on 50Mbps) ---
net.core.rmem_max = 4194304
net.core.wmem_max = 4194304
net.ipv4.tcp_rmem = 4096 16384 4194304
net.ipv4.tcp_wmem = 4096 16384 4194304

# --- Stability Optimization ---
net.core.somaxconn = 2048
net.ipv4.tcp_max_syn_backlog = 2048
net.mptcp.enabled = 1</code></pre></div>

<h3 class="relative group">3. Auto-load Module
    <div id="3-auto-load-module" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#3-auto-load-module" aria-label="Anchor">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">"sch_dualpi2"</span> <span class="p">|</span> sudo tee /etc/modules-load.d/dualpi2.conf</span></span></code></pre></div></div>

<h3 class="relative group">4. Persistence (udev Rule)
    <div id="4-persistence-udev-rule" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#4-persistence-udev-rule" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>Create <code>/etc/udev/rules.d/99-ens3-dualpi2.rules</code>:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">ACTION</span><span class="o">==</span><span class="s2">"add"</span>, <span class="nv">SUBSYSTEM</span><span class="o">==</span><span class="s2">"net"</span>, <span class="nv">NAME</span><span class="o">==</span><span class="s2">"ens3"</span>, <span class="nv">RUN</span><span class="o">+=</span><span class="s2">"/sbin/tc qdisc replace dev ens3 root dualpi2"</span></span></span></code></pre></div></div>
<blockquote><p>Replace <code>ens3</code> with your network interface name.</p>
</blockquote>
<h3 class="relative group">5. Apply Configuration
    <div id="5-apply-configuration" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#5-apply-configuration" aria-label="Anchor">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo sysctl -p
</span></span><span class="line"><span class="cl">sudo tc qdisc replace dev ens3 root dualpi2</span></span></code></pre></div></div>

<h2 class="relative group">Verification Methods
    <div id="verification-methods" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#verification-methods" aria-label="Anchor">#</a>
    </span>
    
</h2>

<h3 class="relative group">Check DualPI2 Status
    <div id="check-dualpi2-status" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#check-dualpi2-status" aria-label="Anchor">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">tc -s qdisc show dev ens3
</span></span><span class="line"><span class="cl"><span class="c1"># Observe the target value, default is 5ms</span></span></span></code></pre></div></div>

<h3 class="relative group">Stress Testing (Solution for ICMP-Disabled Servers)
    <div id="stress-testing-solution-for-icmp-disabled-servers" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#stress-testing-solution-for-icmp-disabled-servers" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>Traditional <code>ping</code> fails on servers with ICMP blocked. Use kernel RTT statistics instead:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Client: Saturate the bandwidth</span>
</span></span><span class="line"><span class="cl">iperf3 -c <server> -t <span class="m">60</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Server: Observe Kernel RTT</span>
</span></span><span class="line"><span class="cl">ss -tni
</span></span><span class="line"><span class="cl"><span class="c1"># Focus on rtt:X/Y — X is mean RTT, Y is jitter (mdev)</span></span></span></code></pre></div></div>

<h2 class="relative group">Results
    <div id="results" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#results" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>Under full 50Mbps load:</p>
<table>
  <thead>
      <tr>
          <th>Metric</th>
          <th>Result</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Throughput</td>
          <td>50 Mbps (Peak 51.4 Mbps)</td>
      </tr>
      <tr>
          <td>SSH RTT under load</td>
          <td>54.78ms (Physical limit 48.33ms)</td>
      </tr>
      <tr>
          <td>RTT Jitter</td>
          <td><strong>2.08ms</strong> ✅</td>
      </tr>
  </tbody>
</table>
<p><strong>SSH latency remained only 6.4ms above the physical limit during full downloads, with 2ms jitter. Bufferbloat is completely eliminated.</strong></p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Why Rooting Still Matters in 2025: A Power User&#39;s Perspective</title>
      <link>https://blog.ferstar.org/en/posts/why-rooting-still-matters-in-2025/</link>
      <pubDate>Sat, 04 Oct 2025 14:42:47 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/en/posts/why-rooting-still-matters-in-2025/</guid>
      <description>Android power users still face performance and privacy limits; use root plus kernel and system-level tuning; regain control with faster, cleaner, and more predictable devices.</description><content:encoded><![CDATA[<blockquote><p>I am not a native English speaker; this article was translated by AI.</p>
</blockquote><p>Android has matured significantly, but for power users, rooting remains a prerequisite for total control over the hardware and software experience. Here is why rooting still matters in 2025.</p>
<hr>

<h2 class="relative group">1. Bleeding Edge Kernel Features
    <div id="1-bleeding-edge-kernel-features" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#1-bleeding-edge-kernel-features" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>Manufacturers often use older, stable kernels. Rooting allows me to leverage modern features:</p>
<ul>
<li><strong>BBRv2 Congestion Control</strong>: Significantly improves speeds on cellular networks (noticeable on China Unicom 4G/5G).</li>
<li><strong>F2FS Optimization</strong>: Paired with UFS 3.1, it offers noticeable gains in random I/O performance.</li>
<li><strong>Zstd Compressed ZRAM</strong>: Offers ~20% better compression than lz4.</li>
<li><strong>Custom CPU Schedulers</strong>: Schedulers like EAS (Energy Aware Scheduling) can reduce daily power consumption by ~15%.</li>
</ul>
<p>I’ve compiled custom kernels for <code>xaga</code> and <code>RMX3888</code> devices, enabling many features disabled by OEMs. KernelSU has proven to be more stable than Magisk for these scenarios.</p>
<p><strong>Tools</strong>: Franco Kernel Manager / UKMM</p>

<h2 class="relative group">2. Killing “Cloud Control” and OEM Bloat
    <div id="2-killing-cloud-control-and-oem-bloat" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#2-killing-cloud-control-and-oem-bloat" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>Manufacturers often push unwanted “optimizations” or ads silently via cloud control. My approach:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Freeze system updates</span>
</span></span><span class="line"><span class="cl">pm disable com.xxx.ota
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Block cloud control domains via hosts</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">"127.0.0.1 cloud.manufacturer.com"</span> >> /system/etc/hosts</span></span></code></pre></div></div>
<p><strong>Essential Modules</strong>: CorePatch (bypass signature verification), Shamiko (hide root).</p>

<h2 class="relative group">3. Bypassing Ecosystem Locks
    <div id="3-bypassing-ecosystem-locks" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#3-bypassing-ecosystem-locks" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>Some banking apps or region-locked games restrict access based on device models. Technically, this involves hooking the <code>android.os.Build</code> class:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">XposedHelpers</span><span class="p">.</span><span class="na">setStaticObjectField</span><span class="p">(</span><span class="n">Build</span><span class="p">.</span><span class="na">class</span><span class="p">,</span><span class="w"> </span><span class="s">"MODEL"</span><span class="p">,</span><span class="w"> </span><span class="s">"SM-G9980"</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">XposedHelpers</span><span class="p">.</span><span class="na">setStaticObjectField</span><span class="p">(</span><span class="n">Build</span><span class="p">.</span><span class="na">class</span><span class="p">,</span><span class="w"> </span><span class="s">"MANUFACTURER"</span><span class="p">,</span><span class="w"> </span><span class="s">"samsung"</span><span class="p">);</span></span></span></code></pre></div></div>
<p><strong>Modules</strong>: Device Faker / Hide My Applist.</p>

<h2 class="relative group">4. Automated Bookkeeping (The LSPosed Way)
    <div id="4-automated-bookkeeping-the-lsposed-way" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#4-automated-bookkeeping-the-lsposed-way" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>Accessibility services and OCR are too unstable for automated bookkeeping. LSPosed hooks are the gold standard—hooking directly into the payment success callback:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="c1">// Hooking WeChat Pay success callback</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">findAndHookMethod</span><span class="p">(</span><span class="s">"com.tencent.mm.plugin.wallet..."</span><span class="p">,</span><span class="w"> </span><span class="s">"onPaySuccess"</span><span class="p">,</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">new</span><span class="w"> </span><span class="n">XC_MethodHook</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="kd">protected</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">afterHookedMethod</span><span class="p">(</span><span class="n">MethodHookParam</span><span class="w"> </span><span class="n">param</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="c1">// Extract amount and merchant info directly into the database</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">saveTransaction</span><span class="p">(</span><span class="n">param</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">});</span></span></span></code></pre></div></div>
<p>It achieves 100% accuracy and never gets killed by the system because it runs within the app process itself.</p>
<p><strong>Reference</strong>: AutoAccounting (Open Source).</p>

<h2 class="relative group">5. System-Wide Ad Blocking
    <div id="5-system-wide-ad-blocking" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#5-system-wide-ad-blocking" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>Beyond DNS-based blocking, root allows for:</p>
<ul>
<li><strong>AdAway</strong>: The classic hosts-based blocker.</li>
<li><strong>ReVanced</strong>: The essential YouTube and streaming app enhancer.</li>
<li><strong>iptables Rules</strong>: Forcefully rejecting ad-domain traffic at the kernel level.</li>
</ul>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Reject ad domains via iptables</span>
</span></span><span class="line"><span class="cl">iptables -A OUTPUT -d ad.xxx.com -j REJECT</span></span></code></pre></div></div>

<h2 class="relative group">6. Underclocking and Undervolting for Longevity
    <div id="6-underclocking-and-undervolting-for-longevity" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#6-underclocking-and-undervolting-for-longevity" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>Every SoC is different. On my Snapdragon 8 Gen2:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Metric</th>
          <th style="text-align: left">Factory Settings</th>
          <th style="text-align: left">Undervolted</th>
          <th style="text-align: left">Improvement</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">Big Core</td>
          <td style="text-align: left">3.2GHz @ 1.05V</td>
          <td style="text-align: left">3.2GHz @ 0.98V (-70mV)</td>
          <td style="text-align: left">~18% Battery Life</td>
      </tr>
      <tr>
          <td style="text-align: left">GPU</td>
          <td style="text-align: left">680MHz @ 0.90V</td>
          <td style="text-align: left">680MHz @ 0.85V (-50mV)</td>
          <td style="text-align: left">2-3°C Cooler</td>
      </tr>
  </tbody>
</table>
<p><strong>Method</strong>: Modify the kernel device tree voltage table or use Franco Kernel Manager for real-time tuning.</p>

<h2 class="relative group">7. Bypassing Thermal Throttling & Charging Mods
    <div id="7-bypassing-thermal-throttling--charging-mods" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#7-bypassing-thermal-throttling--charging-mods" aria-label="Anchor">#</a>
    </span>
    
</h2>

<h3 class="relative group">Thermal Control
    <div id="thermal-control" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#thermal-control" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>OEMs are often too conservative, throttling at 45°C. I increase the threshold to 55°C:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Modify thermal trip points</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="m">55000</span> > /sys/class/thermal/thermal_zone0/trip_point_0_temp</span></span></code></pre></div></div>

<h3 class="relative group">Smart Charging
    <div id="smart-charging" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#smart-charging" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>For devices always plugged in, I limit the charging current and capacity:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Limit charging current to 2A</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="m">2000000</span> > /sys/class/power_supply/battery/constant_charge_current_max
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Stop charging at 80%</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="m">80</span> > /sys/class/power_supply/battery/charge_control_limit</span></span></code></pre></div></div>
<p><strong>Module</strong>: ACC (Advanced Charging Controller).</p>

<h2 class="relative group">8. Termux: The Full Linux Experience
    <div id="8-termux-the-full-linux-experience" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#8-termux-the-full-linux-experience" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>Without root, Termux is isolated. With root, it becomes a complete mobile server:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Install a full Debian distro</span>
</span></span><span class="line"><span class="cl">proot-distro install debian
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Network packet analysis</span>
</span></span><span class="line"><span class="cl">tcpdump -i wlan0 -w capture.pcap
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Flash custom kernels directly</span>
</span></span><span class="line"><span class="cl">dd <span class="k">if</span><span class="o">=</span>/sdcard/custom_boot.img <span class="nv">of</span><span class="o">=</span>/dev/block/by-name/boot</span></span></code></pre></div></div>
<hr>

<h2 class="relative group">Conclusion
    <div id="conclusion" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#conclusion" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>In 2025, rooting is still a necessity for me. It’s about <strong>performance extraction, privacy protection, and functional expansion</strong>. While there are risks like warranty loss or app detection, the journey of tuning the system is where the learning and fun reside.</p>
<p>My strategy: <strong>Keep the primary device conservative, but never stop tinkering with the secondary ones.</strong></p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>How to update InsydeH2O BIOS with pure Linux</title>
      <link>https://blog.ferstar.org/en/posts/update-insydeh2o-bios-linux/</link>
      <pubDate>Sun, 12 Jan 2025 16:58:10 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/en/posts/update-insydeh2o-bios-linux/</guid>
      <description>Vendor BIOS updates require Windows and a large partition; use a pure-Linux extraction and flashing workflow; update BIOS reliably without keeping Windows.</description><content:encoded><![CDATA[<blockquote><p>I am not a native English speaker; this article was translated by AI.</p>
</blockquote><p>As a long-time Linux enthusiast, I usually install a Linux distro as soon as I get a new laptop. That makes the preinstalled Windows almost useless, but I still couldn’t delete it — I needed it to run vendor BIOS updaters. Keeping a ~100GB Windows partition just for the occasional BIOS update is pretty annoying. Sure, something like Windows To Go exists, but that doesn’t really change the situation: you’re still relying on Windows.</p>
<p>Right before the end of 2024, I came across someone mentioning a pure-Linux BIOS update workflow on Twitter:
<a href="https://x.com/felixonmars/status/1876646199207604351"  target="_blank" rel="noreferrer">https://x.com/felixonmars/status/1876646199207604351</a></p>
<p>It was only a few lines, but it lit the tinkering fire in my head: this might work — let’s do it.</p>
<p>All my laptops are Lenovo ThinkBooks. I started with one machine at home (A). Specs:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">OS: Ubuntu oracular 24.10 x86_64
</span></span><span class="line"><span class="cl">Host: 21D0 <span class="o">(</span>ThinkBook <span class="m">14</span> G4+ ARA<span class="o">)</span>
</span></span><span class="line"><span class="cl">Kernel: Linux 6.12.9-bigv
</span></span><span class="line"><span class="cl">Uptime: <span class="m">1</span> day, <span class="m">53</span> mins
</span></span><span class="line"><span class="cl">Packages: <span class="m">2800</span> <span class="o">(</span>dpkg<span class="o">)</span>, <span class="m">7</span> <span class="o">(</span>flatpak<span class="o">)</span>
</span></span><span class="line"><span class="cl">Shell: zsh 5.9
</span></span><span class="line"><span class="cl">Display <span class="o">(</span>AUOC391<span class="o">)</span>: 2880x1800 @ <span class="m">90</span> Hz in 14<span class="s2">"
</span></span></span><span class="line"><span class="cl"><span class="s2">DE: GNOME
</span></span></span><span class="line"><span class="cl"><span class="s2">WM: Mutter (X11)
</span></span></span><span class="line"><span class="cl"><span class="s2">WM Theme: Yaru
</span></span></span><span class="line"><span class="cl"><span class="s2">Theme: Yaru [GTK2/3/4]
</span></span></span><span class="line"><span class="cl"><span class="s2">Icons: Yaru [GTK2/3/4]
</span></span></span><span class="line"><span class="cl"><span class="s2">Font: Cantarell (11pt) [GTK2/3/4]
</span></span></span><span class="line"><span class="cl"><span class="s2">Cursor: DMZ-White (48px)
</span></span></span><span class="line"><span class="cl"><span class="s2">Terminal: tmux 3.4
</span></span></span><span class="line"><span class="cl"><span class="s2">CPU: AMD Ryzen 7 6800H (16) @ 4.79 GHz
</span></span></span><span class="line"><span class="cl"><span class="s2">GPU: AMD Radeon 680M [Integrated]
</span></span></span><span class="line"><span class="cl"><span class="s2">Memory: 12.95 GiB / 29.55 GiB (44%)
</span></span></span><span class="line"><span class="cl"><span class="s2">Swap: 5.25 MiB / 32.00 GiB (0%)
</span></span></span><span class="line"><span class="cl"><span class="s2">Disk (/): 312.72 GiB / 414.32 GiB (75%) - btrfs
</span></span></span><span class="line"><span class="cl"><span class="s2">Local IP (wlan0): 192.168.31.157/24
</span></span></span><span class="line"><span class="cl"><span class="s2">Battery (AP16L5J): 77% [AC Connected]
</span></span></span><span class="line"><span class="cl"><span class="s2">Locale: en_US.UTF-8</span></span></span></code></pre></div></div>
<ol>
<li>Download Framework’s UEFI Shell flashing tool. We only need <code>H2OFFT-Sx64.efi</code>. Save it to <code>/boot/efi</code>: <a href="https://downloads.frame.work/bios/Framework_Laptop_13_13th_Gen_Intel_Core_BIOS__3.05_EFI.zip"  target="_blank" rel="noreferrer">https://downloads.frame.work/bios/Framework_Laptop_13_13th_Gen_Intel_Core_BIOS__3.05_EFI.zip</a></li>
<li>Download UEFI Shell. Save <code>shellx64.efi</code> to <code>/boot/efi</code>: <a href="https://github.com/pbatard/UEFI-Shell/releases/tag/24H2"  target="_blank" rel="noreferrer">https://github.com/pbatard/UEFI-Shell/releases/tag/24H2</a></li>
<li>Download the BIOS updater for your exact laptop model from Lenovo’s driver page. It’s usually named something like <code>j6cn50ww.exe</code>.</li>
<li>Try extracting the <code>exe</code> with <code>7z</code>: <code>7z e j6cn50ww.exe</code>. You may see output like: <code>Comments: This installation was built with Inno Setup.</code></li>
<li>Use <code>innoextract</code> to unpack it: <code>innoextract -e j6cn50ww.exe</code>. You’ll get a new <code>exe</code> — mine was <code>J6CN50WW.exe</code>.</li>
<li>Extract again with <code>7z</code>: <code>7z e J6CN50WW.exe</code>. This time you should get the actual BIOS firmware, <code>WinJ6CN50WW.fd</code>.</li>
<li>Save <code>WinJ6CN50WW.fd</code> to <code>/boot/efi</code>.</li>
<li>Add a UEFI Shell boot entry. Edit <code>/etc/grub.d/40_custom</code>, paste the following, then run <code>sudo update-grub</code>:
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">menuentry <span class="s2">"UEFI Shell"</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    insmod part_gpt
</span></span><span class="line"><span class="cl">    insmod chain
</span></span><span class="line"><span class="cl">    <span class="nb">set</span> <span class="nv">root</span><span class="o">=</span><span class="s1">'(hd0,gpt1)'</span>
</span></span><span class="line"><span class="cl">    chainloader /shellx64.efi
</span></span><span class="line"><span class="cl"><span class="o">}</span></span></span></code></pre></div></div>
</li>
<li>Reboot into UEFI Shell. Type <code>FS0:</code> and press Enter to switch to the EFI partition, then run <code>H2OFFT-Sx64.efi WinJ6CN50WW.fd</code> and press Enter.</li>
<li>You should see the familiar BIOS flashing UI. Wait for it to complete.</li>
</ol>
<p>Finally, I can throw the bundled Windows partition into the trash ✌️</p>
<hr>
<p>Note: For some newer Lenovo models, <code>7z</code> may be able to extract the BIOS firmware directly — usually named <code>xxx.bin</code> — so <code>innoextract</code> is not required. Also, <code>7z</code> isn’t mandatory either. You can use any tool that can unpack an <code>exe</code> (for example <code>bsdtar</code>).</p>
<hr>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">NOTE</span><span class="o">:</span> <span class="nx">I</span> <span class="nx">am</span> <span class="nx">not</span> <span class="nx">responsible</span> <span class="k">for</span> <span class="nx">any</span> <span class="nx">expired</span> <span class="nx">content</span><span class="p">.</span>
</span></span><span class="line"><span class="cl"><span class="nx">Created</span> <span class="nx">at</span><span class="o">:</span> <span class="mi">2025</span><span class="o">-</span><span class="mi">01</span><span class="o">-</span><span class="mi">12</span><span class="nx">T16</span><span class="o">:</span><span class="mi">58</span><span class="o">:</span><span class="mi">10</span><span class="o">+</span><span class="mi">08</span><span class="o">:</span><span class="mi">00</span>
</span></span><span class="line"><span class="cl"><span class="nx">Updated</span> <span class="nx">at</span><span class="o">:</span> <span class="mi">2025</span><span class="o">-</span><span class="mi">01</span><span class="o">-</span><span class="mi">12</span><span class="nx">T17</span><span class="o">:</span><span class="mi">04</span><span class="o">:</span><span class="mi">21</span><span class="o">+</span><span class="mi">08</span><span class="o">:</span><span class="mi">00</span>
</span></span><span class="line"><span class="cl"><span class="nx">Origin</span> <span class="nx">issue</span><span class="o">:</span> <span class="nx">https</span><span class="o">:</span><span class="c1">//github.com/ferstar/blog/issues/83
</span></span></span></code></pre></div></div>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Enhancing Linux Touchpad Gestures: Implementing &#39;Three-Finger Drag&#39;</title>
      <link>https://blog.ferstar.org/en/posts/linux-touchpad-gestures-drag/</link>
      <pubDate>Sun, 29 Jan 2023 02:08:34 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/en/posts/linux-touchpad-gestures-drag/</guid>
      <description>Miss the smooth macOS three-finger drag on Linux? This high-performance Rust-based solution for libinput gestures supports both X11 and Wayland with minimal CPU overhead.</description><content:encoded><![CDATA[<blockquote><p>I am not a native English speaker; this article was translated by AI.</p>
</blockquote><p>After switching to Linux, I’ve been missing the smooth <strong>three-finger drag</strong> experience from macOS. Given that recent Windows laptops have significantly improved touchpad size and responsiveness, I decided it was time to tackle <strong>touchpad gestures</strong> on Linux.</p>
<hr>

<h2 class="relative group">Research and Selection
    <div id="research-and-selection" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#research-and-selection" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>There are basically two approaches to implementation:</p>
<ol>
<li>Parsing <code>libinput debug-events</code> output and using tools like <code>xdotool</code> to send keystrokes or mouse clicks.</li>
<li>Calling the <code>libinput</code> API directly, which offers the best performance.</li>
</ol>
<hr>

<h2 class="relative group">Implementation
    <div id="implementation" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#implementation" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>I chose to fork and merge two Rust projects to create a unified solution. One project handles general <a href="https://github.com/riley-martin/gestures"  target="_blank" rel="noreferrer">gestures</a>, and the other focuses on the <a href="https://github.com/marsqing/libinput-three-finger-drag"  target="_blank" rel="noreferrer">three-finger drag</a> effect at the API level.</p>

<h3 class="relative group">Architecture
    <div id="architecture" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#architecture" aria-label="Anchor">#</a>
    </span>
    
</h3>
<pre class="not-prose mermaid">
graph TD
    A[Touchpad Event] --> B{libinput}
    B --> C[ferstar/gestures <br/>Rust Engine]
    C --> D{Display Server}
    D -- X11 --> E[libxdo API]
    D -- Wayland --> F[ydotool daemon]
    E --> G[Smooth Drag / Key Stroke]
    F --> G
    
    style C fill:#f96,stroke:#333,stroke-width:2px
    style G fill:#4ecdc4,stroke:#333,stroke-width:2px
</pre>

<p>The project is now at <strong>v0.8.1</strong>, with major improvements including:</p>
<ul>
<li><strong>Dual Platform Support</strong>: Works on both X11 and Wayland (auto-detection).</li>
<li><strong>Performance Optimization</strong>:
<ul>
<li>Direct <code>libxdo</code> API calls for X11 (minimum latency).</li>
<li>Optimized <code>ydotool</code> integration for Wayland with 60 FPS throttling.</li>
<li>Thread pooling to prevent PID exhaustion.</li>
<li>Regex and config caching for speed.</li>
</ul>
</li>
</ul>
<hr>

<h2 class="relative group">Performance Metrics
    <div id="performance-metrics" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#performance-metrics" aria-label="Anchor">#</a>
    </span>
    
</h2>
<ol>
<li>
<p><strong>Low CPU Usage</strong></p>
<ul>
<li>Even under intense three-finger dragging, this implementation uses less than 1% CPU.</li>
<li>Competing Python or Ruby implementations often exceed 20%.</li>
</ul>
</li>
<li>
<p><strong>Resource Efficiency</strong></p>
<ul>
<li>Memory usage: < 5MB.</li>
<li>Binary size: < 2MB.</li>
<li>Zero unnecessary dependencies.</li>
</ul>
</li>
</ol>
<hr>

<h2 class="relative group">Installation & Usage
    <div id="installation--usage" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#installation--usage" aria-label="Anchor">#</a>
    </span>
    
</h2>

<h3 class="relative group">Dependencies
    <div id="dependencies" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#dependencies" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p><strong>Ubuntu/Debian:</strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt install libudev-dev libinput-dev libxdo-dev xdotool
</span></span><span class="line"><span class="cl"><span class="c1"># For Wayland</span>
</span></span><span class="line"><span class="cl">sudo apt install ydotool</span></span></code></pre></div></div>
<p><strong>Arch Linux:</strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo pacman -S libinput xdotool
</span></span><span class="line"><span class="cl"><span class="c1"># Wayland</span>
</span></span><span class="line"><span class="cl">yay -S ydotool</span></span></code></pre></div></div>

<h3 class="relative group">Install Binary
    <div id="install-binary" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#install-binary" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p><strong>Option 1: Pre-compiled Binary (Recommended)</strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">wget https://github.com/ferstar/gestures/releases/latest/download/gestures
</span></span><span class="line"><span class="cl">chmod +x gestures
</span></span><span class="line"><span class="cl">sudo mv gestures /usr/local/bin/</span></span></code></pre></div></div>
<p><strong>Option 2: Install via Cargo</strong></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cargo install --git https://github.com/ferstar/gestures.git</span></span></code></pre></div></div>
<hr>

<h2 class="relative group">Configuration
    <div id="configuration" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#configuration" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>The config uses the KDL format. Example:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-kdl" data-lang="kdl"><span class="line"><span class="cl"><span class="c1">// Three-finger drag (X11 & Wayland)
</span></span></span><span class="line"><span class="cl"><span class="nl">gesture</span><span class="w"> </span><span class="s">"drag"</span><span class="w"> </span><span class="s">swipe</span><span class="w"> </span><span class="s">any</span><span class="w"> </span><span class="o">{</span><span class="nl">
</span></span></span><span class="line"><span class="cl"><span class="nl">    fingers</span><span class="w"> </span><span class="m">3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nl">    acceleration</span><span class="w"> </span><span class="mf">1.0</span><span class="w">      </span><span class="c1">// Drag speed
</span></span></span><span class="line"><span class="cl"><span class="nl">    mouse_up_delay</span><span class="w"> </span><span class="m">500</span><span class="w">    </span><span class="c1">// Delay after lifting fingers (ms)
</span></span></span><span class="line"><span class="cl"><span class="o">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">// Four-finger swipe up to switch workspace
</span></span></span><span class="line"><span class="cl"><span class="nl">gesture</span><span class="w"> </span><span class="s">"switch-workspace-up"</span><span class="w"> </span><span class="s">swipe</span><span class="w"> </span><span class="s">up</span><span class="w"> </span><span class="o">{</span><span class="nl">
</span></span></span><span class="line"><span class="cl"><span class="nl">    fingers</span><span class="w"> </span><span class="m">4</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nl">    exec</span><span class="w"> </span><span class="s">"xdotool"</span><span class="w"> </span><span class="s">"key"</span><span class="w"> </span><span class="s">"super+Page_Up"</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">}</span></span></span></code></pre></div></div>
<hr>

<h2 class="relative group">Running the Program
    <div id="running-the-program" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#running-the-program" aria-label="Anchor">#</a>
    </span>
    
</h2>

<h3 class="relative group">Systemd Service
    <div id="systemd-service" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#systemd-service" aria-label="Anchor">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Install service file</span>
</span></span><span class="line"><span class="cl">gestures install-service
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Enable and start</span>
</span></span><span class="line"><span class="cl">systemctl --user <span class="nb">enable</span> --now gestures</span></span></code></pre></div></div>
<hr>

<h2 class="relative group">Common Issues
    <div id="common-issues" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#common-issues" aria-label="Anchor">#</a>
    </span>
    
</h2>

<h3 class="relative group">Permission Denied
    <div id="permission-denied" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#permission-denied" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>You need to add your user to the <code>input</code> group:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo usermod -aG input <span class="nv">$USER</span></span></span></code></pre></div></div>
<hr>

<h2 class="relative group">Links
    <div id="links" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#links" aria-label="Anchor">#</a>
    </span>
    
</h2>
<ul>
<li><strong>GitHub</strong>: <a href="https://github.com/ferstar/gestures"  target="_blank" rel="noreferrer">https://github.com/ferstar/gestures</a></li>
<li><strong>Issues</strong>: <a href="https://github.com/ferstar/gestures/issues"  target="_blank" rel="noreferrer">https://github.com/ferstar/gestures/issues</a></li>
</ul>
<hr>
<p><strong>Enjoy!</strong></p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>AX3600: Upgrading ShellClash to Clash-Meta Core for Hysteria 2 Support</title>
      <link>https://blog.ferstar.org/en/posts/ax3600-clash-meta-upgrade/</link>
      <pubDate>Tue, 24 Jan 2023 01:17:09 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/en/posts/ax3600-clash-meta-upgrade/</guid>
      <description>Solve the storage limitation on Xiaomi AX3600 routers. Learn how to use UPX compression to replace the Clash-Meta core, enabling support for Hysteria 2 and TUIC protocols.</description><content:encoded><![CDATA[<blockquote><p>I am not a native English speaker; this article was translated by AI.</p>
</blockquote><p>The root process and ShellClash installation for this router are well-documented here: <a href="https://qust.me/post/ax3600_shellclash/"  target="_blank" rel="noreferrer">https://qust.me/post/ax3600_shellclash/</a></p>
<p>I won’t repeat those steps. Instead, I’ll focus on how to replace the Clash-Meta core to support modern proxy protocols like Hysteria 2 and TUIC.</p>

<h3 class="relative group">1. Download Clash-Meta
    <div id="1-download-clash-meta" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#1-download-clash-meta" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>Download the appropriate version (arm64): <a href="https://github.com/MetaCubeX/Clash.Meta/releases"  target="_blank" rel="noreferrer">Clash.Meta Releases</a>. I personally use the Alpha version for the latest features.</p>

<h3 class="relative group">2. The Storage Challenge
    <div id="2-the-storage-challenge" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#2-the-storage-challenge" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>Here’s the problem: The unzipped Clash-Meta binary is nearly 20MB, but the AX3600’s root partition usually has less than 8MB left after installing ShellClash.</p>
<p><strong>Solution: UPX Compression</strong>
Download <a href="https://github.com/upx/upx"  target="_blank" rel="noreferrer">UPX</a> and compress the binary:
<code>upx -9 clash</code></p>
<p>This reduces the size to under 7MB, making it easy to fit into the router’s storage.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">root@XiaoQiang:~# du -sh /data/clash/clash
</span></span><span class="line"><span class="cl">6.6M    /data/clash/clash
</span></span><span class="line"><span class="cl">root@XiaoQiang:~# /data/clash/clash -v
</span></span><span class="line"><span class="cl">Clash Meta alpha-096bb8d linux arm64 with go1.19.5 Mon Jan <span class="m">23</span> 06:08:40 UTC <span class="m">2023</span></span></span></code></pre></div></div>

<h3 class="relative group">3. Cleaning Up Logs
    <div id="3-cleaning-up-logs" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#3-cleaning-up-logs" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>The default firmware has a habit of filling up <code>/data/usr/log</code> with junk files, which can cause network lag. It’s best to set up a cron job to clear it regularly.</p>
<p>Add this to your crontab (<code>crontab -l</code>):</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">*/3 * * * * rm -rf /data/usr/log/*  <span class="c1"># Clear junk logs every 3 minutes</span>
</span></span><span class="line"><span class="cl"><span class="m">30</span> <span class="m">3</span> * * 1~7 /data/clash/start.sh restart >/dev/null 2><span class="p">&</span><span class="m">1</span> <span class="c1"># Restart Clash daily at 3:30 AM</span></span></span></code></pre></div></div>

<h3 class="relative group">4. Performance Tip
    <div id="4-performance-tip" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#4-performance-tip" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>Since Hysteria can be CPU-intensive, I recommend limiting the downlink speed (e.g., to 100 Mbps) to keep the router load manageable.</p>
<hr>
<p><em>Created: 2023-01-24</em>
<em>Updated: 2026-01-04</em></p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Cloudflare Tunnel &#43; Brook: A High-Value Acceleration Solution for Slow VPS</title>
      <link>https://blog.ferstar.org/en/posts/cloudflare-argo-tunnel-brook-setup/</link>
      <pubDate>Wed, 16 Mar 2022 23:10:03 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/en/posts/cloudflare-argo-tunnel-brook-setup/</guid>
      <description>Is your VPS line quality poor? Learn how to use Cloudflare Tunnel (formerly Argo Tunnel) and Brook to leverage Cloudflare&#39;s edge network for acceleration and hidden IP protection.</description><content:encoded><![CDATA[<blockquote><p>I am not a native English speaker; this article was translated by AI.</p>
</blockquote><blockquote><p><strong>Note:</strong> Technical audit performed in 2026. While protocols evolve, the underlying logic of Cloudflare’s edge acceleration remains robust.</p>
</blockquote>
<h3 class="relative group">The Principle
    <div id="the-principle" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#the-principle" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>Use <code>Cloudflare Tunnel</code> to map local services to Cloudflare’s edge network. VPS with poor connectivity (like Oracle Free Tier) can gain a significant speed boost through CDN edge acceleration.</p>
<pre class="not-prose mermaid">
graph LR
    User((Client)) -- "WSS Request" --> CF[Cloudflare Edge]
    subgraph "Your Home/VPS"
        CFT[cloudflared] -- "Persistent Tunnel" --> CF
        Brook[Brook WSServer] -- "Local Loopback" --> CFT
    end
    style User fill:#f9f,stroke:#333,stroke-width:2px
    style CF fill:#f96,stroke:#333,stroke-width:2px
    style Brook fill:#4ecdc4,stroke:#333,stroke-width:2px
</pre>


<h3 class="relative group">Setup Guide
    <div id="setup-guide" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#setup-guide" aria-label="Anchor">#</a>
    </span>
    
</h3>
<ol>
<li>
<p><strong>Preparation</strong>:</p>
<ul>
<li><a href="https://one.dash.cloudflare.com/"  target="_blank" rel="noreferrer">Cloudflare Zero Trust Dashboard</a>: Create a Tunnel directly in the dashboard—it’s much easier than managing certificates via CLI.</li>
<li><a href="https://txthinking.github.io/brook/#/brook-wsserver"  target="_blank" rel="noreferrer">Brook wsserver</a>: A very simple and logical proxy tool.</li>
</ul>
</li>
<li>
<p><strong>Start the Server</strong>:
<code>brook wsserver --listen 127.0.0.1:8888 --password your_pass</code></p>
</li>
<li>
<p><strong>Configure the Tunnel</strong>:
In the Cloudflare dashboard, point your domain (Public Hostname) to <code>http://localhost:8888</code>.</p>
</li>
<li>
<p><strong>Crucial Setting (Must Do)</strong>:
<strong>Ensure WebSocket support is enabled</strong> in Cloudflare’s “Network” settings; otherwise, the WSS connection will be terminated immediately.</p>
</li>
<li>
<p><strong>Client Connection</strong>:
Select <strong>WSS</strong> mode on your client. Traffic now flows through the encrypted tunnel.</p>
</li>
</ol>

<h3 class="relative group">Pros and Cons
    <div id="pros-and-cons" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#pros-and-cons" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p><strong>Pros:</strong></p>
<ul>
<li><strong>Stealth</strong>: Your VPS IP is never exposed. Cloudflare handles all traffic, significantly reducing the risk of IP blocking.</li>
<li><strong>Penetration</strong>: Works in IPv6-only or NAT/internal network environments.</li>
<li><strong>Free</strong>: Cloudflare’s free tier for personal users is more than sufficient.</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li><strong>Latency</strong>: The extra hops add latency (typically in the hundreds of milliseconds), making it unsuitable for gaming.</li>
<li><strong>UDP</strong>: Support for non-HTTP UDP traffic is limited; recommended for TCP/WSS scenarios.</li>
</ul>

<h3 class="relative group">Further Reading
    <div id="further-reading" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#further-reading" aria-label="Anchor">#</a>
    </span>
    
</h3>
<ul>
<li><a href="https://github.com/HyNetwork/hysteria"  target="_blank" rel="noreferrer">Hysteria 2</a>: A performance monster for non-CDN scenarios.</li>
<li><a href="https://1.1.1.1/"  target="_blank" rel="noreferrer">Cloudflare WARP</a>: For auxiliary optimization.</li>
</ul>
<hr>
<p><em>Created: 2022-03-16</em>
<em>Updated: 2026-01-04</em></p>
]]></content:encoded>
      
    </item>
    
  </channel>
</rss>
