<?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>Bui Anh Tuan</title><link>https://tuanbui.net/</link><description>Recent content on Bui Anh Tuan</description><generator>Hugo -- gohugo.io</generator><language>en</language><managingEditor>me@tuanbui.net (Bui Anh Tuan)</managingEditor><webMaster>me@tuanbui.net (Bui Anh Tuan)</webMaster><copyright>© 2025 Bui Anh Tuan</copyright><lastBuildDate>Sun, 08 Feb 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tuanbui.net/index.xml" rel="self" type="application/rss+xml"/><item><title>Winter CMS login insecure issue</title><link>https://tuanbui.net/2026/02/08/winter-cms-link-policy/</link><pubDate>Sun, 08 Feb 2026 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2026/02/08/winter-cms-link-policy/</guid><description>&lt;p>I&amp;rsquo;m running Winter CMS behind a reverse proxy. Every time I log in to the backend, the login form shows an insecure connection warning.&lt;/p>
&lt;p>I set &lt;code>APP_URL&lt;/code> to my domain (&lt;code>example.com&lt;/code>), but the issue still remained.&lt;/p>
&lt;p>I wasn&amp;rsquo;t sure if Winter CMS stored the URL somewhere else. After checking the source code, I found this:&lt;/p>
&lt;pre tabindex="0">&lt;code>/*
|--------------------------------------------------------------------------
| Linking policy
|--------------------------------------------------------------------------
|
| Controls how URL links are generated throughout the application.
|
| detect - detect hostname and use the current scheme
| secure - detect hostname and force HTTPS scheme
| insecure - detect hostname and force HTTP scheme
| force - force hostname and scheme using app.url config value
|
| NOTE: force will ensure that the app.url value is used as the host for
| URLs generated through the URL helpers, which might have unintended
| consequences for projects that support multiple hostnames.
|
*/&lt;/code>&lt;/pre>
&lt;p>I changed &lt;code>LINK_POLICY&lt;/code> in the &lt;code>.env&lt;/code> file from &lt;code>detect&lt;/code> to &lt;code>force&lt;/code>, and the insecure connection issue was resolved.&lt;/p></description><content:encoded><![CDATA[<p>I&rsquo;m running Winter CMS behind a reverse proxy. Every time I log in to the backend, the login form shows an insecure connection warning.</p>
<p>I set <code>APP_URL</code> to my domain (<code>example.com</code>), but the issue still remained.</p>
<p>I wasn&rsquo;t sure if Winter CMS stored the URL somewhere else. After checking the source code, I found this:</p>






<pre tabindex="0"><code>/*
|--------------------------------------------------------------------------
| Linking policy
|--------------------------------------------------------------------------
|
| Controls how URL links are generated throughout the application.
|
| detect   - detect hostname and use the current scheme
| secure   - detect hostname and force HTTPS scheme
| insecure - detect hostname and force HTTP scheme
| force    - force hostname and scheme using app.url config value
|
| NOTE: force will ensure that the app.url value is used as the host for
| URLs generated through the URL helpers, which might have unintended
| consequences for projects that support multiple hostnames.
|
*/</code></pre>
<p>I changed <code>LINK_POLICY</code> in the <code>.env</code> file from <code>detect</code> to <code>force</code>, and the insecure connection issue was resolved.</p>
]]></content:encoded></item><item><title>Canon LBP6030 USB Printer on LAN</title><link>https://tuanbui.net/2025/12/25/linux-canon-lbp6030/</link><pubDate>Thu, 25 Dec 2025 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2025/12/25/linux-canon-lbp6030/</guid><description>&lt;p>Canon LBP6030 is a USB-only printer.&lt;br>
I use a Linux machine as a print server to share it on my LAN.&lt;/p>
&lt;p>Driver download:&lt;br>
&lt;a href="https://vn.canon/vi/support/0100595001?model=imageCLASS+LBP6030__+LBP6030B__+LBP6030w">https://vn.canon/vi/support/0100595001?model=imageCLASS+LBP6030__+LBP6030B__+LBP6030w&lt;/a>&lt;/p>
&lt;h3 id="linux-mint">Linux Mint&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1&lt;/span>&lt;span>sudo bash ./install.sh&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;h3 id="ubuntu-server">Ubuntu Server&lt;/h3>
&lt;p>Missing dependency:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1&lt;/span>&lt;span>sudo apt install libcupsimage2t64&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>No GUI → add printer via CUPS web UI.&lt;/p>
&lt;h3 id="notes">Notes&lt;/h3>
&lt;ul>
&lt;li>Driver does &lt;strong>not&lt;/strong> support ARM (cannot use Raspberry Pi)&lt;/li>
&lt;li>Using AMD64 as print server&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Canon LBP6030 is a USB-only printer.<br>
I use a Linux machine as a print server to share it on my LAN.</p>
<p>Driver download:<br>
<a href="https://vn.canon/vi/support/0100595001?model=imageCLASS+LBP6030__+LBP6030B__+LBP6030w">https://vn.canon/vi/support/0100595001?model=imageCLASS+LBP6030__+LBP6030B__+LBP6030w</a></p>
<h3 id="linux-mint">Linux Mint</h3>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>sudo bash ./install.sh</span></span></code></pre></div>
<h3 id="ubuntu-server">Ubuntu Server</h3>
<p>Missing dependency:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>sudo apt install libcupsimage2t64</span></span></code></pre></div>
<p>No GUI → add printer via CUPS web UI.</p>
<h3 id="notes">Notes</h3>
<ul>
<li>Driver does <strong>not</strong> support ARM (cannot use Raspberry Pi)</li>
<li>Using AMD64 as print server</li>
</ul>
]]></content:encoded></item><item><title>Expose Aria2 JSON-RPC over HTTPS/WSS with Caddy</title><link>https://tuanbui.net/2025/12/25/aria2-jsonrpc-https-wss-caddy/</link><pubDate>Thu, 25 Dec 2025 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2025/12/25/aria2-jsonrpc-https-wss-caddy/</guid><description>&lt;p>This note shows how to expose &lt;strong>Aria2 JSON-RPC&lt;/strong> securely over &lt;strong>HTTPS / WSS&lt;/strong> when using &lt;a href="https://github.com/hurlenko/aria2-ariang-docker">aria2-ariang-docker&lt;/a> behind &lt;strong>Caddy&lt;/strong>.&lt;/p>
&lt;h2 id="setup">Setup&lt;/h2>
&lt;h3 id="1-docker-compose">1. Docker Compose&lt;/h3>
&lt;p>Set the RPC port to &lt;code>443&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-env" data-lang="env">&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1&lt;/span>&lt;span>&lt;span style="color:#f5e0dc">ARIA2RPCPORT&lt;/span>&lt;span style="color:#89dceb;font-weight:bold">=&lt;/span>&lt;span style="color:#fab387">443&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;blockquote>
&lt;p>This setup is used &lt;strong>inside a private VPN only&lt;/strong>, so there is &lt;strong>no need to set &lt;code>RPC_SECRET&lt;/code>&lt;/strong>.&lt;/p>&lt;/blockquote>
&lt;h3 id="2-caddy-reverse-proxy">2. Caddy Reverse Proxy&lt;/h3>
&lt;p>Configure Caddy normally for AriaNg:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-caddyfile" data-lang="caddyfile">&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 1&lt;/span>&lt;span>&lt;span style="color:#fab387;font-weight:bold">ariang.example.com&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 2&lt;/span>&lt;span> &lt;span style="color:#cba6f7">reverse_proxy&lt;/span> ariang:&lt;span style="color:#fab387">8080&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 3&lt;/span>&lt;span> &lt;span style="color:#cba6f7">header_up&lt;/span> &lt;span style="color:#a6e3a1">Host&lt;/span> &lt;span style="color:#89b4fa">{host}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 4&lt;/span>&lt;span> &lt;span style="color:#cba6f7">header_up&lt;/span> &lt;span style="color:#a6e3a1">X-Real-IP&lt;/span> &lt;span style="color:#89b4fa">{remote}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 5&lt;/span>&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 6&lt;/span>&lt;span> &lt;span style="color:#cba6f7">transport&lt;/span> &lt;span style="color:#a6e3a1">http&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 7&lt;/span>&lt;span> &lt;span style="color:#cba6f7">read_timeout&lt;/span> &lt;span style="color:#fab387">1h&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 8&lt;/span>&lt;span> &lt;span style="color:#cba6f7">write_timeout&lt;/span> &lt;span style="color:#fab387">1h&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 9&lt;/span>&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">10&lt;/span>&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">11&lt;/span>&lt;span>}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>Caddy automatically handles TLS and WebSocket upgrades.&lt;/p></description><content:encoded><![CDATA[<p>This note shows how to expose <strong>Aria2 JSON-RPC</strong> securely over <strong>HTTPS / WSS</strong> when using <a href="https://github.com/hurlenko/aria2-ariang-docker">aria2-ariang-docker</a> behind <strong>Caddy</strong>.</p>
<h2 id="setup">Setup</h2>
<h3 id="1-docker-compose">1. Docker Compose</h3>
<p>Set the RPC port to <code>443</code>:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-env" data-lang="env"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span><span style="color:#f5e0dc">ARIA2RPCPORT</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#fab387">443</span></span></span></code></pre></div>
<blockquote>
<p>This setup is used <strong>inside a private VPN only</strong>, so there is <strong>no need to set <code>RPC_SECRET</code></strong>.</p></blockquote>
<h3 id="2-caddy-reverse-proxy">2. Caddy Reverse Proxy</h3>
<p>Configure Caddy normally for AriaNg:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-caddyfile" data-lang="caddyfile"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 1</span><span><span style="color:#fab387;font-weight:bold">ariang.example.com</span> {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 2</span><span>    <span style="color:#cba6f7">reverse_proxy</span> ariang:<span style="color:#fab387">8080</span> {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 3</span><span>        <span style="color:#cba6f7">header_up</span> <span style="color:#a6e3a1">Host</span> <span style="color:#89b4fa">{host}</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 4</span><span>        <span style="color:#cba6f7">header_up</span> <span style="color:#a6e3a1">X-Real-IP</span> <span style="color:#89b4fa">{remote}</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 5</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 6</span><span>        <span style="color:#cba6f7">transport</span> <span style="color:#a6e3a1">http</span> {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 7</span><span>            <span style="color:#cba6f7">read_timeout</span> <span style="color:#fab387">1h</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 8</span><span>            <span style="color:#cba6f7">write_timeout</span> <span style="color:#fab387">1h</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 9</span><span>        }
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">10</span><span>    }
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">11</span><span>}</span></span></code></pre></div>
<p>Caddy automatically handles TLS and WebSocket upgrades.</p>
<h3 id="3-ariang-webui-settings">3. AriaNg WebUI Settings</h3>
<p>In <strong>AriaNg Settings → RPC</strong>:</p>
<ul>
<li><strong>Aria2 RPC Address</strong>






<pre tabindex="0"><code>https://ariang.example.com:443/jsonrpc</code></pre>
or






<pre tabindex="0"><code>wss://ariang.example.com:443/jsonrpc</code></pre>
</li>
</ul>
<h2 id="notes">Notes</h2>
<ul>
<li><code>wss://</code> is recommended for better WebSocket stability.</li>
<li>No extra Caddy configuration is required for WebSockets.</li>
<li>Access is restricted to the VPN, so disabling <code>RPC_SECRET</code> is acceptable in this case.</li>
</ul>
]]></content:encoded></item><item><title>Adding Twikoo Comments to My Hugo Website</title><link>https://tuanbui.net/2025/11/21/hugo-twikoo-comments/</link><pubDate>Fri, 21 Nov 2025 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2025/11/21/hugo-twikoo-comments/</guid><description>&lt;blockquote>
&lt;p>&lt;strong>Update (2025-11-22):&lt;/strong> I have since switched from Twikoo to &lt;strong>meh&lt;/strong> (&lt;a href="https://github.com/splitbrain/meh)">https://github.com/splitbrain/meh)&lt;/a>.&lt;br>
This post remains for reference.&lt;/p>&lt;/blockquote>
&lt;h1 id="adding-twikoo-comments-to-my-hugo-website">Adding Twikoo Comments to My Hugo Website&lt;/h1>
&lt;p>This is me trying to add a comment system to my website (this website).&lt;/p>
&lt;p>Hugo has plenty of comment integrations available:&lt;br>
&lt;a href="https://gohugo.io/content-management/comments">https://gohugo.io/content-management/comments&lt;/a>&lt;/p>
&lt;p>Disqus, Comentario, giscus, Isso, Remark42, Talkyard… you name it.&lt;/p>
&lt;p>However, since I already use &lt;strong>Twikoo&lt;/strong> for other services, it makes sense to reuse it here. This post documents how I integrated Twikoo into a Hugo site using the &lt;strong>hugo-simple&lt;/strong> theme.&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p><strong>Update (2025-11-22):</strong> I have since switched from Twikoo to <strong>meh</strong> (<a href="https://github.com/splitbrain/meh)">https://github.com/splitbrain/meh)</a>.<br>
This post remains for reference.</p></blockquote>
<h1 id="adding-twikoo-comments-to-my-hugo-website">Adding Twikoo Comments to My Hugo Website</h1>
<p>This is me trying to add a comment system to my website (this website).</p>
<p>Hugo has plenty of comment integrations available:<br>
<a href="https://gohugo.io/content-management/comments">https://gohugo.io/content-management/comments</a></p>
<p>Disqus, Comentario, giscus, Isso, Remark42, Talkyard… you name it.</p>
<p>However, since I already use <strong>Twikoo</strong> for other services, it makes sense to reuse it here. This post documents how I integrated Twikoo into a Hugo site using the <strong>hugo-simple</strong> theme.</p>
<hr>
<h2 id="1-override-hugos-singlehtml">1. Override Hugo’s <code>single.html</code></h2>
<p>Because I only want comments on blog posts, I created:</p>






<pre tabindex="0"><code>layouts/blog/single.html</code></pre>
<p>I copied it from the theme’s <code>layouts/single.html</code> and inserted the comment partial right after the <code>&lt;content&gt;</code> block:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-html" data-lang="html"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>&lt;<span style="color:#cba6f7">content</span>&gt;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span>  {{ .Content }}
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">3</span><span>&lt;/<span style="color:#cba6f7">content</span>&gt;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">4</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">5</span><span>{{ partial &#34;comments.html&#34; . }}</span></span></code></pre></div>
<hr>
<h2 id="2-create-the-commentshtml-partial">2. Create the <code>comments.html</code> partial</h2>
<p>Next, I added a central comment selector at:</p>






<pre tabindex="0"><code>layouts/_partials/comments.html</code></pre>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-html" data-lang="html"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 1</span><span>{{/* determine provider: page-level overrides site-level */}}
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 2</span><span>{{ $provider := .Params.comments | default .Site.Params.comments.provider }}
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 3</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 4</span><span>{{ if eq $provider &#34;twikoo&#34; }}
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 5</span><span>  {{ partial &#34;comments/twikoo.html&#34; . }}
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 6</span><span>{{ end }}
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 7</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 8</span><span>{{ if eq $provider &#34;giscus&#34; }}
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 9</span><span>  {{ partial &#34;comments/giscus.html&#34; . }}
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">10</span><span>{{ end }}
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">11</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">12</span><span>{{/* more systems can be added here */}}</span></span></code></pre></div>
<hr>
<h2 id="3-add-the-twikoo-partial">3. Add the Twikoo partial</h2>
<p>I created the Twikoo implementation at:</p>






<pre tabindex="0"><code>layouts/_partials/comments/twikoo.html</code></pre>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-html" data-lang="html"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 1</span><span>&lt;<span style="color:#cba6f7">div</span> <span style="color:#89b4fa">id</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#a6e3a1">&#34;twikoo&#34;</span>&gt;&lt;/<span style="color:#cba6f7">div</span>&gt;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 2</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 3</span><span>&lt;<span style="color:#cba6f7">script</span> <span style="color:#89b4fa">src</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#a6e3a1">&#34;https://cdn.jsdelivr.net/npm/twikoo@1.6.44/dist/twikoo.min.js&#34;</span>&gt;&lt;/<span style="color:#cba6f7">script</span>&gt;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 4</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 5</span><span>&lt;<span style="color:#cba6f7">script</span>&gt;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 6</span><span>twikoo.init({
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 7</span><span>  envId<span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#a6e3a1">&#34;{{ .Site.Params.twikoo.endpoint }}&#34;</span>,
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 8</span><span>  el<span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#a6e3a1">&#34;#twikoo&#34;</span>,
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 9</span><span>  path<span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#a6e3a1">&#34;{{ .Permalink }}&#34;</span>,
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">10</span><span>  lang<span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#a6e3a1">&#34;{{ .Site.Params.twikoo.language | default &#34;</span>en<span style="color:#a6e3a1">&#34; }}&#34;</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">11</span><span>});
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">12</span><span>&lt;/<span style="color:#cba6f7">script</span>&gt;</span></span></code></pre></div>
<hr>
<h2 id="4-configure-twikoo-in-hugotoml">4. Configure Twikoo in <code>hugo.toml</code></h2>
<p>Finally, I added the following settings:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-toml" data-lang="toml"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>[params.comments]
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span>provider = <span style="color:#a6e3a1">&#34;twikoo&#34;</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">3</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">4</span><span>[params.twikoo]
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">5</span><span>endpoint = <span style="color:#a6e3a1">&#34;https://your.twikoo.api&#34;</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">6</span><span>language = <span style="color:#a6e3a1">&#34;en&#34;</span></span></span></code></pre></div>
<hr>
<h2 id="thats-it">That’s it</h2>
<p>After these changes, Twikoo successfully loads at the bottom of each blog post.</p>
<p>If I ever want to switch to another system (e.g., giscus), I can just update <code>provider</code>:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-toml" data-lang="toml"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>[params.comments]
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span>provider = <span style="color:#a6e3a1">&#34;giscus&#34;</span></span></span></code></pre></div>
]]></content:encoded></item><item><title>Secure Private Domains with Caddy + Tailscale</title><link>https://tuanbui.net/2025/11/18/caddy-tailscale-cloudflare/</link><pubDate>Tue, 18 Nov 2025 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2025/11/18/caddy-tailscale-cloudflare/</guid><description>&lt;p>Imagine you want to access &lt;strong>secret.example.com&lt;/strong>, but only you — or a few trusted people — should be able to reach it.&lt;br>
Traditional methods like IP allowlists or blocklists quickly become unreliable:&lt;/p>
&lt;ul>
&lt;li>People use multiple devices&lt;/li>
&lt;li>They move between locations&lt;/li>
&lt;li>ISPs constantly change IP addresses&lt;/li>
&lt;li>Mobile networks rotate IPs all the time&lt;/li>
&lt;/ul>
&lt;p>In 2025, IP filtering is simply not enough.&lt;/p>
&lt;hr>
&lt;h2 id="why-tailscale-solves-this">Why Tailscale Solves This&lt;/h2>
&lt;p>Caddy can serve your site directly over the &lt;strong>Tailscale network interface&lt;/strong>, meaning the service is only reachable through your &lt;strong>private 100.x.x.x Tailscale IP&lt;/strong>.&lt;/p></description><content:encoded><![CDATA[<p>Imagine you want to access <strong>secret.example.com</strong>, but only you — or a few trusted people — should be able to reach it.<br>
Traditional methods like IP allowlists or blocklists quickly become unreliable:</p>
<ul>
<li>People use multiple devices</li>
<li>They move between locations</li>
<li>ISPs constantly change IP addresses</li>
<li>Mobile networks rotate IPs all the time</li>
</ul>
<p>In 2025, IP filtering is simply not enough.</p>
<hr>
<h2 id="why-tailscale-solves-this">Why Tailscale Solves This</h2>
<p>Caddy can serve your site directly over the <strong>Tailscale network interface</strong>, meaning the service is only reachable through your <strong>private 100.x.x.x Tailscale IP</strong>.</p>
<p>You gain:</p>
<ul>
<li>
<p><strong>Private access by default</strong><br>
Only devices on your Tailscale network can connect.</p>
</li>
<li>
<p><strong>Reliable HTTPS certificates</strong><br>
Caddy still obtains valid certificates via <strong>DNS-01</strong>, even for private-only services.</p>
</li>
<li>
<p><strong>Simplified security</strong><br>
Tailscale handles authentication and access control — no messy allowlists or firewall rules.</p>
</li>
</ul>
<p>To begin, point your DNS <strong>A / AAAA</strong> record to the Tailscale IP of your server.</p>
<hr>
<h2 id="why-dns-01-is-required">Why DNS-01 Is Required</h2>
<p>HTTP-01 and TLS-ALPN-01 challenges won&rsquo;t work because the service is not publicly reachable.<br>
The ACME server simply cannot connect.</p>
<p><strong>DNS-01</strong> solves this by verifying domain ownership through DNS API calls instead of network access.</p>
<p>If you use Cloudflare, the <strong>caddy-cloudflare</strong> build makes this process seamless:</p>
<blockquote>
<p><a href="https://github.com/CaddyBuilds/caddy-cloudflare">https://github.com/CaddyBuilds/caddy-cloudflare</a></p></blockquote>
<hr>
<h2 id="step-1--add-cloudflare-api-token-to-docker-compose">Step 1 — Add Cloudflare API Token to Docker Compose</h2>
<p>Your <code>compose.yaml</code> should include:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span><span style="color:#cba6f7">environment</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span>  - CLOUDFLARE_API_TOKEN=your_cloudflare_api_token</span></span></code></pre></div>
<p>The token must have:<br>
<strong>Zone → DNS → Edit</strong></p>
<hr>
<h2 id="step-2--configure-caddy-to-use-cloudflare-dns">Step 2 — Configure Caddy to Use Cloudflare DNS</h2>
<p>Add this to the global block of your <code>Caddyfile</code>:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-caddy" data-lang="caddy"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>{<span style="color:#6c7086;font-style:italic">
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span><span style="color:#6c7086;font-style:italic">  # Use Cloudflare for all ACME DNS challenges
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">3</span><span><span style="color:#6c7086;font-style:italic"></span>  <span style="color:#cba6f7">acme_dns</span> <span style="color:#a6e3a1">cloudflare</span> <span style="color:#89b4fa">{env.CLOUDFLARE_API_TOKEN}</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">4</span><span>}</span></span></code></pre></div>
<p>This tells Caddy to always use Cloudflare’s API for certificate validation.</p>
<hr>
<h2 id="done--your-private-https-domain">Done — Your Private HTTPS Domain</h2>
<p>You can now visit:</p>






<pre tabindex="0"><code>https://secret.example.com</code></pre>
<p>The site is:</p>
<ul>
<li>Fully private (Tailscale-only)</li>
<li>Fully encrypted (valid HTTPS)</li>
<li>Fully managed (automatic renewals via DNS-01)</li>
</ul>
<p>No public exposure.<br>
No complicated firewall rules.<br>
No unstable IP allowlists.</p>
<p>Just clean, modern, private infrastructure.</p>
<hr>
]]></content:encoded></item><item><title>Hot-Reloading Caddy in Docker Without Restarting the Container</title><link>https://tuanbui.net/2025/11/17/caddy-reload-docker/</link><pubDate>Mon, 17 Nov 2025 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2025/11/17/caddy-reload-docker/</guid><description>&lt;p>I’ve been running a bunch of services in Docker for a while, and Caddy is my reverse proxy of choice. It’s simple, fast, and the automatic TLS is fantastic. But there’s one thing that annoyed me for a long time:&lt;/p>
&lt;p>Every time I changed the Caddyfile, I had to &lt;strong>restart the entire Caddy container&lt;/strong>.&lt;/p>
&lt;p>It’s only a few seconds of downtime, but when Caddy is sitting in front of many services, those seconds hurt — logs go red, monitors fire alerts, and of course I hit refresh like a caveman wondering why everything’s dead.&lt;/p></description><content:encoded><![CDATA[<p>I’ve been running a bunch of services in Docker for a while, and Caddy is my reverse proxy of choice. It’s simple, fast, and the automatic TLS is fantastic. But there’s one thing that annoyed me for a long time:</p>
<p>Every time I changed the Caddyfile, I had to <strong>restart the entire Caddy container</strong>.</p>
<p>It’s only a few seconds of downtime, but when Caddy is sitting in front of many services, those seconds hurt — logs go red, monitors fire alerts, and of course I hit refresh like a caveman wondering why everything’s dead.</p>
<p>Then I finally learned that Caddy can actually <strong>reload its configuration live</strong>.<br>
No restart. No interruption. Zero downtime.</p>
<p>This post is me writing down how I got it working in Docker — partly to help others, partly so that future-me doesn’t forget.</p>
<hr>
<h2 id="why-restarting-is-not-ideal">Why restarting is not ideal</h2>
<p>Restarting Caddy seems harmless, but in practice it:</p>
<ul>
<li>briefly stops forwarding traffic</li>
<li>closes existing connections</li>
<li>delays TLS initialization</li>
<li>makes other containers look offline</li>
<li>annoys anyone using your services at that moment (including yourself)</li>
</ul>
<p>The good news: you don’t have to restart anything.</p>
<hr>
<h2 id="caddy-can-hot-reload">Caddy can hot-reload</h2>
<p>Caddy has two built-in ways to reload its configuration:</p>
<ul>
<li>via the <strong>CLI</strong> (<code>caddy reload</code>)</li>
<li>via the <strong>Admin API</strong> (<code>:2019</code>)</li>
</ul>
<p>Both validate the config and apply it without restarting the process.</p>
<hr>
<h2 id="my-setup">My setup</h2>
<p>My Caddyfile lives next to my <code>docker-compose.yml</code>, and I mount it into the container like this:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span><span style="color:#cba6f7">volumes</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span>  - ./Caddyfile:/etc/caddy/Caddyfile
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">3</span><span>  - ./config:/config
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">4</span><span>  - ./data:/data</span></span></code></pre></div>
<p>So Caddy always reads <code>/etc/caddy/Caddyfile</code>.</p>
<hr>
<h2 id="the-easiest-way-docker-exec">The easiest way: <code>docker exec</code></h2>
<p>This is the method I use most:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>docker <span style="color:#89dceb">exec</span> &lt;container_name&gt; caddy reload <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span><span style="color:#89b4fa"></span>    --config /etc/caddy/Caddyfile <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">3</span><span><span style="color:#89b4fa"></span>    --adapter caddyfile</span></span></code></pre></div>
<p>Replace <code>&lt;container_name&gt;</code> with your actual Caddy container name.</p>
<p>Example:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>docker <span style="color:#89dceb">exec</span> caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile</span></span></code></pre></div>
<p>And that’s it — Caddy reloads instantly with <strong>zero downtime</strong>.</p>
<hr>
<h2 id="reloading-via-the-admin-api-optional">Reloading via the Admin API (optional)</h2>
<p>If you prefer using <code>curl</code>, you can expose the admin API safely by binding it only to localhost:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span><span style="color:#cba6f7">ports</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span>  - <span style="color:#a6e3a1">&#34;127.0.0.1:2019:2019&#34;</span></span></span></code></pre></div>
<p>Then enable it in your Caddyfile:</p>






<pre tabindex="0"><code>{
    admin :2019
}</code></pre>
<p>Now you can run:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>curl -X POST <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span><span style="color:#89b4fa"></span>    -H <span style="color:#a6e3a1">&#34;Content-Type: text/caddyfile&#34;</span> <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">3</span><span><span style="color:#89b4fa"></span>    --data-binary @Caddyfile <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">4</span><span><span style="color:#89b4fa"></span>    http://localhost:2019/load</span></span></code></pre></div>
<p>This is useful if you want automation or external tools to reload Caddy.</p>
<p>But personally, I still prefer:</p>






<pre tabindex="0"><code>docker exec caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile</code></pre>
<hr>
<h2 id="my-tiny-reload-script">My tiny reload script</h2>
<p>Because why not:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span><span style="color:#6c7086;font-style:italic">#!/bin/bash
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span><span style="color:#6c7086;font-style:italic"></span>docker <span style="color:#89dceb">exec</span> caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile</span></span></code></pre></div>
<p>Now I just type:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>bash ./reload.sh</span></span></code></pre></div>
<p>and Caddy refreshes instantly.</p>
<hr>
<h2 id="final-thoughts">Final thoughts</h2>
<p>Caddy has been great for my Docker setup, and hot-reloading the config makes it even smoother. No restarts, no interruptions, and no more “why is everything down again?” moments.</p>
<p>If you’re running Caddy in Docker, switch to the <code>caddy reload</code> workflow.<br>
Your future self (and your uptime graphs) will thank you.</p>
]]></content:encoded></item><item><title>🔒 Leak-Free DNS on Linux Mint 22.2 Cinnamon (SmartDNS + systemd-resolved + NextDNS)</title><link>https://tuanbui.net/2025/11/09/leak-free-dns-linux-mint/</link><pubDate>Sun, 09 Nov 2025 12:00:00 +0700</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2025/11/09/leak-free-dns-linux-mint/</guid><description>&lt;p>By default, Linux Mint 22.2 Cinnamon uses NetworkManager and your router’s or ISP’s DNS, which may leak queries in plaintext.&lt;br>
This guide shows how to route &lt;strong>all DNS requests through SmartDNS&lt;/strong>, with &lt;strong>NextDNS (DoH3 / DoH2)&lt;/strong> as the encrypted upstream — while keeping &lt;code>systemd-resolved&lt;/code> for caching and stub resolution.&lt;/p>
&lt;hr>
&lt;h2 id="-architecture">🧩 Architecture&lt;/h2>
&lt;pre tabindex="0">&lt;code>Applications → 127.0.0.53:53 (systemd-resolved)
→ 127.0.0.1:65353 (SmartDNS)
→ NextDNS (DoH3 / DoH2, encrypted)&lt;/code>&lt;/pre>
&lt;ul>
&lt;li>&lt;strong>systemd-resolved&lt;/strong> — local stub resolver (&lt;code>127.0.0.53&lt;/code>)&lt;/li>
&lt;li>&lt;strong>SmartDNS&lt;/strong> — local cache and resolver (&lt;code>127.0.0.1:65353&lt;/code>)&lt;/li>
&lt;li>&lt;strong>NextDNS&lt;/strong> — encrypted upstream resolver&lt;/li>
&lt;li>&lt;strong>NetworkManager&lt;/strong> — DNS disabled and unmanaged&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="-configuration">⚙️ Configuration&lt;/h2>
&lt;h3 id="1-disable-networkmanager-dns-control">1️⃣ Disable NetworkManager DNS control&lt;/h3>
&lt;p>Create:&lt;/p></description><content:encoded><![CDATA[<p>By default, Linux Mint 22.2 Cinnamon uses NetworkManager and your router’s or ISP’s DNS, which may leak queries in plaintext.<br>
This guide shows how to route <strong>all DNS requests through SmartDNS</strong>, with <strong>NextDNS (DoH3 / DoH2)</strong> as the encrypted upstream — while keeping <code>systemd-resolved</code> for caching and stub resolution.</p>
<hr>
<h2 id="-architecture">🧩 Architecture</h2>






<pre tabindex="0"><code>Applications → 127.0.0.53:53 (systemd-resolved)
               → 127.0.0.1:65353 (SmartDNS)
               → NextDNS (DoH3 / DoH2, encrypted)</code></pre>
<ul>
<li><strong>systemd-resolved</strong> — local stub resolver (<code>127.0.0.53</code>)</li>
<li><strong>SmartDNS</strong> — local cache and resolver (<code>127.0.0.1:65353</code>)</li>
<li><strong>NextDNS</strong> — encrypted upstream resolver</li>
<li><strong>NetworkManager</strong> — DNS disabled and unmanaged</li>
</ul>
<hr>
<h2 id="-configuration">⚙️ Configuration</h2>
<h3 id="1-disable-networkmanager-dns-control">1️⃣ Disable NetworkManager DNS control</h3>
<p>Create:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>sudo mkdir -p /etc/NetworkManager/conf.d
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span>sudo nano /etc/NetworkManager/conf.d/dns.conf</span></span></code></pre></div>
<p>Paste:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span><span style="color:#cba6f7">[main]</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span><span style="color:#89b4fa">dns</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#a6e3a1">none</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">3</span><span><span style="color:#89b4fa">rc-manager</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#a6e3a1">unmanaged</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">4</span><span><span style="color:#89b4fa">systemd-resolved</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#a6e3a1">false</span></span></span></code></pre></div>
<p>Restart NetworkManager:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>sudo systemctl restart NetworkManager</span></span></code></pre></div>
<h4 id="-explanation">💡 Explanation</h4>
<table>
  <thead>
      <tr>
          <th>Directive</th>
          <th>Purpose</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>dns=none</code></td>
          <td>Prevents NetworkManager from managing DNS. It will not modify <code>/etc/resolv.conf</code> or forward DNS to other plugins.</td>
      </tr>
      <tr>
          <td><code>rc-manager=unmanaged</code></td>
          <td>Tells NetworkManager <strong>not to touch <code>/etc/resolv.conf</code></strong> — no symlinks, no updates from DHCP or VPN.</td>
      </tr>
      <tr>
          <td><code>systemd-resolved=false</code></td>
          <td>Stops NetworkManager from pushing per-connection DNS information to <code>systemd-resolved</code>. You’ll manage it manually.</td>
      </tr>
  </tbody>
</table>
<p>Together, these settings disable NetworkManager’s internal DNS management and hand full DNS control to <strong>systemd-resolved</strong> and <strong>SmartDNS</strong>.<br>
This ensures <strong>no unwanted DNS overrides or leaks</strong> from DHCP and provides a stable, predictable DNS setup even when switching networks.</p>
<blockquote>
<p>📝 <strong>Note:</strong><br>
The directive <code>dns=none</code> already <em>implies</em> <code>rc-manager=unmanaged</code>, meaning NetworkManager will not modify <code>/etc/resolv.conf</code>.<br>
It’s included here explicitly for clarity and to ensure consistent behavior across distributions like Linux Mint.</p></blockquote>
<hr>
<h3 id="2-point-systemd-resolved-to-smartdns">2️⃣ Point systemd-resolved to SmartDNS</h3>
<p>Create a drop-in:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>sudo mkdir -p /etc/systemd/resolved.conf.d
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span>sudo nano /etc/systemd/resolved.conf.d/smartdns.conf</span></span></code></pre></div>
<p>Contents:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span><span style="color:#cba6f7">[Resolve]</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span><span style="color:#89b4fa">DNS</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#a6e3a1">127.0.0.1:65353</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">3</span><span><span style="color:#89b4fa">FallbackDNS</span><span style="color:#89dceb;font-weight:bold">=</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">4</span><span><span style="color:#89b4fa">DNSStubListener</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#a6e3a1">yes</span></span></span></code></pre></div>
<h4 id="-explanation-1">💡 Explanation</h4>
<table>
  <thead>
      <tr>
          <th>Directive</th>
          <th>Purpose</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>DNS=127.0.0.1:65353</code></td>
          <td>Forwards all DNS requests from systemd-resolved to SmartDNS.</td>
      </tr>
      <tr>
          <td><code>FallbackDNS=</code></td>
          <td>Overrides systemd-resolved’s built-in default fallback list (<code>1.1.1.1</code>, <code>8.8.8.8</code>, <code>9.9.9.9</code>), ensuring <strong>no bypass</strong> to public resolvers if SmartDNS becomes unreachable.</td>
      </tr>
      <tr>
          <td><code>DNSStubListener=yes</code></td>
          <td>Keeps the local stub active at <code>127.0.0.53</code>, so applications and libraries continue to function normally.</td>
      </tr>
  </tbody>
</table>
<p>This configuration ensures <strong>all DNS queries are handled exclusively by SmartDNS</strong>, with <strong>no fallback to default or ISP resolvers</strong>, creating a fully leak-proof chain.</p>
<p>Apply:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>sudo systemctl restart systemd-resolved
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span>sudo resolvectl flush-caches</span></span></code></pre></div>
<p>Verify:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>resolvectl status</span></span></code></pre></div>
<p>Expected:</p>






<pre tabindex="0"><code>Current DNS Server: 127.0.0.1:65353
DNS Servers: 127.0.0.1:65353</code></pre>
<hr>
<h3 id="3-configure-smartdns">3️⃣ Configure SmartDNS</h3>
<p>Edit:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>sudo nano /etc/smartdns/smartdns.conf</span></span></code></pre></div>
<p>Minimal configuration:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 1</span><span><span style="color:#89b4fa">bind 127.0.0.1:65353</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 2</span><span><span style="color:#89b4fa">bind-tcp 127.0.0.1:65353</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 3</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 4</span><span><span style="color:#89b4fa">server-tls 1.1.1.1:853 -bootstrap-dns -host-name cloudflare-dns.com</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 5</span><span><span style="color:#89b4fa">server-tls 8.8.8.8:853 -bootstrap-dns -host-name dns.google</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 6</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 7</span><span><span style="color:#89b4fa">server-h3 h3://dns.nextdns.io/&lt;YOUR_NEXTDNS_ID&gt; -host-name dns.nextdns.io -tls-host-verify dns.nextdns.io</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 8</span><span><span style="color:#89b4fa">server-https https://dns.nextdns.io/&lt;YOUR_NEXTDNS_ID&gt; -host-name dns.nextdns.io -tls-host-verify dns.nextdns.io</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 9</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">10</span><span><span style="color:#89b4fa">ca-file /etc/ssl/certs/ca-certificates.crt</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">11</span><span><span style="color:#89b4fa">ca-path /etc/ssl/certs</span></span></span></code></pre></div>
<p>Replace <code>&lt;YOUR_NEXTDNS_ID&gt;</code> with your unique ID from<br>
🔗 <a href="https://my.nextdns.io/setup">https://my.nextdns.io/setup</a></p>
<p>Restart SmartDNS:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>sudo systemctl restart smartdns</span></span></code></pre></div>
<hr>
<h2 id="-verification">🧪 Verification</h2>
<h3 id="check-the-resolver-chain">Check the resolver chain</h3>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>resolvectl status</span></span></code></pre></div>
<p>✅ DNS = 127.0.0.1:65353</p>
<h3 id="confirm-nextdns">Confirm NextDNS</h3>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>dig @127.0.0.1 -p <span style="color:#fab387">65353</span> whoami.nextdns.io TXT +short</span></span></code></pre></div>
<p>Expected:</p>






<pre tabindex="0"><code>&#34;Your NextDNS ID&#34;</code></pre>
<h3 id="verify-no-leaks">Verify no leaks</h3>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>sudo tcpdump -ni any port <span style="color:#fab387">53</span></span></span></code></pre></div>
<p>✅ No outbound DNS traffic to your router or ISP.</p>
<hr>
<h2 id="-result">✅ Result</h2>
<p>✔ SmartDNS listens only on <code>127.0.0.1:65353</code><br>
✔ systemd-resolved forwards all DNS → SmartDNS<br>
✔ NextDNS encrypts and filters DNS traffic<br>
✔ No DHCP or ISP DNS overwrites<br>
✔ Works flawlessly on Linux Mint 22.2 Cinnamon</p>
<hr>
<h2 id="-summary">🧾 Summary</h2>
<table>
  <thead>
      <tr>
          <th>Component</th>
          <th>Address</th>
          <th>Role</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>systemd-resolved</td>
          <td>127.0.0.53</td>
          <td>Stub resolver</td>
      </tr>
      <tr>
          <td>SmartDNS</td>
          <td>127.0.0.1:65353</td>
          <td>Local resolver</td>
      </tr>
      <tr>
          <td>NextDNS</td>
          <td>DoH3 / DoH2</td>
          <td>Encrypted upstream</td>
      </tr>
      <tr>
          <td>NetworkManager</td>
          <td>—</td>
          <td><code>dns=none</code>, <code>rc-manager=unmanaged</code>, <code>systemd-resolved=false</code></td>
      </tr>
  </tbody>
</table>
<hr>
<p><strong>Enjoy your private, encrypted, and leak-free DNS setup on Linux Mint 22.2 Cinnamon.</strong></p>
]]></content:encoded></item><item><title>How I Normalize Filenames on Windows Using MSYS2 Bash and CLI Tools</title><link>https://tuanbui.net/2025/11/09/normalize-filenames-on-windows/</link><pubDate>Sun, 09 Nov 2025 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2025/11/09/normalize-filenames-on-windows/</guid><description>A clean, Windows-native workflow for renaming and sanitizing files using MSYS2 Bash, Alacritty, and cross-platform CLI tools like notox and date2name.</description><content:encoded><![CDATA[<blockquote>
<p>⚙️ <strong>Note (2025 Update)</strong><br>
I’ve since developed my own tool — <strong><a href="https://github.com/johndo100/cleanfy">cleanfy</a></strong> — a fully cross-platform CLI utility for cleaning and normalizing filenames.<br>
It works on Windows, macOS, and Linux natively, so this older MSYS2-based workflow is no longer necessary.</p>
<p>Still, if you prefer using <strong>MSYS2 Bash</strong> with tools like <strong>notox</strong> and <strong>date2name</strong>, the guide below remains a solid reference.</p></blockquote>
<hr>
<p>If you handle messy filenames every day — photos, invoices, USB dumps — you know the pain:<br>
spaces, accents, duplicate <code>#copy</code> files, and random dates everywhere.</p>
<p>This is my <strong>Windows-native</strong> workflow for cleaning and normalizing filenames fast:</p>
<blockquote>
<p><strong>Alacritty</strong> + <strong>MSYS2 Bash</strong> + <strong>cross-platform CLI tools</strong> (<code>notox</code>, <code>date2name</code>, <code>pipx</code>)</p></blockquote>
<p>No WSL. No emulation.<br>
Everything runs <em>natively</em> on Windows, but with the power and convenience of Unix commands.</p>
<hr>
<h2 id="-setup-overview">⚙️ Setup Overview</h2>
<p>I already have:</p>
<ol>
<li><strong>MSYS2</strong> installed (<a href="https://www.msys2.org">msys2.org</a>) — provides a native Unix-like shell on Windows</li>
<li><strong>Scoop</strong> installed (<a href="https://scoop.sh">scoop.sh</a>) — a lightweight package manager for Windows</li>
<li><strong>pipx</strong> installed <strong>via Scoop</strong> — manages and runs Python-based CLI tools (requires Python from <a href="https://www.python.org/downloads/">python.org</a>)</li>
<li><strong>Rust</strong> installed via <a href="https://rustup.rs">rustup.rs</a> — compiles and installs Rust-based CLI tools</li>
<li><strong>Alacritty</strong> (<a href="https://alacritty.org">alacritty.org</a>) — a lightweight, GPU-accelerated terminal that works beautifully with MSYS2 Bash</li>
</ol>
<p>Then I simply:</p>
<ol>
<li>Set <strong>MSYS2 Bash</strong> as the <strong>default shell in Alacritty</strong> via <code>bash.exe</code></li>
<li>Adjusted my Alacritty configuration so it launches directly into the UCRT64 environment.</li>
</ol>
<p>Alacritty automatically adds an <strong>“Open Alacritty here”</strong> option to the Windows right-click menu, so I can open any folder in the same Bash environment instantly.</p>
<p>Here’s the relevant part of my <code>alacritty.toml</code>:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-toml" data-lang="toml"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>[terminal.shell]
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span>program = <span style="color:#a6e3a1">&#34;C:\\msys64\\usr\\bin\\bash.exe&#34;</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">3</span><span>args = [<span style="color:#a6e3a1">&#34;--login&#34;</span>, <span style="color:#a6e3a1">&#34;-i&#34;</span>]
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">4</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">5</span><span>[env]
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">6</span><span>MSYSTEM = <span style="color:#a6e3a1">&#34;UCRT64&#34;</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">7</span><span>CHERE_INVOKING = <span style="color:#a6e3a1">&#34;1&#34;</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">8</span><span>LANG = <span style="color:#a6e3a1">&#34;en_US.UTF-8&#34;</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">9</span><span>LC_ALL = <span style="color:#a6e3a1">&#34;en_US.UTF-8&#34;</span></span></span></code></pre></div>
<p>This ensures Alacritty opens MSYS2 Bash directly, keeps the working directory when launched via the right-click menu, and uses UTF-8 everywhere for consistent filenames and Unicode output.</p>
<hr>
<h2 id="-1-make-msys2-see-your-windows-tools">🧩 1. Make MSYS2 See Your Windows Tools</h2>
<p>By default, MSYS2 doesn’t automatically detect CLI tools installed on Windows (for example, via <code>pipx</code>, <code>cargo</code>, or <code>scoop</code>).<br>
To bridge them, add their install paths manually to your Bash config.</p>
<p>Open your <strong>MSYS2 Bash inside Alacritty</strong>, then edit:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>vim ~/.bashrc</span></span></code></pre></div>
<p>Append this at the end:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span><span style="color:#6c7086;font-style:italic"># Access Windows-native binaries from MSYS2</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span><span style="color:#89dceb">export</span> <span style="color:#f5e0dc">PATH</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#a6e3a1">&#34;</span><span style="color:#f5e0dc">$PATH</span><span style="color:#a6e3a1">:/c/Users/USERNAME/.local/bin&#34;</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">3</span><span><span style="color:#89dceb">export</span> <span style="color:#f5e0dc">PATH</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#a6e3a1">&#34;</span><span style="color:#f5e0dc">$PATH</span><span style="color:#a6e3a1">:/c/Users/USERNAME/.cargo/bin&#34;</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">4</span><span><span style="color:#89dceb">export</span> <span style="color:#f5e0dc">PATH</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#a6e3a1">&#34;</span><span style="color:#f5e0dc">$PATH</span><span style="color:#a6e3a1">:/c/Users/USERNAME/scoop/shims&#34;</span></span></span></code></pre></div>
<p>Reload your shell:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span><span style="color:#89dceb">source</span> ~/.bashrc</span></span></code></pre></div>
<p>Now MSYS2 can run tools installed by <strong>pipx</strong>, <strong>Rust</strong>, and <strong>Scoop</strong> directly:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>pipx --version
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span>cargo --version</span></span></code></pre></div>
<p>If those commands respond normally, your environment is ready.</p>
<hr>
<h2 id="-2-install-notox-rust">🧽 2. Install notox (Rust)</h2>
<p><a href="https://github.com/Its-Just-Nans/notox"><code>notox</code></a> cleans filenames — removes accents, replaces spaces, and strips unsafe characters.</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>cargo install notox</span></span></code></pre></div>
<p>🗂 The binary is installed to:</p>






<pre tabindex="0"><code>C:\Users\USERNAME\.cargo\bin\notox.exe</code></pre>
<p>You can test it immediately in MSYS2 Bash:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>notox --help</span></span></code></pre></div>
<hr>
<h2 id="-3-install-date2name-python-via-pipx">🗓️ 3. Install date2name (Python via pipx)</h2>
<p><a href="https://github.com/novoid/date2name"><code>date2name</code></a> renames files based on EXIF metadata or last-modified dates.</p>
<p>Since <strong><code>pipx</code></strong> is installed via Scoop, it automatically links its executables under:</p>






<pre tabindex="0"><code>C:\Users\USERNAME\.local\bin</code></pre>
<p>So installing is simple:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>pipx install date2name</span></span></code></pre></div>
<p>Verify it’s available:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>date2name --help</span></span></code></pre></div>
<hr>
<h2 id="-4-the-workflow">🔁 4. The Workflow</h2>
<p>Once everything’s in place:</p>
<ol>
<li>Right-click any folder → “<strong>Open Alacritty here</strong>”</li>
<li>Run:</li>
</ol>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>notox -r .
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span>date2name -r .</span></span></code></pre></div>
<p>That’s it — clean, normalized filenames in seconds.</p>
<hr>
<h2 id="-example">📦 Example</h2>
<p><strong>Before:</strong></p>






<pre tabindex="0"><code>Ảnh chụp (2) #Copy.JPG
Thử nghiệm 03.2024 - final.docx</code></pre>
<p><strong>After:</strong></p>






<pre tabindex="0"><code>2024-03-05_Anh_chup_2_Copy.JPG
2024-03-05_Thu_nghiem_03_2024_final.docx</code></pre>
<p>✔️ Safe<br>
✔️ Sortable<br>
✔️ Cross-platform ready</p>
<hr>
<h2 id="-why-this-setup-works">🧭 Why This Setup Works</h2>
<p><strong>System overview:</strong><br>
<code>Windows → Alacritty → MSYS2 Bash → CLI tools (notox, date2name)</code></p>
<table>
  <thead>
      <tr>
          <th>Component</th>
          <th>Role</th>
          <th>Benefit</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Alacritty + MSYS2 Bash</strong></td>
          <td>Shell</td>
          <td>Fast startup, GPU-accelerated, right-click integration</td>
      </tr>
      <tr>
          <td><strong>notox (Rust tool)</strong></td>
          <td>Filename normalizer</td>
          <td>Blazing fast, Unicode-aware</td>
      </tr>
      <tr>
          <td><strong>date2name (Python tool)</strong></td>
          <td>Date-based renamer</td>
          <td>Smart EXIF/timestamp parsing</td>
      </tr>
      <tr>
          <td><strong>PATH bridge</strong></td>
          <td>Integration</td>
          <td>Lets MSYS2 see Windows-installed tools</td>
      </tr>
  </tbody>
</table>
<hr>
<h3 id="-why-powershell-fails-and-msys2-works">⚠️ Why PowerShell Fails (and MSYS2 Works)</h3>
<p><code>date2name</code> and <code>notox</code> don’t run reliably in <strong>PowerShell</strong> because it’s not a POSIX shell.<br>
They expect Unix-style path handling and argument behavior that PowerShell doesn’t provide.</p>
<p>If you try a command like:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-powershell" data-lang="powershell"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span><span style="color:#89dceb">PS </span>C:\Users\USERNAME\Temp&gt; date2name.<span style="color:#fab387">exe</span> .\dada\</span></span></code></pre></div>
<p>you’ll likely see an error such as:</p>






<pre tabindex="0"><code>FileNotFoundError: [WinError 3] The system cannot find the path specified: &#39;&#39;</code></pre>
<p>That happens because PowerShell passes <code>.\dada\</code> as a Windows-style path, while the script’s internal logic expects POSIX-style behavior.</p>
<p><strong>MSYS2 Bash</strong> fixes this automatically — it converts <code>/c/...</code> paths to real Windows locations and provides the POSIX environment these tools expect.<br>
So if you hit errors like this in PowerShell, just rerun the same command in <strong>Alacritty’s MSYS2 Bash</strong> — it’ll work perfectly there.</p>
<hr>
<h3 id="-pro-tips">💡 Pro Tips</h3>
<ul>
<li>Use <code>--dry-run</code> before committing changes.</li>
<li>You can chain both commands with <code>&amp;&amp;</code>:






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>notox -r . <span style="color:#89dceb;font-weight:bold">&amp;&amp;</span> date2name -r .</span></span></code></pre></div>
</li>
<li>Right-click any folder → <em>Open Alacritty here</em> → run cleanup instantly.</li>
</ul>
<hr>
<h3 id="-references">🔗 References</h3>
<ul>
<li><a href="https://github.com/johndo100/cleanfy">Cleanfy</a></li>
<li><a href="https://www.msys2.org">MSYS2</a></li>
<li><a href="https://alacritty.org">Alacritty</a></li>
<li><a href="https://scoop.sh">Scoop</a></li>
<li><a href="https://github.com/Its-Just-Nans/notox">notox on GitHub</a></li>
<li><a href="https://github.com/novoid/date2name">date2name on GitHub</a></li>
<li><a href="https://pypa.github.io/pipx/">pipx</a></li>
</ul>
<hr>
<p><strong>In short:</strong><br>
Use Alacritty with MSYS2 Bash as your compatibility layer on Windows.<br>
Install your CLI tools once — <code>notox</code>, <code>date2name</code>, or anything provided by <strong>Scoop</strong>, <strong>pipx</strong>, <strong>Cargo</strong>, or <strong>MSYS2</strong>.<br>
Open any folder in Bash via Alacritty, run your cleanup commands, and get perfectly normalized filenames in seconds.<br>
If PowerShell throws path errors or nothing happens, switch to MSYS2 Bash — it’s built for Unix-style tools.</p>
<hr>
<p><strong>And that’s why I don’t use PowerShell.</strong><br>
Alacritty with MSYS2 Bash just works — paths, UTF-8, globs, and all.</p>
]]></content:encoded></item><item><title>Hệ thống DNS tôi đang dùng – AdGuard Home + NPMplus</title><link>https://tuanbui.net/2025/11/06/npm-adguard-dns-doh-dot-doq/</link><pubDate>Thu, 06 Nov 2025 00:00:00 +0700</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2025/11/06/npm-adguard-dns-doh-dot-doq/</guid><description>&lt;p>Dạo gần đây tôi làm lại toàn bộ hệ thống DNS ở nhà và văn phòng.&lt;br>
Tôi muốn một hệ thống:&lt;/p>
&lt;ul>
&lt;li>sạch và nhanh&lt;/li>
&lt;li>lọc quảng cáo ngay ở tầng DNS&lt;/li>
&lt;li>hỗ trợ DoH, DoT và DoQ&lt;/li>
&lt;li>không mở nhiều cổng&lt;/li>
&lt;li>dễ bảo trì, dễ nâng cấp&lt;/li>
&lt;/ul>
&lt;p>Cuối cùng tôi chọn mô hình:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>NPMplus&lt;/strong>: HTTPS, reverse proxy, DoH&lt;/li>
&lt;li>&lt;strong>AdGuard Home&lt;/strong>: DNS, DoT, DoQ&lt;/li>
&lt;li>Hai docker-compose tách biệt, cùng dùng network &lt;code>reverse_proxy&lt;/code>&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="-1-tạo-network-chung">🌐 1. Tạo network chung&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1&lt;/span>&lt;span>docker network create reverse_proxy&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;hr>
&lt;h2 id="-2-npmplus--cổng-https-cho-toàn-hệ-thống">🧩 2. NPMplus – cổng HTTPS cho toàn hệ thống&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 1&lt;/span>&lt;span>&lt;span style="color:#cba6f7">services&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 2&lt;/span>&lt;span> &lt;span style="color:#cba6f7">npmplus&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 3&lt;/span>&lt;span> &lt;span style="color:#cba6f7">container_name&lt;/span>: npmplus
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 4&lt;/span>&lt;span> &lt;span style="color:#cba6f7">image&lt;/span>: docker.io/zoeyvid/npmplus:latest
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 5&lt;/span>&lt;span> &lt;span style="color:#cba6f7">restart&lt;/span>: unless-stopped
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 6&lt;/span>&lt;span> &lt;span style="color:#cba6f7">ports&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 7&lt;/span>&lt;span> - &lt;span style="color:#fab387">80&lt;/span>:&lt;span style="color:#fab387">80&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 8&lt;/span>&lt;span> - &lt;span style="color:#fab387">443&lt;/span>:&lt;span style="color:#fab387">443&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 9&lt;/span>&lt;span> &lt;span style="color:#cba6f7">volumes&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">10&lt;/span>&lt;span> - ./npmplus-data:/data
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">11&lt;/span>&lt;span> - ./npmplus-ssl:/etc/letsencrypt
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">12&lt;/span>&lt;span> &lt;span style="color:#cba6f7">networks&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">13&lt;/span>&lt;span> - reverse_proxy
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">14&lt;/span>&lt;span> &lt;span style="color:#cba6f7">environment&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">15&lt;/span>&lt;span> - TZ=Asia/Ho_Chi_Minh
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">16&lt;/span>&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">17&lt;/span>&lt;span>&lt;span style="color:#cba6f7">networks&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">18&lt;/span>&lt;span> &lt;span style="color:#cba6f7">reverse_proxy&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">19&lt;/span>&lt;span> &lt;span style="color:#cba6f7">external&lt;/span>: &lt;span style="color:#fab387">true&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>NPM xử lý DoH, dashboard AdGuard, chứng chỉ Let’s Encrypt (cho DoH) và reverse proxy.&lt;/p></description><content:encoded><![CDATA[<p>Dạo gần đây tôi làm lại toàn bộ hệ thống DNS ở nhà và văn phòng.<br>
Tôi muốn một hệ thống:</p>
<ul>
<li>sạch và nhanh</li>
<li>lọc quảng cáo ngay ở tầng DNS</li>
<li>hỗ trợ DoH, DoT và DoQ</li>
<li>không mở nhiều cổng</li>
<li>dễ bảo trì, dễ nâng cấp</li>
</ul>
<p>Cuối cùng tôi chọn mô hình:</p>
<ul>
<li><strong>NPMplus</strong>: HTTPS, reverse proxy, DoH</li>
<li><strong>AdGuard Home</strong>: DNS, DoT, DoQ</li>
<li>Hai docker-compose tách biệt, cùng dùng network <code>reverse_proxy</code></li>
</ul>
<hr>
<h2 id="-1-tạo-network-chung">🌐 1. Tạo network chung</h2>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>docker network create reverse_proxy</span></span></code></pre></div>
<hr>
<h2 id="-2-npmplus--cổng-https-cho-toàn-hệ-thống">🧩 2. NPMplus – cổng HTTPS cho toàn hệ thống</h2>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 1</span><span><span style="color:#cba6f7">services</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 2</span><span>  <span style="color:#cba6f7">npmplus</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 3</span><span>    <span style="color:#cba6f7">container_name</span>: npmplus
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 4</span><span>    <span style="color:#cba6f7">image</span>: docker.io/zoeyvid/npmplus:latest
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 5</span><span>    <span style="color:#cba6f7">restart</span>: unless-stopped
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 6</span><span>    <span style="color:#cba6f7">ports</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 7</span><span>      - <span style="color:#fab387">80</span>:<span style="color:#fab387">80</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 8</span><span>      - <span style="color:#fab387">443</span>:<span style="color:#fab387">443</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 9</span><span>    <span style="color:#cba6f7">volumes</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">10</span><span>      - ./npmplus-data:/data
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">11</span><span>      - ./npmplus-ssl:/etc/letsencrypt
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">12</span><span>    <span style="color:#cba6f7">networks</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">13</span><span>      - reverse_proxy
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">14</span><span>    <span style="color:#cba6f7">environment</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">15</span><span>      - TZ=Asia/Ho_Chi_Minh
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">16</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">17</span><span><span style="color:#cba6f7">networks</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">18</span><span>  <span style="color:#cba6f7">reverse_proxy</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">19</span><span>    <span style="color:#cba6f7">external</span>: <span style="color:#fab387">true</span></span></span></code></pre></div>
<p>NPM xử lý DoH, dashboard AdGuard, chứng chỉ Let’s Encrypt (cho DoH) và reverse proxy.</p>
<hr>
<h2 id="-3-adguard-home--dns--dotdoq">🛡️ 3. AdGuard Home – DNS + DoT/DoQ</h2>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 1</span><span><span style="color:#cba6f7">services</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 2</span><span>  <span style="color:#cba6f7">adguardhome</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 3</span><span>    <span style="color:#cba6f7">container_name</span>: adguardhome
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 4</span><span>    <span style="color:#cba6f7">image</span>: adguard/adguardhome:latest
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 5</span><span>    <span style="color:#cba6f7">restart</span>: unless-stopped
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 6</span><span>    <span style="color:#cba6f7">ports</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 7</span><span>      - <span style="color:#fab387">53</span>:<span style="color:#fab387">53</span>/tcp
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 8</span><span>      - <span style="color:#fab387">53</span>:<span style="color:#fab387">53</span>/udp
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 9</span><span>      - <span style="color:#fab387">853</span>:<span style="color:#fab387">853</span>/tcp
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">10</span><span>      - <span style="color:#fab387">853</span>:<span style="color:#fab387">853</span>/udp
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">11</span><span>    <span style="color:#cba6f7">volumes</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">12</span><span>      - ./workdir:/opt/adguardhome/work
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">13</span><span>      - ./confdir:/opt/adguardhome/conf
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">14</span><span>      - /opt/certbot/live/dns.example.com:/opt/adguardhome/certs:ro
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">15</span><span>    <span style="color:#cba6f7">networks</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">16</span><span>      - reverse_proxy
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">17</span><span>    <span style="color:#cba6f7">environment</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">18</span><span>      - TZ=Asia/Ho_Chi_Minh
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">19</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">20</span><span><span style="color:#cba6f7">networks</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">21</span><span>  <span style="color:#cba6f7">reverse_proxy</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">22</span><span>    <span style="color:#cba6f7">external</span>: <span style="color:#fab387">true</span></span></span></code></pre></div>
<p>Dashboard chạy nội bộ port 8080 và chỉ NPM truy cập.</p>
<hr>
<h2 id="-4-cấp-chứng-chỉ-tls-cho-dotdoq-bằng-certbot">🔐 4. Cấp chứng chỉ TLS cho DoT/DoQ bằng Certbot</h2>
<p>Wildcard không bao gồm root domain, nên tôi xin hai SAN:</p>
<ul>
<li><code>dns.example.com</code></li>
<li><code>*.dns.example.com</code></li>
</ul>
<h3 id="41-container-certbot">4.1 Container Certbot</h3>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span><span style="color:#cba6f7">services</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span>  <span style="color:#cba6f7">certbot</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">3</span><span>    <span style="color:#cba6f7">container_name</span>: certbot
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">4</span><span>    <span style="color:#cba6f7">image</span>: certbot/certbot:latest
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">5</span><span>    <span style="color:#cba6f7">restart</span>: unless-stopped
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">6</span><span>    <span style="color:#cba6f7">volumes</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">7</span><span>      - /opt/certbot:/etc/letsencrypt
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">8</span><span>    <span style="color:#cba6f7">command</span>: sleep infinity</span></span></code></pre></div>
<h3 id="42-xin-chứng-chỉ-san-dns-01-tự-động">4.2 Xin chứng chỉ SAN (DNS-01 tự động)</h3>
<p>Tôi dùng DNS-01 tự động để Certbot tự thêm TXT record qua API của DNS provider
(ví dụ Cloudflare → certbot-dns-cloudflare, Porkbun → certbot-dns-porkbun, …).</p>
<p>Ví dụ với Cloudflare:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>docker <span style="color:#89dceb">exec</span> -it certbot bash</span></span></code></pre></div>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>certbot certonly <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span><span style="color:#89b4fa"></span>  --dns-cloudflare <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">3</span><span><span style="color:#89b4fa"></span>  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">4</span><span><span style="color:#89b4fa"></span>  -d dns.example.com <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">5</span><span><span style="color:#89b4fa"></span>  -d <span style="color:#a6e3a1">&#39;*.dns.example.com&#39;</span></span></span></code></pre></div>
<p>Certbot tự tạo TXT → tự xác thực → tự ra cert → không phải thao tác thủ công.</p>
<p>Cert (trên container) nằm tại:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>/etc/letsencrypt/live/dns.example.com/</span></span></code></pre></div>
<p>Cert (trên host) nằm tại:</p>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>/opt/certbot/live/dns.example.com/</span></span></code></pre></div>
<h3 id="43-mount-cert-vào-adguard">4.3 Mount cert vào AdGuard</h3>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span><span style="color:#cba6f7">volumes</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">2</span><span>  - /opt/certbot/live/dns.example.com:/opt/adguardhome/certs:ro</span></span></code></pre></div>
<h3 id="44-cấu-hình-http--tls-trong-adguard">4.4 Cấu hình HTTP / TLS trong AdGuard</h3>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 1</span><span><span style="color:#cba6f7">http</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 2</span><span>  <span style="color:#cba6f7">address</span>: <span style="color:#fab387">0.0.0.0</span>:<span style="color:#fab387">8080</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 3</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 4</span><span><span style="color:#cba6f7">dns</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 5</span><span>  <span style="color:#cba6f7">bind_hosts</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 6</span><span>    - <span style="color:#fab387">0.0.0.0</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 7</span><span>  <span style="color:#cba6f7">port</span>: <span style="color:#fab387">53</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 8</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 9</span><span><span style="color:#cba6f7">tls</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">10</span><span>  <span style="color:#cba6f7">enabled</span>: <span style="color:#fab387">true</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">11</span><span>  <span style="color:#cba6f7">certificate_path</span>: /opt/adguardhome/certs/fullchain.pem
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">12</span><span>  <span style="color:#cba6f7">private_key_path</span>: /opt/adguardhome/certs/privkey.pem
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">13</span><span>  <span style="color:#cba6f7">port_dns_over_tls</span>: <span style="color:#fab387">853</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">14</span><span>  <span style="color:#cba6f7">port_dns_over_quic</span>: <span style="color:#fab387">853</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">15</span><span>  <span style="color:#cba6f7">port_https</span>: <span style="color:#fab387">0</span></span></span></code></pre></div>
<h3 id="45-gia-hạn-chứng-chỉ">4.5 Gia hạn chứng chỉ</h3>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span><span style="color:#fab387">0</span> */12 * * * docker <span style="color:#89dceb">exec</span> certbot certbot renew</span></span></code></pre></div>
<hr>
<h2 id="-5-reverse-proxy-cho-doh--dashboard-trong-npm">🔁 5. Reverse proxy cho DoH + Dashboard trong NPM</h2>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nginx" data-lang="nginx"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 1</span><span><span style="color:#cba6f7">location</span> ~ <span style="color:#94e2d5">^/dns-query(/.*)?$</span> {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 2</span><span>    <span style="color:#94e2d5">set</span> <span style="color:#f5e0dc">$clientid</span> <span style="color:#a6e3a1">&#34;&#34;</span>;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 3</span><span>    <span style="color:#94e2d5">if</span> <span style="color:#a6e3a1">(</span><span style="color:#f5e0dc">$1</span> <span style="color:#a6e3a1">!=</span> <span style="color:#a6e3a1">&#34;&#34;)</span> { <span style="color:#94e2d5">set</span> <span style="color:#f5e0dc">$clientid</span> <span style="color:#f5e0dc">$1</span>; }
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 4</span><span>    <span style="color:#94e2d5">proxy_pass</span> <span style="color:#a6e3a1">http://adguardhome:8080/dns-query</span>;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 5</span><span>    <span style="color:#94e2d5">proxy_set_header</span> <span style="color:#a6e3a1">X-Client-ID</span> <span style="color:#f5e0dc">$clientid</span>;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 6</span><span>    <span style="color:#94e2d5">proxy_set_header</span> <span style="color:#a6e3a1">X-Real-IP</span>  <span style="color:#f5e0dc">$remote_addr</span>;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 7</span><span>}
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 8</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 9</span><span><span style="color:#cba6f7">location</span> <span style="color:#a6e3a1">/</span> {
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">10</span><span>    <span style="color:#94e2d5">proxy_pass</span> <span style="color:#a6e3a1">http://adguardhome:8080</span>;
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">11</span><span>}</span></span></code></pre></div>
<p>DoH: <code>https://dns.example.com/dns-query/my-device</code><br>
Dashboard: <code>https://dns.example.com/</code></p>
<hr>
<h2 id="-6-định-danh-thiết-bị-trong-adguard">🧬 6. Định danh thiết bị trong AdGuard</h2>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 1</span><span><span style="color:#cba6f7">clients</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 2</span><span>  <span style="color:#cba6f7">persistent</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 3</span><span>    - <span style="color:#cba6f7">name</span>: Windows YogaDNS (Hanoi)
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 4</span><span>      <span style="color:#cba6f7">ids</span>: [<span style="color:#fab387">100.68.0.11</span>, hn-p1]
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 5</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 6</span><span>    - <span style="color:#cba6f7">name</span>: iPhone15
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 7</span><span>      <span style="color:#cba6f7">ids</span>: [my-ip15]
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 8</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c"> 9</span><span>    - <span style="color:#cba6f7">name</span>: Android ZTE
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">10</span><span>      <span style="color:#cba6f7">ids</span>: [my-zte]
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">11</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">12</span><span>    - <span style="color:#cba6f7">name</span>: Laptop Dell3558
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">13</span><span>      <span style="color:#cba6f7">ids</span>: [my-d58]</span></span></code></pre></div>
<hr>
<h2 id="-7-cách-tôi-cấu-hình-dns-trên-từng-thiết-bị">📱 7. Cách tôi cấu hình DNS trên từng thiết bị</h2>
<table>
  <thead>
      <tr>
          <th>Thiết bị</th>
          <th>Giao thức</th>
          <th>Endpoint</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Windows</td>
          <td>Plain DNS</td>
          <td><code>udp://192.168.2.2</code> hoặc <code>udp://100.68.x.x</code></td>
      </tr>
      <tr>
          <td>iPhone</td>
          <td>DoH</td>
          <td><code>https://dns.example.com/dns-query/my-ip15</code></td>
      </tr>
      <tr>
          <td>Android</td>
          <td>DoH</td>
          <td><code>https://dns.example.com/dns-query/my-zte</code></td>
      </tr>
      <tr>
          <td>Linux</td>
          <td>DoQ</td>
          <td><code>quic://my-d58.dns.example.com:853</code></td>
      </tr>
  </tbody>
</table>
<p>LAN/Tailscale → Plain DNS<br>
Internet → DoH/DoQ</p>
<hr>
<h2 id="-8-kiểm-tra-nhanh">🧪 8. Kiểm tra nhanh</h2>






<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f849c">1</span><span>doggo example.com @udp://192.168.2.2 --time</span></span></code></pre></div>
<hr>
<h2 id="-kết-luận">🎯 Kết luận</h2>
<ul>
<li>Stack tách biệt, dễ nâng cấp</li>
<li>Không rối port</li>
<li>Dashboard chạy sau HTTPS</li>
<li>Đầy đủ DoH / DoT / DoQ</li>
<li>Chứng chỉ chia đúng vai trò</li>
<li>Hoạt động tốt nhiều site</li>
</ul>
<p>Dựng một lần và gần như không phải đụng vào nữa.</p>
]]></content:encoded></item><item><title>Hướng dẫn gọi API của UpSnap từ máy tính, iPhone và Android</title><link>https://tuanbui.net/2025/10/25/upsnap-api/</link><pubDate>Sat, 25 Oct 2025 20:00:00 +0700</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2025/10/25/upsnap-api/</guid><description>Tìm hiểu cách gọi API của UpSnap từ máy tính, iPhone và Android bằng HTTPie hoặc ứng dụng di động. Điều khiển bật, tắt, khởi động lại máy tính từ xa qua Wake-on-LAN chỉ với vài thao tác.</description><content:encoded><![CDATA[<p><strong>UpSnap</strong> là ứng dụng mã nguồn mở giúp bạn điều khiển máy tính trong mạng nội bộ bằng Wake-on-LAN (WOL).<br>
Chỉ với vài thao tác, bạn có thể bật, tắt hoặc khởi động lại máy tính từ xa.<br>
Bài viết này hướng dẫn cách <strong>gọi API của UpSnap</strong> trên <strong>máy tính</strong>, <strong>iPhone</strong> và <strong>Android</strong>,<br>
sử dụng <strong>HTTPie Desktop</strong>, <strong>HTTPie CLI</strong>, hoặc <strong>ứng dụng gửi API tương ứng trên di động</strong>.</p>
<blockquote>
<p>💡 <strong>Lưu ý:</strong></p>
<ul>
<li>Trong bài viết, địa chỉ mặc định là <strong><code>http://localhost:8090</code></strong> (chạy thử trên máy cá nhân).</li>
<li>Nếu triển khai <strong>UpSnap</strong> trong mạng LAN, hãy thay <code>localhost</code> bằng <strong>địa chỉ IP thực tế</strong>, ví dụ <code>http://192.168.1.100:8090</code>.</li>
<li>Khi điều khiển từ xa, bạn có thể sử dụng <strong>VPN</strong> hoặc <strong>reverse proxy</strong> (Nginx, Caddy, Cloudflare Tunnel, v.v.) và thay <code>localhost</code> bằng <strong>tên miền</strong> của bạn.</li>
</ul></blockquote>
<hr>
<h2 id="-mục-lục">🧭 Mục lục</h2>
<ol>
<li><a href="#1-gi%E1%BB%9Bi-thi%E1%BB%87u-v%E1%BB%81-upsnap-api">Giới thiệu về UpSnap API</a></li>
<li><a href="#2-t%E1%BA%A1o-ng%C6%B0%E1%BB%9Di-d%C3%B9ng-m%E1%BB%9Bi">Tạo người dùng mới</a></li>
<li><a href="#3-ph%C3%A2n-quy%E1%BB%81n-ng%C6%B0%E1%BB%9Di-d%C3%B9ng">Phân quyền người dùng</a></li>
<li><a href="#4-%C4%91%C4%83ng-nh%E1%BA%ADp-v%C3%A0-%C4%91i%E1%BB%81u-khi%E1%BB%83n-thi%E1%BA%BFt-b%E1%BB%8B-qua-api">Đăng nhập và điều khiển thiết bị qua API</a></li>
<li><a href="#5-t%C3%B3m-t%E1%BA%AFt-nhanh-tldr">Tóm tắt nhanh (TL;DR)</a></li>
<li><a href="#6-t%C3%A0i-li%E1%BB%87u-tham-kh%E1%BA%A3o">Tài liệu tham khảo</a></li>
</ol>
<hr>
<h2 id="1-giới-thiệu-về-upsnap-api">1. Giới thiệu về UpSnap API</h2>
<p>UpSnap cung cấp <strong>REST API</strong> theo chuẩn HTTP, sử dụng dữ liệu JSON.<br>
Thông qua API, bạn có thể:</p>
<ul>
<li>Đăng nhập và nhận mã xác thực (JWT)</li>
<li>Liệt kê danh sách thiết bị</li>
<li>Bật, tắt hoặc khởi động lại máy tính</li>
<li>Gửi lệnh cho nhiều thiết bị cùng lúc</li>
</ul>
<p><img src="upsnap-api-flow.svg" alt="Sơ đồ luồng hoạt động của UpSnap API – Client gửi POST để đăng nhập và GET để bật, tắt hoặc khởi động lại máy qua UpSnap Server"></p>
<p><em>Sơ đồ minh họa quá trình <strong>Client → UpSnap Server → Máy đích (Wake-on-LAN)</strong>.<br>
Người dùng gửi POST để đăng nhập và GET để điều khiển thiết bị qua API của UpSnap.</em></p>
<hr>
<h2 id="2-tạo-người-dùng-mới">2. Tạo người dùng mới</h2>
<p>Sau khi cài đặt, UpSnap mặc định chỉ có <strong>tài khoản quản trị (superuser)</strong>.<br>
Để sử dụng API an toàn hơn, bạn nên tạo <strong>người dùng thông thường</strong> có quyền hạn giới hạn.</p>
<h3 id="cách-tạo-người-dùng-trong-giao-diện-quản-trị">Cách tạo người dùng trong giao diện quản trị</h3>
<ol>
<li>Mở trình duyệt và truy cập:






<pre tabindex="0"><code>http://localhost:8090</code></pre>
</li>
<li>Đăng nhập bằng tài khoản <strong>quản trị viên (superuser)</strong></li>
<li>Ở góc trên bên trái, chọn <strong>Users (Người dùng)</strong></li>
<li>Bên dưới danh sách, bạn sẽ thấy <strong>biểu mẫu “Create new user (Tạo người dùng mới)”</strong></li>
<li>Nhập thông tin:
<ul>
<li><strong>Email:</strong> <code>nguoidung1@example.com</code></li>
<li><strong>Mật khẩu:</strong> <code>matkhaucuatoi</code></li>
</ul>
</li>
<li>Nhấn <strong>Add (Thêm)</strong> để tạo tài khoản.</li>
</ol>
<blockquote>
<p>⚠️ Không nên dùng tài khoản superuser cho thao tác điều khiển hàng ngày.<br>
Hãy tạo người dùng riêng cho từng người để đảm bảo an toàn và dễ quản lý.</p></blockquote>
<hr>
<h2 id="3-phân-quyền-người-dùng">3. Phân quyền người dùng</h2>
<p>Bạn có thể giới hạn quyền thao tác của từng người dùng để tăng cường bảo mật.<br>
Trong giao diện <strong>Users</strong>, chọn người dùng cần chỉnh, sau đó vào phần <strong>Device permissions</strong> để bật hoặc tắt các quyền:</p>
<ul>
<li><strong>Read</strong> – Xem thông tin thiết bị</li>
<li><strong>Update</strong> – Thay đổi cấu hình thiết bị</li>
<li><strong>Delete</strong> – Xóa thiết bị</li>
<li><strong>Power</strong> – Gửi lệnh bật, tắt, khởi động lại</li>
</ul>
<p>👉 Nếu chỉ cần điều khiển từ xa, bạn nên <strong>chỉ bật quyền Power</strong>.</p>
<hr>
<h2 id="4-đăng-nhập-và-điều-khiển-thiết-bị-qua-api">4. Đăng nhập và điều khiển thiết bị qua API</h2>
<p>Để điều khiển thiết bị qua API của UpSnap, trước hết bạn cần <strong>đăng nhập để lấy mã xác thực (JWT token)</strong>.<br>
Sau đó, dùng mã này trong tiêu đề (HTTP header) của các yêu cầu (HTTP request) để <strong>bật, tắt hoặc khởi động lại máy tính từ xa</strong>.</p>
<hr>
<h3 id="-hiểu-về-phương-thức-get-và-post-khi-gọi-api">🔍 Hiểu về phương thức GET và POST khi gọi API</h3>
<p>Khi gửi yêu cầu đến API, có hai phương thức (HTTP request method) chính bạn cần nắm rõ:</p>
<ul>
<li><strong>POST</strong> – Dùng để <strong>gửi dữ liệu</strong> đến máy chủ, ví dụ: đăng nhập để nhận token.</li>
<li><strong>GET</strong> – Dùng để <strong>lấy dữ liệu hoặc kích hoạt hành động</strong>, ví dụ: gửi lệnh bật/tắt thiết bị.</li>
</ul>
<p>Hầu hết các công cụ gửi API (như HTTPie, cURL, Postman, hoặc ứng dụng di động)<br>
đều cho phép bạn chọn phương thức và thêm tiêu đề khi gửi yêu cầu.</p>
<hr>
<h3 id="-đăng-nhập-và-lấy-mã-xác-thực">🔐 Đăng nhập và lấy mã xác thực</h3>
<table>
  <thead>
      <tr>
          <th>Loại tài khoản</th>
          <th>Đường dẫn đăng nhập</th>
          <th>Mô tả</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Người dùng thường</strong></td>
          <td><code>/api/collections/users/auth-with-password</code></td>
          <td>Dành cho tài khoản thông thường</td>
      </tr>
      <tr>
          <td><strong>Quản trị viên (superuser)</strong></td>
          <td><code>/api/collections/_superusers/auth-with-password</code></td>
          <td>Dành cho người quản trị hệ thống</td>
      </tr>
  </tbody>
</table>
<p>Chúng ta sử dụng người dùng thường để đăng nhập (sử dụng POST):</p>






<pre tabindex="0"><code>POST http://localhost:8090/api/collections/users/auth-with-password
Content-Type: application/json

{
  &#34;identity&#34;: &#34;nguoidung1&#34;,
  &#34;password&#34;: &#34;matkhaucuatoi&#34;
}</code></pre>
<p>Kết quả phản hồi:</p>






<pre tabindex="0"><code>{
  &#34;token&#34;: &#34;eyJhbGciOi...&#34;,
  &#34;record&#34;: { &#34;id&#34;: &#34;abcd1234&#34;, &#34;email&#34;: &#34;nguoidung1@example.com&#34; }
}</code></pre>
<p>Ghi nhớ mã xác thực ở phần token trả về để gửi kèm trong các yêu cầu điều khiển về sau:</p>






<pre tabindex="0"><code>&lt;ma_xac_thuc&gt; = eyJhbGciOi...</code></pre>
<hr>
<h3 id="-gửi-yêu-cầu-điều-khiển-thiết-bị">⚙️ Gửi yêu cầu điều khiển thiết bị</h3>
<p>Sau khi đã có mã xác thực, bạn có thể gửi các yêu cầu điều khiển đến API của UpSnap.<br>
Tất cả các yêu cầu này đều cần thêm tiêu đề xác thực:</p>






<pre tabindex="0"><code>Authorization: Bearer &lt;ma_xac_thuc&gt;</code></pre>
<p>Các thao tác phổ biến:</p>
<h4 id="bật-máy-wake-on-lan">Bật máy (Wake-on-LAN)</h4>






<pre tabindex="0"><code>GET http://localhost:8090/api/upsnap/wake/&lt;ma_thiet_bi&gt;
Authorization: Bearer &lt;ma_xac_thuc&gt;</code></pre>
<h4 id="tắt-máy">Tắt máy</h4>






<pre tabindex="0"><code>GET http://localhost:8090/api/upsnap/shutdown/&lt;ma_thiet_bi&gt;
Authorization: Bearer &lt;ma_xac_thuc&gt;</code></pre>
<h4 id="khởi-động-lại-máy">Khởi động lại máy</h4>






<pre tabindex="0"><code>GET http://localhost:8090/api/upsnap/reboot/&lt;ma_thiet_bi&gt;
Authorization: Bearer &lt;ma_xac_thuc&gt;</code></pre>
<hr>
<h3 id="-ví-dụ-lệnh-get--post-trên-các-ứng-dụng">🧩 Ví dụ lệnh GET / POST trên các ứng dụng</h3>
<h4 id="-httpie-cli">🟦 HTTPie CLI</h4>
<p><strong>Cài đặt:</strong></p>






<pre tabindex="0"><code>pipx install httpie</code></pre>
<p><strong>Đăng nhập (POST):</strong></p>






<pre tabindex="0"><code>http POST http://localhost:8090/api/collections/users/auth-with-password \
  identity=nguoidung1 password=matkhaucuatoi</code></pre>
<p><strong>Bật máy (GET):</strong></p>






<pre tabindex="0"><code>http GET http://localhost:8090/api/upsnap/wake/&lt;ma_thiet_bi&gt; \
  &#34;Authorization: Bearer &lt;ma_xac_thuc&gt;&#34;</code></pre>
<hr>
<h4 id="-httpie-desktop">🟦 HTTPie Desktop</h4>
<ul>
<li>Tạo <strong>Draft Request</strong></li>
<li>Chọn phương thức <strong>POST</strong> hoặc <strong>GET</strong></li>
<li>Khi đăng nhập, thêm <strong>Body</strong> -&gt; <strong>Text</strong> -&gt; <strong>JSON</strong>: <code>{&quot;identity&quot;: &quot;nguoidung1&quot;, &quot;password&quot;: &quot;matkhaucuatoi&quot; }</code></li>
<li>Khi điều khiển thiết bị, thêm <strong>Auth</strong> -&gt; <strong>Bearer token</strong>: <code>&lt;ma_xac_thuc&gt;</code></li>
<li>Nhấn <strong>Send</strong> để gửi yêu cầu.</li>
</ul>
<p><img src="httpie-desktop-upsnap-login.png" alt="Đăng nhập để lấy token bằng POST request"></p>
<p><img src="httpie-desktop-upsnap-wake.png" alt="Gửi yêu cầu bật máy bằng GET request"></p>
<hr>
<h4 id="-http-request-shortcuts-android">🟩 HTTP Request Shortcuts (Android)</h4>
<ul>
<li>Tạo Shortcut mới.</li>
<li>Chọn <strong>POST</strong> để đăng nhập hoặc <strong>GET</strong> để bật máy.</li>
<li>Nhập URL API.</li>
<li>Khi đăng nhập thêm header JSON: <code>{&quot;identity&quot;: &quot;nguoidung1&quot;, &quot;password&quot;: &quot;matkhaucuatoi&quot; }</code></li>
<li>Khi điều khiển thiết bị, bật <strong>Bearer Authentication</strong> và dán token vào ô <strong>Token</strong>.</li>
</ul>
<hr>
<h4 id="-phím-tắt-shortcuts--iphone">🟦 Phím tắt (Shortcuts – iPhone)</h4>
<ul>
<li>Tạo <strong>Phím tắt mới</strong> → thêm hành động <strong>Lấy nội dung từ URL</strong></li>
<li>Chọn <strong>Phương thức: POST</strong> (đăng nhập) hoặc <strong>GET</strong> (bật/tắt máy).</li>
<li>Khi gửi POST, chọn <strong>Yêu cầu dạng JSON</strong> và nhập thông tin đăng nhập:






<pre tabindex="0"><code>{&#34;identity&#34;: &#34;nguoidung1&#34;, &#34;password&#34;: &#34;matkhaucuatoi&#34; }</code></pre>
</li>
<li>Khi gửi GET thêm Header:






<pre tabindex="0"><code>Key: Authorization
Value: Bearer &lt;ma_xac_thuc&gt;</code></pre>
</li>
<li>Nhấn <strong>Chạy (Run)</strong> để xem kết quả phản hồi.</li>
<li>📱 Bạn có thể <strong>lưu phím tắt này</strong> và <strong>kích hoạt bằng Siri</strong> để bật/tắt máy chỉ bằng giọng nói.</li>
</ul>
<hr>
<blockquote>
<p>💡 <strong>Lưu ý:</strong></p>
<ul>
<li>Các yêu cầu <code>GET</code> dùng cho hành động như bật, tắt, khởi động lại.</li>
<li>Các yêu cầu <code>POST</code> dùng để đăng nhập hoặc gửi dữ liệu.</li>
<li>Wake-on-LAN chỉ hoạt động khi <strong>thiết bị điều khiển và máy đích cùng mạng LAN hoặc Wi-Fi</strong>.</li>
<li>Khi điều khiển qua Internet, hãy dùng <strong>VPN</strong> hoặc <strong>reverse proxy</strong> để bảo mật kết nối.</li>
<li>Đảm bảo <strong>Wake on Magic Packet</strong> đã được bật trong BIOS hoặc driver mạng của máy đích.</li>
</ul></blockquote>
<hr>
<h2 id="5--tóm-tắt-nhanh-tldr">5. 📘 Tóm tắt nhanh (TL;DR)</h2>
<p><strong>1️⃣ Đăng nhập để lấy token</strong></p>






<pre tabindex="0"><code>POST http://localhost:8090/api/collections/users/auth-with-password \
  identity=nguoidung1 password=matkhaucuatoi</code></pre>
<p><strong>2️⃣ Bật máy tính từ xa</strong></p>






<pre tabindex="0"><code>GET http://localhost:8090/api/upsnap/wake/&lt;ma_thiet_bi&gt; \
  &#34;Authorization: Bearer &lt;ma_xac_thuc&gt;&#34;</code></pre>
<p><strong>3️⃣ Tắt máy tính từ xa</strong></p>






<pre tabindex="0"><code>GET http://localhost:8090/api/upsnap/shutdown/&lt;ma_thiet_bi&gt; \
  &#34;Authorization: Bearer &lt;ma_xac_thuc&gt;&#34;</code></pre>
<blockquote>
<p>🔑 Chỉ cần 3 lệnh trên — bạn có thể đăng nhập, bật hoặc tắt máy tính từ xa bằng API của UpSnap.</p></blockquote>
<hr>
<h2 id="6-tài-liệu-tham-khảo">6. Tài liệu tham khảo</h2>
<p>👉 UpSnap Wiki – REST API Documentation<br>
<a href="https://github.com/seriousm4x/UpSnap/wiki/Rest-API">https://github.com/seriousm4x/UpSnap/wiki/Rest-API</a></p>
<p>👉 Xem thêm: <a href="/2024/08/18/upsnap-wake-on-lan-tat-mo-may-tinh-tu-xa-cung-mang-noi-bo/">UpSnap - Wake-on-LAN tắt mở máy tính từ xa trong mạng nội bộ</a></p>
]]></content:encoded></item><item><title>Create a Password Hash</title><link>https://tuanbui.net/2025/10/23/create-password-hash/</link><pubDate>Thu, 23 Oct 2025 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2025/10/23/create-password-hash/</guid><description>&lt;p>When setting up &lt;strong>WUD (What&amp;rsquo;s up Docker?)&lt;/strong>, I needed to enable basic
authentication to protect the web interface. Normally, this requires
generating a password hash — but I didn&amp;rsquo;t have the &lt;code>htpasswd&lt;/code> tool
installed.&lt;/p>
&lt;p>After a bit of searching, I discovered that &lt;strong>OpenSSL&lt;/strong> can do the same
thing with a single command:&lt;/p>
&lt;pre>&lt;code>openssl passwd -apr1
&lt;/code>&lt;/pre>
&lt;p>This command prompts you to enter a password and returns its Apache
MD5-based hash, which works perfectly for WUD or any service expecting
an &lt;code>.htpasswd&lt;/code> format.&lt;/p></description><content:encoded><![CDATA[<p>When setting up <strong>WUD (What&rsquo;s up Docker?)</strong>, I needed to enable basic
authentication to protect the web interface. Normally, this requires
generating a password hash — but I didn&rsquo;t have the <code>htpasswd</code> tool
installed.</p>
<p>After a bit of searching, I discovered that <strong>OpenSSL</strong> can do the same
thing with a single command:</p>
<pre><code>openssl passwd -apr1
</code></pre>
<p>This command prompts you to enter a password and returns its Apache
MD5-based hash, which works perfectly for WUD or any service expecting
an <code>.htpasswd</code> format.</p>
<p>There are several other ways to create password hashes, but since
<code>openssl</code> is usually preinstalled on most systems, this method is quick
and convenient.</p>
<hr>
<p>💡 <strong>Tip:</strong> You can also use flags like <code>-stdin</code> to pass passwords
directly from scripts if needed — just remember not to expose plain
text passwords in your shell history.</p>
]]></content:encoded></item><item><title>Giới thiệu TrueNAS Connect: Đơn giản hóa việc giám sát và quản lý NAS trên toàn hệ thống của bạn</title><link>https://tuanbui.net/2025/10/23/truenas-connect/</link><pubDate>Thu, 23 Oct 2025 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2025/10/23/truenas-connect/</guid><description>&lt;h1 id="giới-thiệu-truenas-connect-đơn-giản-hóa-việc-giám-sát-và-quản-lý-nas-trên-toàn-hệ-thống-của-bạn">Giới thiệu TrueNAS Connect: Đơn giản hóa việc giám sát và quản lý NAS trên toàn hệ thống của bạn&lt;/h1>
&lt;p>&lt;strong>TrueNAS Connect&lt;/strong> là nền tảng quản lý tập trung mới của &lt;strong>iXsystems&lt;/strong>, giúp quản trị viên và đội ngũ IT có thể giám sát và kiểm soát tất cả các hệ thống &lt;strong>TrueNAS&lt;/strong> — bao gồm &lt;strong>TrueNAS CORE&lt;/strong>, &lt;strong>SCALE&lt;/strong>, và &lt;strong>Enterprise&lt;/strong> — từ một bảng điều khiển duy nhất và an toàn.&lt;/p></description><content:encoded><![CDATA[<h1 id="giới-thiệu-truenas-connect-đơn-giản-hóa-việc-giám-sát-và-quản-lý-nas-trên-toàn-hệ-thống-của-bạn">Giới thiệu TrueNAS Connect: Đơn giản hóa việc giám sát và quản lý NAS trên toàn hệ thống của bạn</h1>
<p><strong>TrueNAS Connect</strong> là nền tảng quản lý tập trung mới của <strong>iXsystems</strong>, giúp quản trị viên và đội ngũ IT có thể giám sát và kiểm soát tất cả các hệ thống <strong>TrueNAS</strong> — bao gồm <strong>TrueNAS CORE</strong>, <strong>SCALE</strong>, và <strong>Enterprise</strong> — từ một bảng điều khiển duy nhất và an toàn.</p>
<hr>
<h2 id="bảng-điều-khiển-tập-trung-cho-mọi-thiết-bị-nas">Bảng điều khiển tập trung cho mọi thiết bị NAS</h2>
<p>TrueNAS Connect cung cấp giao diện duy nhất giúp bạn xem tình trạng, hiệu suất và hoạt động của tất cả hệ thống NAS được kết nối. Thay vì đăng nhập từng thiết bị riêng lẻ, quản trị viên có thể theo dõi toàn bộ thông tin trong một bảng tổng hợp duy nhất.</p>
<p><strong>Các thông tin chính trên bảng điều khiển:</strong></p>
<ul>
<li><strong>Giám sát tình trạng hệ thống:</strong> CPU, RAM, đĩa, và mạng</li>
<li><strong>Trạng thái cụm lưu trữ:</strong> Dung lượng, hiệu năng, và tác vụ sao chép</li>
<li><strong>Cảnh báo thời gian thực:</strong> Thông báo khi có sự cố, bản cập nhật, hoặc thay đổi cấu hình</li>
</ul>
<hr>
<h2 id="quản-lý-và-cập-nhật-dễ-dàng-hơn">Quản lý và cập nhật dễ dàng hơn</h2>
<p>Việc quản lý firmware, plugin, và cấu hình nhiều thiết bị NAS thường mất thời gian. TrueNAS Connect giúp đơn giản hóa quy trình này với:</p>
<ul>
<li><strong>Cập nhật tập trung:</strong> Cập nhật một hoặc nhiều hệ thống cùng lúc</li>
<li><strong>Đồng bộ cấu hình:</strong> Giữ cài đặt nhất quán giữa các thiết bị</li>
<li><strong>Quản lý từ xa:</strong> Truy cập và điều khiển an toàn ở mọi nơi</li>
</ul>
<hr>
<h2 id="bảo-mật-và-kết-nối-được-tăng-cường">Bảo mật và kết nối được tăng cường</h2>
<p>TrueNAS Connect sử dụng kết nối mã hóa giữa NAS và cổng dịch vụ đám mây để bảo vệ dữ liệu và thông tin đăng nhập. Quản trị viên có thể xác thực an toàn, quản lý quyền truy cập, và kiểm soát hoàn toàn việc liên kết hệ thống.</p>
<hr>
<h2 id="phù-hợp-cho-cả-phòng-lab-tại-nhà-và-môi-trường-doanh-nghiệp">Phù hợp cho cả phòng lab tại nhà và môi trường doanh nghiệp</h2>
<p>Dù bạn đang quản lý vài thiết bị <strong>TrueNAS Mini</strong> tại nhà hay hàng chục hệ thống <strong>Enterprise</strong> ở nhiều địa điểm, TrueNAS Connect đều có thể mở rộng để đáp ứng nhu cầu. Giải pháp này giúp giảm gánh nặng quản trị và cung cấp cái nhìn sâu hơn về hạ tầng lưu trữ của bạn.</p>
<hr>
<h2 id="lợi-ích-chính">Lợi ích chính</h2>
<p>✅ Quản lý tập trung cho tất cả hệ thống TrueNAS<br>
✅ Giám sát và cảnh báo theo thời gian thực<br>
✅ Cập nhật nhanh hơn, an toàn hơn<br>
✅ Tăng khả năng quan sát và kiểm soát hệ thống phân tán<br>
✅ Hỗ trợ cả bản <strong>open-source</strong> và <strong>enterprise</strong></p>
<hr>
<p><strong>TrueNAS Connect</strong> là bước tiến mới trong việc hợp nhất quản lý lưu trữ, mang toàn bộ sức mạnh của hệ sinh thái TrueNAS vào một nền tảng kết nối duy nhất.</p>
]]></content:encoded></item><item><title>How I Convert Low-Quality Images into Scalable Vector Graphics</title><link>https://tuanbui.net/2025/10/02/image-to-vector/</link><pubDate>Thu, 02 Oct 2025 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2025/10/02/image-to-vector/</guid><description>&lt;p>Sometimes I come across a logo, icon, or drawing online that I’d love to use, but it only exists as a tiny, pixelated image. Redrawing it from scratch in Illustrator or Inkscape takes too much time, so I’ve been using a quick two-step workflow that gets me a clean SVG almost instantly. Here’s how I do it.&lt;/p>
&lt;hr>
&lt;h2 id="step-1-upscale-and-clean-the-image">Step 1: Upscale and Clean the Image&lt;/h2>
&lt;p>Low-quality images are pretty common, especially old logos or screenshots. That’s where &lt;a href="https://github.com/upscayl/upscayl">Upscayl&lt;/a> comes in.&lt;/p></description><content:encoded><![CDATA[<p>Sometimes I come across a logo, icon, or drawing online that I’d love to use, but it only exists as a tiny, pixelated image. Redrawing it from scratch in Illustrator or Inkscape takes too much time, so I’ve been using a quick two-step workflow that gets me a clean SVG almost instantly. Here’s how I do it.</p>
<hr>
<h2 id="step-1-upscale-and-clean-the-image">Step 1: Upscale and Clean the Image</h2>
<p>Low-quality images are pretty common, especially old logos or screenshots. That’s where <a href="https://github.com/upscayl/upscayl">Upscayl</a> comes in.</p>
<p>Upscayl is an AI-powered image upscaler. You just drop in your blurry image, and it makes it sharper, cleaner, and bigger. It’s not magic, but most of the time it gives me a version that’s much easier to work with.</p>
<p>👉 Example: A 200px logo suddenly becomes a crisp 800px version with clear edges.</p>
<hr>
<h2 id="step-2-convert-to-vector-with-vtracer">Step 2: Convert to Vector with VTracer</h2>
<p>Once I’ve got a decent-looking image, the next step is turning it into a vector. For that, I use <a href="https://github.com/visioncortex/vtracer">VTracer</a>.</p>
<p>VTracer takes a raster image (JPG, PNG, etc.) and converts it into vector paths (SVG). The cool thing is that the output is fully scalable, so I can resize it without ever losing quality.</p>
<p>It works especially well for logos, icons, and simple graphics with clear shapes. For photos, it’s not perfect (you’ll get a very abstract look), but for anything graphic-style, it’s great.</p>
<p>👉 I usually set <strong>Curve Fitting → Simplify</strong> to <strong>Polygon</strong>, which gives me cleaner, more accurate shapes.</p>
<hr>
<h2 id="why-i-love-this-workflow">Why I Love This Workflow</h2>
<ul>
<li><strong>Fast:</strong> It takes me less than 5 minutes to go from a blurry JPG to a clean SVG.</li>
<li><strong>Free &amp; Open Source:</strong> Both Upscayl and VTracer are open-source projects, so no subscriptions or paywalls.</li>
<li><strong>Saves me from redrawing:</strong> Honestly, tracing logos by hand is one of my least favorite tasks.</li>
</ul>
<hr>
<h2 id="final-thoughts">Final Thoughts</h2>
<p>This little trick has saved me so many times when working on quick design projects. Whether it’s a client logo, a random icon I found online, or even a hand-drawn sketch, this workflow makes it super easy to get a vector version without pulling my hair out.</p>
<p>If you work with graphics a lot, give Upscayl + VTracer a try. You’ll probably end up saving yourself hours of work, too.</p>
]]></content:encoded></item><item><title>My Network Infrastructure</title><link>https://tuanbui.net/2025/09/28/my-network-infrastructure/</link><pubDate>Sun, 28 Sep 2025 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2025/09/28/my-network-infrastructure/</guid><description>&lt;p>This post documents my home network setup, including the hardware I use, how services are organized, and some of the configuration choices I’ve made.&lt;br>
The goal is to keep everything reliable, cost-effective, and easy to maintain, while also experimenting with technologies like IPv6, Docker, and self-hosted applications.&lt;/p>
&lt;h2 id="hardware">Hardware&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Routers&lt;/strong>
&lt;ul>
&lt;li>1 × Celeron N4100 Intel I226 router with 4 × 2.5 GbE NICs (chosen for affordability)&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>Switches&lt;/strong>
&lt;ul>
&lt;li>1 × MERCUSYS MS105G 5-Port Gigabit switch (may upgrade to 2.5 GbE in the future)&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>Servers&lt;/strong>
&lt;ul>
&lt;li>No physical server yet&lt;/li>
&lt;li>1 × bhyve VM&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>Cables &amp;amp; Access Points&lt;/strong>
&lt;ul>
&lt;li>Cat 6 cables&lt;/li>
&lt;li>1 × Xiaomi CR8808&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h2 id="software">Software&lt;/h2>
&lt;h3 id="network-operating-systems">Network Operating Systems&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Operating System&lt;/strong>: FreeBSD (preferred)&lt;/li>
&lt;li>&lt;strong>Virtualization&lt;/strong>: bhyve (lightweight and efficient)&lt;/li>
&lt;li>&lt;strong>Firewall/Router&lt;/strong>: OpenWrt (running in a bhyve VM)&lt;/li>
&lt;li>&lt;strong>Docker Host&lt;/strong>: Debian (running in a bhyve VM)&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="my-network.svg" alt="My network flow">&lt;/p></description><content:encoded><![CDATA[<p>This post documents my home network setup, including the hardware I use, how services are organized, and some of the configuration choices I’ve made.<br>
The goal is to keep everything reliable, cost-effective, and easy to maintain, while also experimenting with technologies like IPv6, Docker, and self-hosted applications.</p>
<h2 id="hardware">Hardware</h2>
<ul>
<li><strong>Routers</strong>
<ul>
<li>1 × Celeron N4100 Intel I226 router with 4 × 2.5 GbE NICs (chosen for affordability)</li>
</ul>
</li>
<li><strong>Switches</strong>
<ul>
<li>1 × MERCUSYS MS105G 5-Port Gigabit switch (may upgrade to 2.5 GbE in the future)</li>
</ul>
</li>
<li><strong>Servers</strong>
<ul>
<li>No physical server yet</li>
<li>1 × bhyve VM</li>
</ul>
</li>
<li><strong>Cables &amp; Access Points</strong>
<ul>
<li>Cat 6 cables</li>
<li>1 × Xiaomi CR8808</li>
</ul>
</li>
</ul>
<h2 id="software">Software</h2>
<h3 id="network-operating-systems">Network Operating Systems</h3>
<ul>
<li><strong>Operating System</strong>: FreeBSD (preferred)</li>
<li><strong>Virtualization</strong>: bhyve (lightweight and efficient)</li>
<li><strong>Firewall/Router</strong>: OpenWrt (running in a bhyve VM)</li>
<li><strong>Docker Host</strong>: Debian (running in a bhyve VM)</li>
</ul>
<p><img src="my-network.svg" alt="My network flow"></p>
<h3 id="management--monitoring-tools">Management &amp; Monitoring Tools</h3>
<p><em>Not implemented yet</em></p>
<h3 id="security-applications">Security Applications</h3>
<p><em>Not implemented yet</em></p>
]]></content:encoded></item><item><title>My OpenWrt IPv6 Configuration</title><link>https://tuanbui.net/2025/09/26/openwrt-ipv6/</link><pubDate>Fri, 26 Sep 2025 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2025/09/26/openwrt-ipv6/</guid><description>&lt;p>Notes on how I set up IPv6 on my OpenWrt router.&lt;br>
Covers SLAAC for Android, firewall rules, and how it works with Docker and NPMplus.&lt;br>
Just a reference for my own setup.&lt;/p>
&lt;h2 id="my-network">My Network&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>WAN&lt;/strong>
&lt;ul>
&lt;li>ISP: Viettel&lt;/li>
&lt;li>IPv6 prefix: &lt;code>2001:1900:iced:cafe::/64&lt;/code>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>Host&lt;/strong>
&lt;ul>
&lt;li>MAC: &lt;code>bb:bf:1b:71:fe:20&lt;/code> (example)&lt;/li>
&lt;li>IPv6 address: &lt;code>2001:1900:iced:cafe:b9bf:1bff:fe71:fe20&lt;/code> (calculated via EUI-64)&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h2 id="openwrt-config">OpenWrt Config&lt;/h2>
&lt;h3 id="interfaces--wan">Interfaces → WAN&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Advanced Settings&lt;/strong>
&lt;ul>
&lt;li>Delegate IPv6 prefixes: ✅&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>DHCP Server → IPv6 Settings&lt;/strong>
&lt;ul>
&lt;li>RA-Service: &lt;code>disabled&lt;/code>&lt;/li>
&lt;li>DHCPv6-Service: &lt;code>disabled&lt;/code>&lt;/li>
&lt;li>NDP-Proxy: &lt;code>disabled&lt;/code>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h3 id="interfaces--lan">Interfaces → LAN&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Advanced Settings&lt;/strong>
&lt;ul>
&lt;li>Delegate IPv6 prefixes: ✅&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>DHCP Server → IPv6 Settings&lt;/strong>
&lt;ul>
&lt;li>Designated master: ❌ (unchecked)&lt;/li>
&lt;li>RA-Service: &lt;code>server mode&lt;/code>&lt;/li>
&lt;li>DHCPv6-Service: &lt;code>disabled&lt;/code>&lt;/li>
&lt;li>Announced IPv6 DNS servers: &lt;code>null&lt;/code>&lt;/li>
&lt;li>Local IPv6 DNS server: ✅&lt;/li>
&lt;li>Announced DNS domains: &lt;code>null&lt;/code>&lt;/li>
&lt;li>NDP-Proxy: &lt;code>disabled&lt;/code>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>IPv6 RA Settings&lt;/strong>
&lt;ul>
&lt;li>Default router: &lt;code>automatic&lt;/code>&lt;/li>
&lt;li>Enable SLAAC: ✅&lt;/li>
&lt;li>RA Flags: &lt;code>other config (O)&lt;/code> (set to &lt;code>none&lt;/code> for Android compatibility)&lt;/li>
&lt;li>NAT64 prefix: &lt;code>null&lt;/code>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h3 id="firewall--traffic-rules">Firewall → Traffic Rules&lt;/h3>
&lt;h4 id="allow-ipv6-http">Allow-IPv6-HTTP&lt;/h4>
&lt;ul>
&lt;li>&lt;strong>General Settings&lt;/strong>
&lt;ul>
&lt;li>Name: &lt;code>Allow-IPv6-HTTP&lt;/code>&lt;/li>
&lt;li>Protocol: &lt;code>TCP&lt;/code>&lt;/li>
&lt;li>Source zone: &lt;code>wan&lt;/code>&lt;/li>
&lt;li>Source port: &lt;code>any&lt;/code>&lt;/li>
&lt;li>Destination zone: &lt;code>lan&lt;/code>&lt;/li>
&lt;li>Destination address: &lt;code>::b9bf:1bff:fe71:fe20/-64&lt;/code>&lt;/li>
&lt;li>Destination port: &lt;code>80&lt;/code>&lt;/li>
&lt;li>Action: &lt;code>accept&lt;/code>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="Allow-IPv6-HTTP-1.png" alt="OpenWrt → Firewall → Traffic Rules → Allow-IPv6-HTTP">&lt;/p></description><content:encoded><![CDATA[<p>Notes on how I set up IPv6 on my OpenWrt router.<br>
Covers SLAAC for Android, firewall rules, and how it works with Docker and NPMplus.<br>
Just a reference for my own setup.</p>
<h2 id="my-network">My Network</h2>
<ul>
<li><strong>WAN</strong>
<ul>
<li>ISP: Viettel</li>
<li>IPv6 prefix: <code>2001:1900:iced:cafe::/64</code></li>
</ul>
</li>
<li><strong>Host</strong>
<ul>
<li>MAC: <code>bb:bf:1b:71:fe:20</code> (example)</li>
<li>IPv6 address: <code>2001:1900:iced:cafe:b9bf:1bff:fe71:fe20</code> (calculated via EUI-64)</li>
</ul>
</li>
</ul>
<h2 id="openwrt-config">OpenWrt Config</h2>
<h3 id="interfaces--wan">Interfaces → WAN</h3>
<ul>
<li><strong>Advanced Settings</strong>
<ul>
<li>Delegate IPv6 prefixes: ✅</li>
</ul>
</li>
<li><strong>DHCP Server → IPv6 Settings</strong>
<ul>
<li>RA-Service: <code>disabled</code></li>
<li>DHCPv6-Service: <code>disabled</code></li>
<li>NDP-Proxy: <code>disabled</code></li>
</ul>
</li>
</ul>
<h3 id="interfaces--lan">Interfaces → LAN</h3>
<ul>
<li><strong>Advanced Settings</strong>
<ul>
<li>Delegate IPv6 prefixes: ✅</li>
</ul>
</li>
<li><strong>DHCP Server → IPv6 Settings</strong>
<ul>
<li>Designated master: ❌ (unchecked)</li>
<li>RA-Service: <code>server mode</code></li>
<li>DHCPv6-Service: <code>disabled</code></li>
<li>Announced IPv6 DNS servers: <code>null</code></li>
<li>Local IPv6 DNS server: ✅</li>
<li>Announced DNS domains: <code>null</code></li>
<li>NDP-Proxy: <code>disabled</code></li>
</ul>
</li>
<li><strong>IPv6 RA Settings</strong>
<ul>
<li>Default router: <code>automatic</code></li>
<li>Enable SLAAC: ✅</li>
<li>RA Flags: <code>other config (O)</code> (set to <code>none</code> for Android compatibility)</li>
<li>NAT64 prefix: <code>null</code></li>
</ul>
</li>
</ul>
<h3 id="firewall--traffic-rules">Firewall → Traffic Rules</h3>
<h4 id="allow-ipv6-http">Allow-IPv6-HTTP</h4>
<ul>
<li><strong>General Settings</strong>
<ul>
<li>Name: <code>Allow-IPv6-HTTP</code></li>
<li>Protocol: <code>TCP</code></li>
<li>Source zone: <code>wan</code></li>
<li>Source port: <code>any</code></li>
<li>Destination zone: <code>lan</code></li>
<li>Destination address: <code>::b9bf:1bff:fe71:fe20/-64</code></li>
<li>Destination port: <code>80</code></li>
<li>Action: <code>accept</code></li>
</ul>
</li>
</ul>
<p><img src="Allow-IPv6-HTTP-1.png" alt="OpenWrt → Firewall → Traffic Rules → Allow-IPv6-HTTP"></p>
<ul>
<li><strong>Advanced Settings</strong>
<ul>
<li>Restrict to address family: <code>IPv6 only</code></li>
</ul>
</li>
</ul>
<h4 id="allow-ipv6-https">Allow-IPv6-HTTPS</h4>
<ul>
<li><strong>General Settings</strong>
<ul>
<li>Name: <code>Allow-IPv6-HTTPS</code></li>
<li>Protocol: <code>TCP, UDP</code></li>
<li>Source zone: <code>wan</code></li>
<li>Source port: <code>any</code></li>
<li>Destination zone: <code>lan</code></li>
<li>Destination address: <code>::b9bf:1bff:fe71:fe20/-64</code></li>
<li>Destination port: <code>443</code></li>
<li>Action: <code>accept</code></li>
</ul>
</li>
</ul>
<p><img src="Allow-IPv6-HTTPS-1.png" alt="OpenWrt → Firewall → Traffic Rules → Allow-IPv6-HTTPS"></p>
<ul>
<li><strong>Advanced Settings</strong>
<ul>
<li>Restrict to address family: <code>IPv6 only</code></li>
</ul>
</li>
</ul>
<h2 id="docker">Docker</h2>
<p>Not configured yet.<br>
Maybe enable IPv6 and assign an IPv6 address to the NPMplus container, and use a link-local address for the reverse proxy. Need to read more docs.</p>
<p class="notice">
  Temporary solution: run the NPMplus container in host mode — Nginx will automatically handle IPv6-to-IPv4.<br>
I run separate GoDNS containers to update both IPv4 and IPv6 addresses to Cloudflare.
</p>

<h2 id="tools">Tools</h2>
<ul>
<li><a href="https://eui64-calc.princelle.org">EUI-64 Calculator</a></li>
<li><a href="https://ping.pe">ping.pe</a> — for testing</li>
<li><a href="https://github.com/mccutchen/go-httpbin">go-httpbin</a> — for testing</li>
</ul>
<h2 id="references">References</h2>
<ul>
<li><a href="https://openwrt.org/docs/guide-user/network/ipv6/configuration">IPv6 configuration</a></li>
<li><a href="https://openwrt.org/docs/guide-user/firewall/fw3_configurations/fw3_ipv6_examples#dynamic_prefix_forwarding">Dynamic prefix forwarding — negative netmask notation <code>/-64</code></a></li>
</ul>
]]></content:encoded></item><item><title>Installing Windows Using netboot.xyz</title><link>https://tuanbui.net/2025/09/22/netboot-xyz-windows/</link><pubDate>Mon, 22 Sep 2025 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2025/09/22/netboot-xyz-windows/</guid><description>&lt;p>This post documents my attempt to install Windows 11 from my NAS.&lt;br>
All related files can be found &lt;a href="https://github.com/johndo100/netbootxyz-windows">here&lt;/a>.&lt;/p>
&lt;h2 id="how-it-works">How It Works&lt;/h2>
&lt;p>&lt;img src="how-it-works.svg" alt="How it works">&lt;/p>
&lt;h2 id="notes">Notes&lt;/h2>
&lt;ul>
&lt;li>Set the permissions of the &lt;code>extracted-wims&lt;/code> directory to &lt;code>775&lt;/code> (otherwise you may get an &lt;em>access denied&lt;/em> error).&lt;/li>
&lt;li>Enable &lt;em>Bypass Windows 11 requirements check&lt;/em> (TPM, Secure Boot, etc.) in &lt;code>autounattend.xml&lt;/code>.&lt;/li>
&lt;/ul>
&lt;h2 id="software">Software&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://netboot.xyz">netboot.xyz&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://schneegans.de/windows/unattend-generator">Generate autounattend.xml files for Windows 10/11&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/boot-and-install-windows">Boot and install Windows&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/boot-to-winpe">Boot to WinPE&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/download-winpe--windows-pe">Download Windows PE (WinPE)&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/winpe-create-usb-bootable-drive">Create bootable Windows PE media&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/cschneegans/unattend-generator">UnattendGenerator&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://netboot.xyz/docs/kb/pxe/windows">Installing Windows with netboot.xyz&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://linkarzu.com/posts/docker-practical/windows11-netbootxyz">Install Windows 11 over the network with netboot.xyz&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>This post documents my attempt to install Windows 11 from my NAS.<br>
All related files can be found <a href="https://github.com/johndo100/netbootxyz-windows">here</a>.</p>
<h2 id="how-it-works">How It Works</h2>
<p><img src="how-it-works.svg" alt="How it works"></p>
<h2 id="notes">Notes</h2>
<ul>
<li>Set the permissions of the <code>extracted-wims</code> directory to <code>775</code> (otherwise you may get an <em>access denied</em> error).</li>
<li>Enable <em>Bypass Windows 11 requirements check</em> (TPM, Secure Boot, etc.) in <code>autounattend.xml</code>.</li>
</ul>
<h2 id="software">Software</h2>
<ul>
<li><a href="https://netboot.xyz">netboot.xyz</a></li>
<li><a href="https://schneegans.de/windows/unattend-generator">Generate autounattend.xml files for Windows 10/11</a></li>
</ul>
<h2 id="references">References</h2>
<ul>
<li><a href="https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/boot-and-install-windows">Boot and install Windows</a></li>
<li><a href="https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/boot-to-winpe">Boot to WinPE</a></li>
<li><a href="https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/download-winpe--windows-pe">Download Windows PE (WinPE)</a></li>
<li><a href="https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/winpe-create-usb-bootable-drive">Create bootable Windows PE media</a></li>
<li><a href="https://github.com/cschneegans/unattend-generator">UnattendGenerator</a></li>
<li><a href="https://netboot.xyz/docs/kb/pxe/windows">Installing Windows with netboot.xyz</a></li>
<li><a href="https://linkarzu.com/posts/docker-practical/windows11-netbootxyz">Install Windows 11 over the network with netboot.xyz</a></li>
</ul>
]]></content:encoded></item><item><title>Set up VLAN on OpenWRT (istoreos-22.03)</title><link>https://tuanbui.net/2025/06/25/openwrt-23-vlan/</link><pubDate>Wed, 25 Jun 2025 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2025/06/25/openwrt-23-vlan/</guid><description>&lt;p>For a long time, I wanted to set up my network in a more proper way, but I couldn’t.&lt;/p>
&lt;p>VLANs were too complicated for me. All I knew was that they could split a physical network into smaller subnetworks.&lt;/p>
&lt;p>After I read &lt;a href="https://gaia.cs.umass.edu/kurose_ross/about.php">&lt;em>Computer Networking: A Top-Down Approach&lt;/em>&lt;/a>, I thought I should try again.&lt;/p>
&lt;p>I’m running an OpenWRT router that connects to my ISP via PPPoE (WAN). My LAN is a bridge that runs DHCP.&lt;/p></description><content:encoded><![CDATA[<p>For a long time, I wanted to set up my network in a more proper way, but I couldn’t.</p>
<p>VLANs were too complicated for me. All I knew was that they could split a physical network into smaller subnetworks.</p>
<p>After I read <a href="https://gaia.cs.umass.edu/kurose_ross/about.php"><em>Computer Networking: A Top-Down Approach</em></a>, I thought I should try again.</p>
<p>I’m running an OpenWRT router that connects to my ISP via PPPoE (WAN). My LAN is a bridge that runs DHCP.</p>
<p>It’s very simple and suitable for a home network, but I wanted something more.</p>
<p>I searched for “OpenWRT VLAN” on Google but had no luck; every tutorial was hard to understand.</p>
<p>Then I searched for “OPNsense VLAN” and found <a href="https://docs.opnsense.org/manual/how-tos/vlan_and_lagg.html">this</a>.</p>
<p>OPNsense docs are clearer and easier to understand. As always, Linux networking docs are a mess — FreeBSD is definitely better.</p>
<p>So I created VLANs, tagged them on a trunk port, and created an interface that runs DHCP.</p>
<p><img src="openwrt_istoreos_2203_luci_bridge_vlan.jpg" alt="OpenWRT VLAN LuCI"></p>
<p>Then I created a firewall zone and set the rules.</p>
<p>I don’t have any managed switch, so I connected my laptop (Windows 11) directly to the trunk port and <a href="https://hubandspoke.amastelek.com/using-hyper-vs-virtual-switch-on-windows-11-to-implement-vlans-over-ethernet-nics-without-built-in-support">tagged the VLAN on Windows 11</a>.</p>
<p>Everything works.</p>
<p>I’ll look more into it when I have free time. That’s it for now.</p>
<p>Some diagrams / flows you may be interested in:</p>
<p><img src="vlan-ingress-egress-10-20-30.svg" alt="VLAN flow INGRESS + EGRESS"></p>
<p><img src="vlan-traffic-flow-10-20.svg" alt="VLAN Traffic Flow 10/20"></p>
<p>Ref:</p>
<ul>
<li><a href="https://en.wikipedia.org/wiki/IEEE_802.1Q">IEEE 802.1Q</a></li>
<li><a href="https://openwrt.org/docs/guide-user/network/vlan/start">OpenWrt VLAN</a></li>
<li><a href="https://openwrt.org/docs/guide-user/network/dsa/start">OpenWrt DSA Networking</a></li>
<li><a href="https://docs.opnsense.org/manual/how-tos/vlan_and_lagg.html">OPNsense VLAN and LAGG Setup</a></li>
<li><a href="https://docs.netgate.com/pfsense/en/latest/vlan/index.html">pfSense Virtual LANs (VLANs)</a></li>
<li><a href="https://www.cisco.com/c/en/us/support/docs/smb/switches/cisco-small-business-300-series-managed-switches/smb5653-configure-port-to-vlan-interface-settings-on-a-switch-throug.html">Cisco Configure Port to VLAN Interface Settings on a Switch through the CLI</a></li>
</ul>
]]></content:encoded></item><item><title>TrueNAS Storage Encryption</title><link>https://tuanbui.net/2025/06/22/truenas-storage-encryption/</link><pubDate>Sun, 22 Jun 2025 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2025/06/22/truenas-storage-encryption/</guid><description>&lt;p>For some reason I need storage encryption for my disks.&lt;/p>
&lt;p>The machine is running TrueNAS 25.04 (Fangtooth) that have a ZFS pool for all data.&lt;/p>
&lt;p>I will need a USB to storge my key plug to my NAS when it running. If this USB connected the data will unlock, if there is none then no one can steal the data.&lt;/p>
&lt;p>Ref&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://www.truenas.com/docs/scale/25.04/scaletutorials/datasets/encryptionscale">TrueNAS 25.04/TrueNAS Tutorials/Datasets/Storage Encryption&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://wiki.archlinux.org/title/ZFS#Native_encryption">ArchWiki: ZFS Native encryption &lt;/a>&lt;/li>
&lt;li>&lt;a href="https://openzfs.org/wiki/Features#Native_data_and_metadata_encryption_for_zfs">Native data and metadata encryption for zfs&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>For some reason I need storage encryption for my disks.</p>
<p>The machine is running TrueNAS 25.04 (Fangtooth) that have a ZFS pool for all data.</p>
<p>I will need a USB to storge my key plug to my NAS when it running. If this USB connected the data will unlock, if there is none then no one can steal the data.</p>
<p>Ref</p>
<ul>
<li><a href="https://www.truenas.com/docs/scale/25.04/scaletutorials/datasets/encryptionscale">TrueNAS 25.04/TrueNAS Tutorials/Datasets/Storage Encryption</a></li>
<li><a href="https://wiki.archlinux.org/title/ZFS#Native_encryption">ArchWiki: ZFS Native encryption </a></li>
<li><a href="https://openzfs.org/wiki/Features#Native_data_and_metadata_encryption_for_zfs">Native data and metadata encryption for zfs</a></li>
</ul>
]]></content:encoded></item><item><title>Komodo Docker Build and deployment system</title><link>https://tuanbui.net/2025/06/12/komodo/</link><pubDate>Thu, 12 Jun 2025 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2025/06/12/komodo/</guid><description>&lt;p>&lt;a href="https://komo.do">Komodo&lt;/a>&lt;/p>
&lt;p>I used to have &lt;a href="https://dockge.kuma.pet">Dockge&lt;/a> on my system. It&amp;rsquo;s simple and easy-to-use.&lt;/p>
&lt;p>Today I heard about &lt;a href="https://komo.do">Komodo&lt;/a> - an other Docker management tool.&lt;/p>
&lt;p>With Komodo you can:&lt;/p>
&lt;ul>
&lt;li>Connect all of your servers, alert on CPU usage, memory usage, and disk usage, and connect to shell sessions.&lt;/li>
&lt;li>Create, start, stop, and restart Docker containers on the connected servers, view their status and logs, and connect to container shell.&lt;/li>
&lt;li>Deploy docker compose stacks. The file can be defined in UI, or in a git repo, with auto deploy on git push.&lt;/li>
&lt;li>Build application source into auto-versioned Docker images, auto built on webhook. Deploy single-use AWS instances for infinite capacity.&lt;/li>
&lt;li>Manage repositories on connected servers, which can perform automation via scripting / webhooks.&lt;/li>
&lt;li>Manage all your configuration / environment variables, with shared global variable and secret interpolation.&lt;/li>
&lt;li>Keep a record of all the actions that are performed and by whom.&lt;/li>
&lt;/ul>
&lt;p>Sound good?&lt;/p></description><content:encoded><![CDATA[<p><a href="https://komo.do">Komodo</a></p>
<p>I used to have <a href="https://dockge.kuma.pet">Dockge</a> on my system. It&rsquo;s simple and easy-to-use.</p>
<p>Today I heard about <a href="https://komo.do">Komodo</a> - an other Docker management tool.</p>
<p>With Komodo you can:</p>
<ul>
<li>Connect all of your servers, alert on CPU usage, memory usage, and disk usage, and connect to shell sessions.</li>
<li>Create, start, stop, and restart Docker containers on the connected servers, view their status and logs, and connect to container shell.</li>
<li>Deploy docker compose stacks. The file can be defined in UI, or in a git repo, with auto deploy on git push.</li>
<li>Build application source into auto-versioned Docker images, auto built on webhook. Deploy single-use AWS instances for infinite capacity.</li>
<li>Manage repositories on connected servers, which can perform automation via scripting / webhooks.</li>
<li>Manage all your configuration / environment variables, with shared global variable and secret interpolation.</li>
<li>Keep a record of all the actions that are performed and by whom.</li>
</ul>
<p>Sound good?</p>
<p>After <a href="https://komo.do/docs/setup/ferretdb">Install Komodo</a> I couldn&rsquo;t make it work. When I clicked to Signup button it responsed an error.</p>
<p>So I checked some videos on Youtube and all I needed to do is type <code>username</code> and <code>password</code> <strong>BEFORE</strong> I click signup button.</p>
<p>It was weird and the dev should change that.</p>
<p>I didn&rsquo;t have much of time so I was only try:</p>
<ul>
<li>Servers (Connect servers for alerting, building, and deploying)</li>
<li>Stacks (Deploy docker compose files)</li>
<li>Deployments (Deploy containers on your servers)</li>
</ul>
<p>All of them worked as I expected.</p>
<p>You can connect multi servers in one web-ui, all you need is expose port 8120 correctly so web-ui can connect to Periphery agent.</p>
<p>Deployments is something like <a href="https://www.truenas.com/docs/scale/scaleuireference/apps">TrueNAS SCALE Apps</a>. You can create Docker container with a pretty UI.</p>
<p>Stacks is something like <a href="https://dockge.kuma.pet">Dockge</a>, it&rsquo;s for someone love to write <code>compose.yaml</code>. I prefer this way.</p>
<p>After all it was a good tool, I will learn more about it:</p>
<ul>
<li>Builds (Build docker images)</li>
<li>Repos (Build using custom scripts or anything else)</li>
<li>Procedures (Compose Komodo actions together)</li>
<li>Actions (Custom scripts using the Komodo client)</li>
<li>Resource Syncs (Declare resources in TOML files)</li>
</ul>
]]></content:encoded></item><item><title>Logo vector các ứng dụng gọi xe tại Việt Nam</title><link>https://tuanbui.net/2025/05/27/vietnam-ride-hailing-apps-logos/</link><pubDate>Tue, 27 May 2025 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2025/05/27/vietnam-ride-hailing-apps-logos/</guid><description>&lt;p>Hỗ trợ&lt;/p>
&lt;ul>
&lt;li>Lalamove&lt;/li>
&lt;li>Ahamove&lt;/li>
&lt;li>ShopeeFood&lt;/li>
&lt;li>Grab&lt;/li>
&lt;li>TADA&lt;/li>
&lt;li>Xanh SM&lt;/li>
&lt;li>Maxim&lt;/li>
&lt;li>inDrive&lt;/li>
&lt;li>Bolt&lt;/li>
&lt;/ul>
&lt;p>Hình mẫu&lt;/p>
&lt;p>&lt;img src="ride-hailing_apps_in_vietnam_logos_v1.png" alt="hình mẫu">&lt;/p>
&lt;p>&lt;a href="ride-hailing_apps_in_vietnam_logos_v1.ai">Link tải .ai&lt;/a>&lt;/p>
&lt;p>Cập nhật thêm vài sticker&lt;/p>
&lt;p>&lt;img src="ride-hailing_apps_in_vietnam_stickers_v1.png" alt="hình mẫu">&lt;/p>
&lt;p>&lt;a href="ride-hailing_apps_in_vietnam_stickers_v1.ai">Link tải .ai&lt;/a>&lt;/p></description><content:encoded><![CDATA[<p>Hỗ trợ</p>
<ul>
<li>Lalamove</li>
<li>Ahamove</li>
<li>ShopeeFood</li>
<li>Grab</li>
<li>TADA</li>
<li>Xanh SM</li>
<li>Maxim</li>
<li>inDrive</li>
<li>Bolt</li>
</ul>
<p>Hình mẫu</p>
<p><img src="ride-hailing_apps_in_vietnam_logos_v1.png" alt="hình mẫu"></p>
<p><a href="ride-hailing_apps_in_vietnam_logos_v1.ai">Link tải .ai</a></p>
<p>Cập nhật thêm vài sticker</p>
<p><img src="ride-hailing_apps_in_vietnam_stickers_v1.png" alt="hình mẫu"></p>
<p><a href="ride-hailing_apps_in_vietnam_stickers_v1.ai">Link tải .ai</a></p>
]]></content:encoded></item><item><title>QR ngân hàng nhận tiền cho tài xế công nghệ Việt Nam</title><link>https://tuanbui.net/2025/05/24/gab-driver-bank-qrcode/</link><pubDate>Sat, 24 May 2025 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2025/05/24/gab-driver-bank-qrcode/</guid><description>&lt;p>File thiết kế sticker QR ngân hàng nhận tiền cho tài xế công nghệ Việt Nam (Grab, Be, XanhSM, Tada..)&lt;/p>
&lt;p>Cái này làm để dán lên xe hoặc nón bảo hiểm, móc khóa..&lt;/p>
&lt;p>File .ai có thể chỉnh sửa thoải mái.&lt;/p>
&lt;p>Hỗ trợ:&lt;/p>
&lt;ol>
&lt;li>Phương tiện
&lt;ol>
&lt;li>2 bánh&lt;/li>
&lt;li>4 bánh&lt;/li>
&lt;/ol>
&lt;/li>
&lt;li>Ứng dụng
&lt;ol>
&lt;li>Grab&lt;/li>
&lt;li>Be&lt;/li>
&lt;li>Xanh SM&lt;/li>
&lt;li>TADA&lt;/li>
&lt;/ol>
&lt;/li>
&lt;li>Ngân hàng
&lt;ol>
&lt;li>MoMo&lt;/li>
&lt;li>ACB&lt;/li>
&lt;li>BIDV&lt;/li>
&lt;/ol>
&lt;/li>
&lt;/ol>
&lt;p>Kích thước khuyến nghị cho 2 bánh 4cm x 4cm.&lt;/p></description><content:encoded><![CDATA[<p>File thiết kế sticker QR ngân hàng nhận tiền cho tài xế công nghệ Việt Nam (Grab, Be, XanhSM, Tada..)</p>
<p>Cái này làm để dán lên xe hoặc nón bảo hiểm, móc khóa..</p>
<p>File .ai có thể chỉnh sửa thoải mái.</p>
<p>Hỗ trợ:</p>
<ol>
<li>Phương tiện
<ol>
<li>2 bánh</li>
<li>4 bánh</li>
</ol>
</li>
<li>Ứng dụng
<ol>
<li>Grab</li>
<li>Be</li>
<li>Xanh SM</li>
<li>TADA</li>
</ol>
</li>
<li>Ngân hàng
<ol>
<li>MoMo</li>
<li>ACB</li>
<li>BIDV</li>
</ol>
</li>
</ol>
<p>Kích thước khuyến nghị cho 2 bánh 4cm x 4cm.</p>
<p>Hình mẫu</p>
<p><img src="VIETNAM_QRCODE_DRIVER.png" alt="hình mẫu"></p>
<p><a href="VIETNAM_QRCODE_DRIVER_V1.ai">Link tải .ai</a></p>
<p>Font: <a href="https://typeof.net/Iosevka">Iosevka</a></p>
<p>Tham khảo</p>
<ul>
<li><a href="https://github.com/sorairolake/qrtool">qrtool</a> - tạo mã QR hỗ trợ SVG</li>
<li><a href="https://vietqr.net">VIETQR</a> - tạo mã VIETQR</li>
<li><a href="https://vietqr.net/portal-service/download/documents/QR_Format_T&amp;C_v1.5.2_EN_102022.pdf">VietQR Code Format Technical Specification</a></li>
</ul>
]]></content:encoded></item><item><title>TADA Vietnam sticker design</title><link>https://tuanbui.net/2025/05/24/tada-vietnam-sticker-design/</link><pubDate>Sat, 24 May 2025 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2025/05/24/tada-vietnam-sticker-design/</guid><description>&lt;p>Hình mẫu&lt;/p>
&lt;p>&lt;img src="TADA_VIETNAM_STICKER_V2.png" alt="hình mẫu">&lt;/p>
&lt;p>&lt;a href="TADA_VIETNAM_STICKER_V2.ai">Link tải .ai&lt;/a>&lt;/p>
&lt;p>Fonts&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://fonts.google.com/specimen/Rowdies">Rowdies&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://fonts.google.com/specimen/Roboto+Mono">Roboto Mono&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Hình mẫu</p>
<p><img src="TADA_VIETNAM_STICKER_V2.png" alt="hình mẫu"></p>
<p><a href="TADA_VIETNAM_STICKER_V2.ai">Link tải .ai</a></p>
<p>Fonts</p>
<ul>
<li><a href="https://fonts.google.com/specimen/Rowdies">Rowdies</a></li>
<li><a href="https://fonts.google.com/specimen/Roboto+Mono">Roboto Mono</a></li>
</ul>
]]></content:encoded></item><item><title>iStoreOS (OpenWrt) Adblock with iPhone iOS</title><link>https://tuanbui.net/2025/05/11/openwrt-adblock-iphone-ios/</link><pubDate>Sun, 11 May 2025 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2025/05/11/openwrt-adblock-iphone-ios/</guid><description>&lt;p>I have an iPhone 13 connect to my iStoreOS (OpenWrt) router but the connection is not stable.&lt;/p>
&lt;p>Need to change something:&lt;/p>
&lt;ol>
&lt;li>iStoreOS (OpenWrt) Adblock
&lt;ol>
&lt;li>Force Local DNS: ✅&lt;/li>
&lt;li>Forced Zones: LAN&lt;/li>
&lt;li>Forced Ports: 53, 853, 5353&lt;/li>
&lt;/ol>
&lt;/li>
&lt;li>iPhone: turn off Private Wi-Fi Address.&lt;/li>
&lt;/ol>
&lt;p>&lt;a href="https://support.apple.com/en-eg/102509">Use private Wi-Fi addresses on Apple devices&lt;/a>.&lt;/p>
&lt;p>Wifi 5Ghz issue:&lt;/p>
&lt;ul>
&lt;li>Disable &lt;a href="https://en.wikipedia.org/wiki/Dynamic_frequency_selection">DFS&lt;/a> by &lt;a href="https://www.reddit.com/r/openwrt/comments/1e9l8xu/comment/lefqhw5/?utm_source=share&amp;amp;utm_medium=web3x&amp;amp;utm_name=web3xcss&amp;amp;utm_term=1&amp;amp;utm_content=share_button">change AP chanel to 36&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>I have an iPhone 13 connect to my iStoreOS (OpenWrt) router but the connection is not stable.</p>
<p>Need to change something:</p>
<ol>
<li>iStoreOS (OpenWrt) Adblock
<ol>
<li>Force Local DNS: ✅</li>
<li>Forced Zones: LAN</li>
<li>Forced Ports: 53, 853, 5353</li>
</ol>
</li>
<li>iPhone: turn off Private Wi-Fi Address.</li>
</ol>
<p><a href="https://support.apple.com/en-eg/102509">Use private Wi-Fi addresses on Apple devices</a>.</p>
<p>Wifi 5Ghz issue:</p>
<ul>
<li>Disable <a href="https://en.wikipedia.org/wiki/Dynamic_frequency_selection">DFS</a> by <a href="https://www.reddit.com/r/openwrt/comments/1e9l8xu/comment/lefqhw5/?utm_source=share&amp;utm_medium=web3x&amp;utm_name=web3xcss&amp;utm_term=1&amp;utm_content=share_button">change AP chanel to 36</a></li>
</ul>
]]></content:encoded></item><item><title>Motorbike riding gear as a gig driver in Vietnam</title><link>https://tuanbui.net/2025/04/23/motorbike-riding-gear/</link><pubDate>Wed, 23 Apr 2025 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2025/04/23/motorbike-riding-gear/</guid><description>&lt;p>I&amp;rsquo;m working as a bike driver in Saigon now (Tada/Grab).&lt;/p>
&lt;p>Will update photos very soon ^^.&lt;/p>
&lt;ul>
&lt;li>Bike:
&lt;ul>
&lt;li>&lt;a href="https://www.honda.com.vn/xe-may/san-pham/wave-rsx">HONDA WAVE RSX FI&lt;/a> SPORT EDITION (AFP110CSFR V)
&lt;ul>
&lt;li>&lt;a href="https://cdn.honda.com.vn/motorbike-manual/March2024/SAwWtly5pzv0KSr56luZ.pdf">Motorbike manual&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Accessories
&lt;ul>
&lt;li>Multi-function extended navigation balance bar
&lt;ul>
&lt;li>&lt;a href="https://e.tb.cn/h.69P0rO0r4IeTclH?tk=ad8VVdU0PV2">适用九号Nzmix/Fz/Mzmix f2z CZ/NZ90铝合金多功能扩展导航平衡杆&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Phone Mount
&lt;ul>
&lt;li>&lt;a href="https://e.tb.cn/h.69i5arxNM3klIYD?tk=u61jVdUJgNV">Kewig凯威格摩托车手机导航架减震外卖骑行电动车支架防雨遮阳帽&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>SAE Battery Connector
&lt;ul>
&lt;li>&lt;a href="https://e.tb.cn/h.697sw50L4CXIqgt?tk=BUOwVdUWCZF">凯威格摩托车usb充电接口SAE转换快插线汽车电瓶快拆无损改装防水&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Motorcycle USB charging port
&lt;ul>
&lt;li>&lt;a href="https://e.tb.cn/h.6945ysZie9p5BR2?tk=K0JsVdUTaDR">凯威格摩托车改装usb充电口加装手机充电器车充PD口超级快充防水&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>SAE to DC 5521 cable (for portable tire pump)
&lt;ul>
&lt;li>&lt;a href="https://e.tb.cn/h.697ATjOIz9Gj20b?tk=xwzrVdUQsy5">纯铜sae转DC延长线sae转监控电源线sae to dc5521线SAE转接线1.5m&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Tire pump
&lt;ul>
&lt;li>&lt;a href="https://baseusonline.com/product/766/baseus-super-mini-inflator-pump-air-compressor-portable-hand-held-auto-tire-pump-black">Baseus Super Mini Inflator Pump Air Compressor Portable Hand-Held Auto Tire Pump - Black&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Bungee cord
&lt;ul>
&lt;li>&lt;a href="https://daravin.vn/san-pham-cua-daravin/day-thun-tron-chang-hang/day-thun-rang-tang-chinh-daravin.html">Dây Thun Ràng Tăng Chỉnh DARAVIN&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Helmet
&lt;ul>
&lt;li>Tada Motorcycle half helmets
&lt;ul>
&lt;li>with &lt;a href="https://mubaohiemandes.vn/san-pham/kinh-chan-gio-andes-105l-danh-cho-cac-loai-mu-bao-hiem-khong-kinh-co-luoi-trai-3-nut-bam-phia-truoc">Kính Chắn Gió Andes 105L – Dành Cho Các Loại Mũ Bảo Hiểm Không Kính, Có Lưỡi Trai (3 nút bấm) Phía Trước&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Clothing
&lt;ul>
&lt;li>Tada safety vest (my favorites)&lt;/li>
&lt;li>Tada jacket (sunny and night)&lt;/li>
&lt;li>Tactical pants&lt;/li>
&lt;li>Sun gloves and sun sleeves (sunny)&lt;/li>
&lt;li>Footwear
&lt;ul>
&lt;li>&lt;a href="https://bitis.com.vn/products/dep-eva-phun-nam-biti-s-dem057010">Dép Eva Biti&amp;rsquo;s Nam DEM057010&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>XYZ
&lt;ul>
&lt;li>Phone
&lt;ul>
&lt;li>&lt;a href="https://s.lazada.vn/s.gb66j">Điện thoại ZTE Libero 5G III - 4/64GB Dimensity 700 ,Màn OLED ,Kháng nước IP57 - Mới nguyên seal - Hàng nhập khẩu&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Power bank
&lt;ul>
&lt;li>&lt;a href="https://s.lazada.vn/s.gb6l0">Sạc dự phòng Baseus sạc nhanh Airpow 20W - 20000mAh dành cho iPhone 15/14/13/12 Xiaomi&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Bluetooth speaker
&lt;ul>
&lt;li>&lt;a href="https://s.lazada.vn/s.gb63p">Monster M3 Bluetooth Speaker Outdoor Wearable Magnetic Clip-on Portable Bluetooth 5.4 Sound Box IPX5 Waterproof Subwoofer Powerful Bass D127&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Vacuum flask
&lt;ul>
&lt;li>&lt;a href="https://moriitalia.vn/products/011037-red">Bình giữ nhiệt thể thao 532ml Lafonte 011037-RED&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Bag
&lt;ul>
&lt;li>&lt;a href="https://s.lazada.vn/s.gb64z">Túi Chiến Thuật Cho Nam Túi Đeo Chéo Hệ Thống Mollle Túi Xách Thể Thao Túi Đeo Vai Túi Đeo Chéo Quân Đội Túi Điện Thoại Cắm Trại Du Lịch&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Rain coat
&lt;ul>
&lt;li>&lt;a href="https://shoprando.vn/ao-mua-canh-doi-poncho-thong-dung/ao-mua-easytrum-62.html">Áo Mưa EASYTRUM&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://shoprando.vn/ao-mua-bo-rando/bo-ao-mua-thong-thoang-gem-51.html">Bộ áo mưa thông thoáng GEM&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>I&rsquo;m working as a bike driver in Saigon now (Tada/Grab).</p>
<p>Will update photos very soon ^^.</p>
<ul>
<li>Bike:
<ul>
<li><a href="https://www.honda.com.vn/xe-may/san-pham/wave-rsx">HONDA WAVE RSX FI</a> SPORT EDITION (AFP110CSFR V)
<ul>
<li><a href="https://cdn.honda.com.vn/motorbike-manual/March2024/SAwWtly5pzv0KSr56luZ.pdf">Motorbike manual</a></li>
</ul>
</li>
<li>Accessories
<ul>
<li>Multi-function extended navigation balance bar
<ul>
<li><a href="https://e.tb.cn/h.69P0rO0r4IeTclH?tk=ad8VVdU0PV2">适用九号Nzmix/Fz/Mzmix f2z CZ/NZ90铝合金多功能扩展导航平衡杆</a></li>
</ul>
</li>
<li>Phone Mount
<ul>
<li><a href="https://e.tb.cn/h.69i5arxNM3klIYD?tk=u61jVdUJgNV">Kewig凯威格摩托车手机导航架减震外卖骑行电动车支架防雨遮阳帽</a></li>
</ul>
</li>
<li>SAE Battery Connector
<ul>
<li><a href="https://e.tb.cn/h.697sw50L4CXIqgt?tk=BUOwVdUWCZF">凯威格摩托车usb充电接口SAE转换快插线汽车电瓶快拆无损改装防水</a></li>
</ul>
</li>
<li>Motorcycle USB charging port
<ul>
<li><a href="https://e.tb.cn/h.6945ysZie9p5BR2?tk=K0JsVdUTaDR">凯威格摩托车改装usb充电口加装手机充电器车充PD口超级快充防水</a></li>
</ul>
</li>
<li>SAE to DC 5521 cable (for portable tire pump)
<ul>
<li><a href="https://e.tb.cn/h.697ATjOIz9Gj20b?tk=xwzrVdUQsy5">纯铜sae转DC延长线sae转监控电源线sae to dc5521线SAE转接线1.5m</a></li>
</ul>
</li>
<li>Tire pump
<ul>
<li><a href="https://baseusonline.com/product/766/baseus-super-mini-inflator-pump-air-compressor-portable-hand-held-auto-tire-pump-black">Baseus Super Mini Inflator Pump Air Compressor Portable Hand-Held Auto Tire Pump - Black</a></li>
</ul>
</li>
<li>Bungee cord
<ul>
<li><a href="https://daravin.vn/san-pham-cua-daravin/day-thun-tron-chang-hang/day-thun-rang-tang-chinh-daravin.html">Dây Thun Ràng Tăng Chỉnh DARAVIN</a></li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>Helmet
<ul>
<li>Tada Motorcycle half helmets
<ul>
<li>with <a href="https://mubaohiemandes.vn/san-pham/kinh-chan-gio-andes-105l-danh-cho-cac-loai-mu-bao-hiem-khong-kinh-co-luoi-trai-3-nut-bam-phia-truoc">Kính Chắn Gió Andes 105L – Dành Cho Các Loại Mũ Bảo Hiểm Không Kính, Có Lưỡi Trai (3 nút bấm) Phía Trước</a></li>
</ul>
</li>
</ul>
</li>
<li>Clothing
<ul>
<li>Tada safety vest (my favorites)</li>
<li>Tada jacket (sunny and night)</li>
<li>Tactical pants</li>
<li>Sun gloves and sun sleeves (sunny)</li>
<li>Footwear
<ul>
<li><a href="https://bitis.com.vn/products/dep-eva-phun-nam-biti-s-dem057010">Dép Eva Biti&rsquo;s Nam DEM057010</a></li>
</ul>
</li>
</ul>
</li>
<li>XYZ
<ul>
<li>Phone
<ul>
<li><a href="https://s.lazada.vn/s.gb66j">Điện thoại ZTE Libero 5G III - 4/64GB Dimensity 700 ,Màn OLED ,Kháng nước IP57 - Mới nguyên seal - Hàng nhập khẩu</a></li>
</ul>
</li>
<li>Power bank
<ul>
<li><a href="https://s.lazada.vn/s.gb6l0">Sạc dự phòng Baseus sạc nhanh Airpow 20W - 20000mAh dành cho iPhone 15/14/13/12 Xiaomi</a></li>
</ul>
</li>
<li>Bluetooth speaker
<ul>
<li><a href="https://s.lazada.vn/s.gb63p">Monster M3 Bluetooth Speaker Outdoor Wearable Magnetic Clip-on Portable Bluetooth 5.4 Sound Box IPX5 Waterproof Subwoofer Powerful Bass D127</a></li>
</ul>
</li>
<li>Vacuum flask
<ul>
<li><a href="https://moriitalia.vn/products/011037-red">Bình giữ nhiệt thể thao 532ml Lafonte 011037-RED</a></li>
</ul>
</li>
<li>Bag
<ul>
<li><a href="https://s.lazada.vn/s.gb64z">Túi Chiến Thuật Cho Nam Túi Đeo Chéo Hệ Thống Mollle Túi Xách Thể Thao Túi Đeo Vai Túi Đeo Chéo Quân Đội Túi Điện Thoại Cắm Trại Du Lịch</a></li>
</ul>
</li>
<li>Rain coat
<ul>
<li><a href="https://shoprando.vn/ao-mua-canh-doi-poncho-thong-dung/ao-mua-easytrum-62.html">Áo Mưa EASYTRUM</a></li>
<li><a href="https://shoprando.vn/ao-mua-bo-rando/bo-ao-mua-thong-thoang-gem-51.html">Bộ áo mưa thông thoáng GEM</a></li>
</ul>
</li>
</ul>
</li>
</ul>
]]></content:encoded></item><item><title>Hinlink (Linkstar) H68K OpenWrt firmware</title><link>https://tuanbui.net/2025/03/29/h68k-openwrt-firmware/</link><pubDate>Sat, 29 Mar 2025 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2025/03/29/h68k-openwrt-firmware/</guid><description>&lt;p>I bought one &lt;a href="https://www.hinlink.cn/44.html">路由狗 H68K&lt;/a> aka &lt;a href="https://www.seeedstudio.com/LinkStar-H68K-1432-V2-p-5886.html">LinkStar H68K&lt;/a> on Taobao last year but I lost its firmware.&lt;/p>
&lt;p>They provide download link but I couldn&amp;rsquo;t download it because it&amp;rsquo;s &lt;a href="https://pan.baidu.com/s/1f_o36_42GRHk60q5e5GsDw?pwd=NASY">Baidu Wangpan 百度网盘&lt;/a>.&lt;/p>
&lt;p>A guy from VOZ forum helped me download it and now I&amp;rsquo;m seeding it on torrent. My computer are not online 24/24 so if you want to download it please contact.&lt;/p>
&lt;pre tabindex="0">&lt;code>magnet:?xt=urn:btih:kld4ult542s6pshxmb3neo5wdffz4ox2&amp;amp;dn=Hinlink_H68K_OpenWrt_OP_250102.7z&amp;amp;xl=74662551&amp;amp;fc=1&lt;/code>&lt;/pre>
&lt;p>You can also download iStoreOS from &lt;a href="https://fw.koolcenter.com/iStoreOS/h6xk/">koolcenter&lt;/a>. (&lt;a href="https://doc.linkease.com/zh/guide/istoreos/install_h68k.html">docs&lt;/a>).&lt;/p></description><content:encoded><![CDATA[<p>I bought one <a href="https://www.hinlink.cn/44.html">路由狗 H68K</a> aka <a href="https://www.seeedstudio.com/LinkStar-H68K-1432-V2-p-5886.html">LinkStar H68K</a> on Taobao last year but I lost its firmware.</p>
<p>They provide download link but I couldn&rsquo;t download it because it&rsquo;s <a href="https://pan.baidu.com/s/1f_o36_42GRHk60q5e5GsDw?pwd=NASY">Baidu Wangpan 百度网盘</a>.</p>
<p>A guy from VOZ forum helped me download it and now I&rsquo;m seeding it on torrent. My computer are not online 24/24 so if you want to download it please contact.</p>






<pre tabindex="0"><code>magnet:?xt=urn:btih:kld4ult542s6pshxmb3neo5wdffz4ox2&amp;dn=Hinlink_H68K_OpenWrt_OP_250102.7z&amp;xl=74662551&amp;fc=1</code></pre>
<p>You can also download iStoreOS from <a href="https://fw.koolcenter.com/iStoreOS/h6xk/">koolcenter</a>. (<a href="https://doc.linkease.com/zh/guide/istoreos/install_h68k.html">docs</a>).</p>
]]></content:encoded></item><item><title>Update Wordpress core and plugin or theme when running on Docker</title><link>https://tuanbui.net/2025/03/14/wordpress-docker-update-core-and-plugin-or-theme/</link><pubDate>Fri, 14 Mar 2025 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2025/03/14/wordpress-docker-update-core-and-plugin-or-theme/</guid><description>&lt;p>I&amp;rsquo;m running a Wordpress website on Docker but for some reason it can not auto update core / plugin / theme.&lt;/p>
&lt;p>It asked for FTP, but maybe it lost permission. Default &lt;code>USER&lt;/code> is &lt;code>www-data&lt;/code>, Alpine image UID is &lt;code>82&lt;/code> and Debian image is &lt;code>33&lt;/code>.&lt;/p>
&lt;p>So I change owner to &lt;code>www-data&lt;/code>:&lt;/p>
&lt;pre tabindex="0">&lt;code>$ sudo chown -R 33:33 /opt/stack/wordpress/html&lt;/code>&lt;/pre>
&lt;p>Now it works.&lt;/p>
&lt;p>Ref:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://hub.docker.com/_/wordpress">wordpress docker&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>I&rsquo;m running a Wordpress website on Docker but for some reason it can not auto update core / plugin / theme.</p>
<p>It asked for FTP, but maybe it lost permission. Default <code>USER</code> is <code>www-data</code>, Alpine image UID is <code>82</code> and Debian image is <code>33</code>.</p>
<p>So I change owner to <code>www-data</code>:</p>






<pre tabindex="0"><code>$ sudo chown -R 33:33 /opt/stack/wordpress/html</code></pre>
<p>Now it works.</p>
<p>Ref:</p>
<ul>
<li><a href="https://hub.docker.com/_/wordpress">wordpress docker</a></li>
</ul>
]]></content:encoded></item><item><title>Vietnam traffic is dangerous, please be careful</title><link>https://tuanbui.net/2025/03/03/vietnam-traffic/</link><pubDate>Mon, 03 Mar 2025 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2025/03/03/vietnam-traffic/</guid><description>&lt;p>I&amp;rsquo;m working as a bike driver in Saigon at this time (Tada). Today I picked up a &amp;ldquo;nguoi nuoc ngoai&amp;rdquo; at Thu Duc Metro Station.&lt;/p>
&lt;p>It was funny because he knew his way to his place, he pointed to every turns on the trip.&lt;/p>
&lt;p>He worried that I (as a driver) don&amp;rsquo;t know the direction and can not take him to the right place. He may think that I was slow on the road.&lt;/p></description><content:encoded><![CDATA[<p>I&rsquo;m working as a bike driver in Saigon at this time (Tada). Today I picked up a &ldquo;nguoi nuoc ngoai&rdquo; at Thu Duc Metro Station.</p>
<p>It was funny because he knew his way to his place, he pointed to every turns on the trip.</p>
<p>He worried that I (as a driver) don&rsquo;t know the direction and can not take him to the right place. He may think that I was slow on the road.</p>
<p>Sorry but even if he&rsquo;s been here for a long time, he should let the driver do their job.</p>
<p>He didn&rsquo;t realize that the traffic in Vietnam is very dangerous. It&rsquo;s not safe, it&rsquo;s not like what he saw on Tiktok..</p>
<p>I drove around 200km per day in this city, I saw a lot. You know what I mean.</p>
<p>It&rsquo;s risky on the road and I just don&rsquo;t want to die.</p>
]]></content:encoded></item><item><title>Khôi phục dữ liệu đã xóa hoặc hư hỏng trên ổ cứng</title><link>https://tuanbui.net/2025/02/17/khoi-phuc-du-lieu/</link><pubDate>Mon, 17 Feb 2025 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2025/02/17/khoi-phuc-du-lieu/</guid><description>&lt;p>Nhân tiện hôm nay nhận được việc phục hồi file Excel bị hỏng của một bạn kế toán (?) nên mình sẽ viết về khôi phục hay phục hồi dữ liệu.&lt;/p>
&lt;p>Cũng rất tình cờ vì mình bình luận dạo trên Facebook một ngưởi đồng nghiệp cũ, bạn này gặp tình trạng tương tự và liên hệ mình.&lt;/p>
&lt;p>Bạn không sử dụng &lt;a href="https://www.dropbox.com">Dropbox&lt;/a> / &lt;a href="https://www.microsoft.com/vi-vn/microsoft-365/onedrive/online-cloud-storage">OneDrive&lt;/a> / &lt;a href="https://workspace.google.com/products/drive">Google Drive&lt;/a> nên không phục hồi được từ đám mây. Sau khi thử &lt;a href="https://support.microsoft.com/en-us/office/repair-a-corrupted-workbook-153a45f4-6cab-44b1-93ca-801ddcd4ea53">Excel Open and Repair&lt;/a> và &lt;a href="https://support.microsoft.com/en-us/office/recover-an-earlier-version-of-an-office-file-169cb166-e7e2-438e-8f39-9a8927828121">Recover Unsaved Workbooks&lt;/a> không được mình đã dùng &lt;a href="https://www.cleverfiles.com/data-recovery-software.html">Disk Drill Data Recovery Software&lt;/a> để khôi phục về phiên bản gần nhất.&lt;/p></description><content:encoded><![CDATA[<p>Nhân tiện hôm nay nhận được việc phục hồi file Excel bị hỏng của một bạn kế toán (?) nên mình sẽ viết về khôi phục hay phục hồi dữ liệu.</p>
<p>Cũng rất tình cờ vì mình bình luận dạo trên Facebook một ngưởi đồng nghiệp cũ, bạn này gặp tình trạng tương tự và liên hệ mình.</p>
<p>Bạn không sử dụng <a href="https://www.dropbox.com">Dropbox</a> / <a href="https://www.microsoft.com/vi-vn/microsoft-365/onedrive/online-cloud-storage">OneDrive</a> / <a href="https://workspace.google.com/products/drive">Google Drive</a> nên không phục hồi được từ đám mây. Sau khi thử <a href="https://support.microsoft.com/en-us/office/repair-a-corrupted-workbook-153a45f4-6cab-44b1-93ca-801ddcd4ea53">Excel Open and Repair</a> và <a href="https://support.microsoft.com/en-us/office/recover-an-earlier-version-of-an-office-file-169cb166-e7e2-438e-8f39-9a8927828121">Recover Unsaved Workbooks</a> không được mình đã dùng <a href="https://www.cleverfiles.com/data-recovery-software.html">Disk Drill Data Recovery Software</a> để khôi phục về phiên bản gần nhất.</p>
<blockquote>
<p>ℹ️ <strong>Ghi chú:</strong> This saved her alot of time, I guess.</p></blockquote>
<h3 id="1-khôi-phục-dữ-liệu-hoạt-động-như-thế-nào">1. Khôi phục dữ liệu hoạt động như thế nào</h3>
<blockquote>
<p>ℹ️ <strong>Ghi chú:</strong> nội dung chỉ vừa đủ hiểu nguyên lý để có thể sử dụng các phần mềm phục hồi dữ liệu trên Windows.</p></blockquote>
<p>Xem thêm chi tiết tại <a href="https://www.r-studio.com/file-recovery-basics.html">File Recovery Basics: How Data Recovery Works</a>.</p>
<h4 id="11-dữ-liệu-được-lưu-trữ-trên-đĩa-và-các-thiết-bị-nhớ">1.1. Dữ liệu được lưu trữ trên đĩa và các thiết bị nhớ</h4>
<details>
<summary>Click vào đây</summary>
<p>Ý tưởng cơ bản là bạn cần một thứ có thể ở hai hoặc nhiều trạng thái, một cách để thiết lập hoặc thay đổi trạng thái đó và một cách để đọc nó.</p>
<p>Ổ cứng và đĩa mềm sử dụng từ tính trong một đĩa quay. Đĩa được chia thành một số lượng lớn các vùng nhỏ và trong mỗi vùng, từ trường có thể chỉ theo một hướng hoặc hướng khác. Một cảm biến từ tính đọc nó, một nam châm điện khiến nó thay đổi.</p>
<p>Phương tiện quang học - CD, DVD và Blu-Ray - cũng tương tự như một đĩa quay, nhưng dữ liệu được lưu trữ bằng cách thay đổi mức độ phản chiếu của các vùng nhỏ trên đĩa và được đọc bằng tia laser. Cách dữ liệu được ghi khác nhau. Các đĩa được sản xuất hàng loạt được &ldquo;ép&rdquo;, dữ liệu được mã hóa dưới dạng các hố vật lý trong một lớp nhựa. Thay vào đó, đĩa có thể ghi sử dụng thuốc nhuộm và tia laser sử dụng nhiều năng lượng hơn để khiến thuốc nhuộm thay đổi độ phản chiếu để ghi (&ldquo;ghi&rdquo;) dữ liệu hoặc ít năng lượng hơn để chỉ đọc dữ liệu. Đĩa có thể ghi lại sử dụng thuốc nhuộm hoặc hợp kim có thể thay đổi giữa trạng thái phản xạ và không phản xạ bằng nhiều mức công suất khác nhau (trong trường hợp của CD-RW, khiến nó nóng lên ít hơn hoặc nhiều hơn).</p>
<p>Phương tiện thể rắn như ổ USB, SSD và bộ lưu trữ trong điện thoại thông minh sử dụng mạch vi điện tử. MOSFET cổng nổi là một bóng bán dẫn bao gồm một &rsquo;nút&rsquo; nhỏ, cổng nổi có thể lưu trữ điện tích và được bao quanh bởi một chất cách điện. Trường điện từ điện tích bị giữ lại của cổng nổi khiến bóng bán dẫn truyền hoặc chặn điện cho phép đọc trạng thái của nó. Để thay đổi trạng thái của cổng nổi, điện áp cao hơn sẽ khiến các electron tạo đường hầm lượng tử qua chất cách điện. Hàng tỷ bóng bán dẫn này được kết hợp trên các mạch tích hợp để tạo thành thiết bị lưu trữ.</p>
<p>Đó là ba loại lưu trữ chính đang được sử dụng hiện nay.</p>
<blockquote>
<p>Về cơ bản tất cả đều là <code>0</code> và <code>1</code>.</p></blockquote>
</details>
<p>Nếu bạn  muốn phục hồi dữ liệu vật lý bạn nên tìm hiểu thêm về những thứ này.</p>
<h4 id="12-các-tập-tin-được-lưu-trữ-trên-đĩa">1.2. Các tập tin được lưu trữ trên đĩa</h4>
<blockquote>
<p>ℹ️ <strong>Ghi chú:</strong> cho Windows và NTFS là hệ điều hành và hệ thống tệp tin phổ biến nhất.</p></blockquote>
<p>Bảng tệp chính (MFT) là tệp hệ thống trong hệ thống tệp NTFS (có tên là $MFT) lưu trữ <code>metadata information</code> về tất cả các tệp và thư mục trên ổ đĩa NTFS. MFT hoạt động như một chỉ mục cho tất cả các tệp và thư mục trên ổ đĩa, cung cấp quyền truy cập nhanh vào thông tin cần thiết để truy xuất tệp.</p>
<p>Mỗi tệp và thư mục trên ổ đĩa NTFS có một bản ghi duy nhất trong MFT, được gọi là mục nhập MFT. Mục nhập MFT chứa thông tin như tên tệp, dấu thời gian, quyền và con trỏ đến dữ liệu của tệp. Mục nhập MFT tương ứng được cập nhật khi tệp được tạo hoặc sửa đổi.</p>
<p>Khi tệp bị xóa, mục nhập MFT tương ứng được đánh dấu là <code>free</code>, nhưng dữ liệu tệp thực tế vẫn nằm trên đĩa cho đến khi bị ghi đè bởi dữ liệu mới. Điều này có thể hữu ích trong các tình huống khôi phục dữ liệu, vì dữ liệu của tệp đã xóa vẫn có thể khôi phục được. Khôi phục dữ liệu thành công yêu cầu các vùng đĩa bị dữ liệu đã xóa chiếm giữ không bị ghi đè.</p>
<h4 id="13-các-phương-pháp-phục-hồi-tập-tin">1.3. Các phương pháp phục hồi tập tin</h4>
<h5 id="131-phục-hồi-tập-tin-thông-qua-phân-tích-thông-tin-về-tập-tin-và-thư-mục">1.3.1. Phục hồi tập tin thông qua phân tích thông tin về tập tin và thư mục</h5>
<p>Phần mềm khôi phục tệp bắt đầu bằng cách cố gắng đọc và xử lý thông tin về tệp và thư mục. Nếu hệ thống tập tin trên đĩa không bị hỏng nghiêm trọng, thường có thể khôi phục toàn bộ cấu trúc tập tin và thư mục.</p>
<h5 id="132-phục-hồi-tập-tin-bằng-cách-tìm-kiếm-các-loại-tập-tin-đã-biết-raw-file-recovery">1.3.2. Phục hồi tập tin bằng cách tìm kiếm các loại tập tin đã biết (raw file recovery)</h5>
<p>Nếu phương pháp đầu tiên không tạo ra kết quả thỏa đáng, thì sẽ thực hiện tìm kiếm tệp thô. Phương pháp khôi phục dữ liệu thứ hai này có thể khôi phục dữ liệu tệp thành công hơn phương pháp đầu tiên, nhưng không thể khôi phục tên tệp gốc, dấu ngày/giờ hoặc toàn bộ cấu trúc thư mục và tệp của đĩa.</p>
<p>Tìm kiếm các loại tệp đã biết hoặc khôi phục tệp thô hoạt động bằng cách phân tích nội dung của đĩa để tìm &ldquo;chữ ký tệp&rdquo;. Chữ ký tệp là các mẫu chung biểu thị phần đầu hoặc phần cuối của tệp. Hầu như mọi loại tệp đều có ít nhất một chữ ký tệp. Ví dụ: tất cả các tệp thuộc loại tệp png (portable network graphics) đều bắt đầu bằng chuỗi &ldquo;‰PNG&rdquo; và nhiều tệp MP3 bắt đầu bằng chuỗi &ldquo;ID3&rdquo;. Các chữ ký tệp như vậy có thể được sử dụng để nhận dạng rằng một phần dữ liệu trên đĩa thuộc về một loại tệp nhất định và do đó có thể khôi phục được.</p>
<h3 id="2-nên-sao-lưu-an-toàn">2. Nên sao lưu an toàn</h3>
<p>Khôi phục dữ liệu chỉ nên là phương án cuối cùng, tốt hơn hết bạn nên sao lưu dữ liệu quan trọng một cách thường xuyên.</p>
<p>Trước đây mình sao lưu thủ công bằng cách sao chép tệp tin hàng ngày ra phân vùng khác và ổ cứng ngoài, hiện tại dùng thêm các giải pháp <a href="https://en.wikipedia.org/wiki/Comparison_of_online_backup_services">lưu trữ đám mây</a>.</p>
<blockquote>
<p>💡 <strong>Lời khuyên:</strong> Quy tắc 3-2-1 (hoặc Chiến lược dự phòng 3-2-1): Ý tưởng cho rằng giải pháp sao lưu tối thiểu phải bao gồm ba bản sao dữ liệu, bao gồm hai bản sao cục bộ và một bản sao từ xa.</p></blockquote>
<h3 id="3-thông-tin-hữu-ích">3. Thông tin hữu ích</h3>
<p>Phần mềm khôi phục dữ liệu</p>
<ul>
<li><a href="https://www.easeus.com/datarecoverywizard/free-data-recovery-software.htm">EaseUS Data Recovery Wizard Free</a> - up to 2GB of data <code>GUI</code></li>
<li><a href="https://www.cleverfiles.com/data-recovery-software.html">Disk Drill Data Recovery Software</a> - up to 500 MB of data <code>GUI</code></li>
<li><a href="https://www.minitool.com/data-recovery-software/free-for-windows.html">MiniTool® Data Recovery Free</a> - up to 1 GB of data <code>GUI</code></li>
<li><a href="https://www.ontrack.com/en-us/software/easyrecovery">Ontrack EasyRecovery</a> - up to 1 GB of data for files sizes less than 25 MB <code>GUI</code></li>
<li><a href="https://www.wisecleaner.com/wise-data-recovery.html">Wise Data Recovery</a> - up to 2GB of data <code>GUI</code></li>
<li><a href="https://dmde.com">DMDE Free Edition</a> - up to 4,000 files from a chosen directory per request <code>GUI</code></li>
<li><a href="https://www.ccleaner.com/recuva">Recuva</a> - file recovery <code>GUI</code></li>
<li><a href="https://apps.microsoft.com/detail/9n26s50ln705?hl=en-US&amp;gl=US">Windows File Recovery</a> - aka winfr <code>CLI</code></li>
<li><a href="https://www.cgsecurity.org">TestDisk and PhotoRec</a> - free and open-source <code>CLI</code></li>
<li><a href="https://alternativeto.net/software/testdisk">TestDisk Alternatives</a></li>
</ul>
<p>Phần mềm sao lưu dữ liệu</p>
<ul>
<li><a href="https://www.microsoft.com/en-us/microsoft-365/onedrive/online-cloud-storage">Microsoft OneDrive</a></li>
<li><a href="https://support.microsoft.com/en-us/windows/backup-and-restore-with-file-history-7bf065bf-f1ea-0a78-c1cf-7dcf51cc8bfc">Windows File History</a> <code>Windows 8+</code></li>
<li><a href="https://en.wikipedia.org/wiki/Backup_and_Restore">Windows Backup and Restore</a> <code>Windows 7</code></li>
<li><a href="https://tuanbui.net/2024/07/09/chuong-trinh-dong-bo-hoa-va-sao-luu">Danh sách các chương trình đồng bộ hóa và sao lưu dữ liệu</a></li>
<li><a href="https://alternativeto.net/software/microsoft-onedrive">Microsoft OneDrive Alternatives</a></li>
</ul>
<p>Thông tin tham khảo</p>
<ul>
<li><a href="https://support.microsoft.com/en-us/office/repair-a-corrupted-workbook-153a45f4-6cab-44b1-93ca-801ddcd4ea53">Excel Repair a corrupted workbook</a></li>
<li><a href="https://www.seagate.com/as/en/blog/how-do-hdds-and-ssds-store-data">HDD vs. SSD: How Do They Store Data?</a></li>
<li><a href="https://www.r-studio.com/file-recovery-basics.html">File Recovery Basics: How Data Recovery Works</a></li>
<li><a href="https://en.wikipedia.org/wiki/Data_recovery">Wikipedia Data recovery</a></li>
<li><a href="https://en.wikipedia.org/wiki/File_system">Wikipedia File system</a></li>
<li><a href="https://en.wikipedia.org/wiki/NTFS">Wikipedia NTFS</a></li>
<li><a href="https://learn.microsoft.com/en-us/windows/win32/fileio/master-file-table">Master File Table (Local File Systems)</a></li>
</ul>
]]></content:encoded></item><item><title>Flat-file CMS Wordpress and SQLite</title><link>https://tuanbui.net/2025/02/16/flat-file-cms-wordpress-sqlite/</link><pubDate>Sun, 16 Feb 2025 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2025/02/16/flat-file-cms-wordpress-sqlite/</guid><description>&lt;p>This is how-to install Flat-file CMS with Wordpress and SQLite.&lt;/p>
&lt;p>I use &lt;a href="https://getgrav.org">Grav&lt;/a> as flat-file CMS before but Wordpress is far more popular and now it support SQLite.&lt;/p>
&lt;p>So first you need to install a temp wordpress website as ussual, install &lt;a href="https://github.com/WordPress/sqlite-database-integration">SQLite Database Integration plugin&lt;/a>.&lt;/p>
&lt;p>Then install an actual wordpress after active this plug-in.&lt;/p>
&lt;p>Remove your temp MySQL database and it&amp;rsquo;s all set.&lt;/p>
&lt;p>Now you can move this website to any host that support Wordpress and SQLite.&lt;/p></description><content:encoded><![CDATA[<p>This is how-to install Flat-file CMS with Wordpress and SQLite.</p>
<p>I use <a href="https://getgrav.org">Grav</a> as flat-file CMS before but Wordpress is far more popular and now it support SQLite.</p>
<p>So first you need to install a temp wordpress website as ussual, install <a href="https://github.com/WordPress/sqlite-database-integration">SQLite Database Integration plugin</a>.</p>
<p>Then install an actual wordpress after active this plug-in.</p>
<p>Remove your temp MySQL database and it&rsquo;s all set.</p>
<p>Now you can move this website to any host that support Wordpress and SQLite.</p>
<p>Video how-to:</p>
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/QWPI0_n5ivg?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>

]]></content:encoded></item><item><title>Something need to learn very soon</title><link>https://tuanbui.net/2025/02/06/something-need-to-learn/</link><pubDate>Thu, 06 Feb 2025 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2025/02/06/something-need-to-learn/</guid><description>&lt;p>Python&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://www.djangoproject.com">Django&lt;/a> and &lt;a href="https://flask.palletsprojects.com">Flask&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://htmx.org">htmx&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://pandas.pydata.org">pandas&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://openpyxl.readthedocs.io">openpyxl&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>Ref&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://github.com/harabat/django-htmx">Django + HTMX tutorial&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/spookylukey/django-htmx-patterns">Django + htmx patterns&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Python</p>
<ul>
<li><a href="https://www.djangoproject.com">Django</a> and <a href="https://flask.palletsprojects.com">Flask</a></li>
<li><a href="https://htmx.org">htmx</a></li>
<li><a href="https://pandas.pydata.org">pandas</a></li>
<li><a href="https://openpyxl.readthedocs.io">openpyxl</a></li>
</ul>
<p>Ref</p>
<ul>
<li><a href="https://github.com/harabat/django-htmx">Django + HTMX tutorial</a></li>
<li><a href="https://github.com/spookylukey/django-htmx-patterns">Django + htmx patterns</a></li>
</ul>
]]></content:encoded></item><item><title>Design patterns</title><link>https://tuanbui.net/2025/02/05/design-patterns/</link><pubDate>Wed, 05 Feb 2025 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2025/02/05/design-patterns/</guid><description>&lt;p>&lt;a href="https://refactoring.guru">Refactoring.Guru&lt;/a>&lt;/p>
&lt;p>&lt;a href="https://github.com/faif/python-patterns">python-patterns&lt;/a>&lt;/p></description><content:encoded><![CDATA[<p><a href="https://refactoring.guru">Refactoring.Guru</a></p>
<p><a href="https://github.com/faif/python-patterns">python-patterns</a></p>
]]></content:encoded></item><item><title>Notes about lumlua</title><link>https://tuanbui.net/2024/12/15/lumlua/</link><pubDate>Sun, 15 Dec 2024 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2024/12/15/lumlua/</guid><description>&lt;p>It&amp;rsquo;s me trying to write a tiny &lt;del>golang&lt;/del> Python app.&lt;/p>
&lt;p>For bank transaction alert.&lt;/p></description><content:encoded><![CDATA[<p>It&rsquo;s me trying to write a tiny <del>golang</del> Python app.</p>
<p>For bank transaction alert.</p>
]]></content:encoded></item><item><title>KVM QEMU libvirt virsh virt-install CLI on Ubuntu Linux</title><link>https://tuanbui.net/2024/11/27/linux-kvm-qemu-libvirt-virsh-virt-install/</link><pubDate>Wed, 27 Nov 2024 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2024/11/27/linux-kvm-qemu-libvirt-virsh-virt-install/</guid><description>&lt;p>Trying to have virtual machines on Ubuntu Linux, of course I&amp;rsquo;m more comfotable on FreeBSD Bhyve.&lt;/p>
&lt;h2 id="virt-install">virt-install&lt;/h2>
&lt;p>provision new virtual machines&lt;/p>
&lt;pre tabindex="0">&lt;code># virt-install --name vm0 --memory 1024 --vcpus 2 \
--os-variant ubuntu24.04 \
--disk size=10 --network bridge=br0 --graphics none --console pty,target_type=serial \
--location /usr/local/etc/iso/ubuntu-24.04.1-live-server-amd64.iso,kernel=casper/vmlinuz,initrd=casper/initrd \
--extra-args &amp;#39;console=ttyS0,115200n8 serial&amp;#39;&lt;/code>&lt;/pre>
&lt;h2 id="virsh">virsh&lt;/h2>
&lt;p>management user interface&lt;/p>
&lt;pre tabindex="0">&lt;code>virsh list --all
virsh start vm0
virsh autostart vm0
virsh autostart --disable vm0
virsh shutdown vm0
virsh destroy vm0
virsh undefine vm0 --remove-all-storage&lt;/code>&lt;/pre>
&lt;h2 id="usb-passthrough">usb passthrough&lt;/h2>
&lt;pre tabindex="0">&lt;code>$ lsusb
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 002: ID 2c7c:0125 Quectel Wireless Solutions Co., Ltd. EC25 LTE modem
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub&lt;/code>&lt;/pre>
&lt;pre tabindex="0">&lt;code>$ cat usb_ec25.xml
&amp;lt;hostdev mode=&amp;#39;subsystem&amp;#39; type=&amp;#39;usb&amp;#39; managed=&amp;#39;yes&amp;#39;&amp;gt;
&amp;lt;source&amp;gt;
&amp;lt;vendor id=&amp;#39;0x2c7c&amp;#39;/&amp;gt;
&amp;lt;product id=&amp;#39;0x0125&amp;#39;/&amp;gt;
&amp;lt;/source&amp;gt;
&amp;lt;/hostdev&amp;gt;&lt;/code>&lt;/pre>
&lt;pre tabindex="0">&lt;code># virsh attach-device vm0 --file usb_ec25.xml --config&lt;/code>&lt;/pre>
&lt;p>&lt;a href="https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/6/html/virtualization_administration_guide/sect-managing_guest_virtual_machines_with_virsh-attaching_and_updating_a_device_with_virsh#sect-Managing_guest_virtual_machines_with_virsh-Attaching_and_updating_a_device_with_virsh">Attaching and Updating a Device with virsh&lt;/a>&lt;/p></description><content:encoded><![CDATA[<p>Trying to have virtual machines on Ubuntu Linux, of course I&rsquo;m more comfotable on FreeBSD Bhyve.</p>
<h2 id="virt-install">virt-install</h2>
<p>provision new virtual machines</p>






<pre tabindex="0"><code># virt-install --name vm0 --memory 1024 --vcpus 2 \
--os-variant ubuntu24.04 \
--disk size=10 --network bridge=br0 --graphics none --console pty,target_type=serial \
--location /usr/local/etc/iso/ubuntu-24.04.1-live-server-amd64.iso,kernel=casper/vmlinuz,initrd=casper/initrd \
--extra-args &#39;console=ttyS0,115200n8 serial&#39;</code></pre>
<h2 id="virsh">virsh</h2>
<p>management user interface</p>






<pre tabindex="0"><code>virsh list --all

virsh start vm0

virsh autostart vm0

virsh autostart --disable vm0

virsh shutdown vm0

virsh destroy vm0

virsh undefine vm0 --remove-all-storage</code></pre>
<h2 id="usb-passthrough">usb passthrough</h2>






<pre tabindex="0"><code>$ lsusb   
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 002: ID 2c7c:0125 Quectel Wireless Solutions Co., Ltd. EC25 LTE modem
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub</code></pre>






<pre tabindex="0"><code>$ cat usb_ec25.xml 
&lt;hostdev mode=&#39;subsystem&#39; type=&#39;usb&#39; managed=&#39;yes&#39;&gt;
    &lt;source&gt;
        &lt;vendor id=&#39;0x2c7c&#39;/&gt;
        &lt;product id=&#39;0x0125&#39;/&gt;
    &lt;/source&gt;
&lt;/hostdev&gt;</code></pre>






<pre tabindex="0"><code># virsh attach-device vm0 --file usb_ec25.xml --config</code></pre>
<p><a href="https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/6/html/virtualization_administration_guide/sect-managing_guest_virtual_machines_with_virsh-attaching_and_updating_a_device_with_virsh#sect-Managing_guest_virtual_machines_with_virsh-Attaching_and_updating_a_device_with_virsh">Attaching and Updating a Device with virsh</a></p>
]]></content:encoded></item><item><title>godns webhook coturn and dynamic IP address on FreeBSD</title><link>https://tuanbui.net/2024/11/22/godns-webhook-coturn/</link><pubDate>Fri, 22 Nov 2024 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2024/11/22/godns-webhook-coturn/</guid><description>&lt;p>This is for FreeBSD. It may not work on Linux because &lt;code>sh&lt;/code> and &lt;code>sed&lt;/code> on FreeBSD are not the same as Linux (&lt;code>bash&lt;/code> and other &lt;code>sed&lt;/code>).&lt;/p>
&lt;p>We&amp;rsquo;re running a STUN/TURN server, it work well but we don&amp;rsquo;t have a static IP address. Everytime we got a new IP address we need to change coturn config and restart the service.&lt;/p>
&lt;p>There is a way to get it work by a &lt;code>cron&lt;/code> job that run a shell script check public ip every 5 minutes and restart service if the public IP address has changed.&lt;/p></description><content:encoded><![CDATA[<p>This is for FreeBSD. It may not work on Linux because <code>sh</code> and <code>sed</code> on FreeBSD are not the same as Linux (<code>bash</code> and other <code>sed</code>).</p>
<p>We&rsquo;re running a STUN/TURN server, it work well but we don&rsquo;t have a static IP address. Everytime we got a new IP address we need to change coturn config and restart the service.</p>
<p>There is a way to get it work by a <code>cron</code> job that run a shell script check public ip every 5 minutes and restart service if the public IP address has changed.</p>






<pre tabindex="0"><code>#!/bin/bash

# Linux only, doesn&#39;t work on FreeBSD
current_external_ip_config=$(cat /etc/turnserver.conf | grep &#34;^external-ip&#34; | cut -d&#39;=&#39; -f2)
current_external_ip=$(dig +short &lt;MY_DOMAIN&gt;)

if [[ -n &#34;$current_external_ip&#34; ]] &amp;&amp; [[ $current_external_ip_config != $current_external_ip ]]; then
        sed -i &#34;/^external-ip=/ c external-ip=$current_external_ip&#34; /etc/turnserver.conf
        systemctl restart coturn
fi</code></pre>
<p>ref: <a href="https://github.com/coturn/coturn/issues/469">set up with dynamic ip address</a></p>
<p>Since we&rsquo;re running a godns daemon to update our IP to Cloudflare DNS server we also want godns send a webhook to coturn server whenever it update IP to Cloudflare. That may be more effective.</p>
<p>So this is what we have:</p>
<p><a href="https://github.com/TimothyYe/godns">godns</a></p>






<pre tabindex="0"><code>$ cat /etc/godns/config.json 
{
  &#34;provider&#34;: &#34;Cloudflare&#34;,
  &#34;login_token&#34;: &#34;YOUR_TOKEN&#34;,
  &#34;domains&#34;: [
    {
      &#34;domain_name&#34;: &#34;yourdomain.com&#34;,
      &#34;sub_domains&#34;: [
        &#34;@&#34;
      ]
    }
  ],
  &#34;ip_urls&#34;: [
    &#34;https://api.ipify.org&#34;
  ],
  &#34;ip_type&#34;: &#34;IPv4&#34;,
  &#34;interval&#34;: 300,
  &#34;resolver&#34;: &#34;8.8.8.8&#34;,
  &#34;webhook&#34;: {
    &#34;enabled&#34;: true,
    &#34;url&#34;: &#34;http://your.coturn.webhook.endpoint:9000/hooks/godns&#34;,
    &#34;request_body&#34;: &#34;{ \&#34;domain\&#34;: \&#34;{{.Domain}}\&#34;, \&#34;ip\&#34;: \&#34;{{.CurrentIP}}\&#34;, \&#34;ip_type\&#34;: \&#34;{{.IPType}}\&#34; }&#34;
  }
}</code></pre>
<p><a href="https://github.com/adnanh/webhook">webhook</a></p>






<pre tabindex="0"><code>$ cat /usr/local/etc/webhook.yaml
---
# See https://github.com/adnanh/webhook/wiki for further information on this
# file and its options.  Instead of YAML, you can also define your
# configuration as JSON.  We&#39;ve picked YAML for these examples because it
# supports comments, whereas JSON does not.
#
# In the default configuration, webhook runs as user nobody.  Depending on
# the actions you want your webhooks to take, you might want to run it as
# user root.  Set the rc.conf(5) variable webhook_user to the desired user,
# and restart webhook.

# An example for a simple webhook you can call from a browser or with
# wget(1) or curl(1):
#   curl -v &#39;localhost:9000/hooks/samplewebhook?secret=geheim&#39;
- id: godns
  execute-command: &#34;/usr/local/etc/godns.sh&#34;
  command-working-directory: &#34;/usr/local/etc&#34;
  pass-arguments-to-command:
  - source: payload
    name: domain
  - source: payload
    name: ip
  - source: payload
    name: ip_type
  trigger-rule:
    and:
      - match:
          type: value
          value: &#34;your.domain.com&#34;
          parameter:
            source: payload
            name: domain</code></pre>
<p>shell script</p>






<pre tabindex="0"><code>$ cat /usr/local/etc/godns.sh
#!/bin/sh

# write ip log to a file
now=&#34;$(date +&#39;%y%m%d%H%M%S%N&#39;)&#34;
echo $now $1 $2 $3 &gt;&gt; godns.txt

# restart coturn when ip changed
turnserver_config=&#34;/usr/local/etc/turnserver.conf&#34;
current_external_ip_config=$(cat $turnserver_config | grep &#34;^external-ip&#34; | cut -d&#39;=&#39; -f2)
current_external_ip_webhook=$2

if [ -n &#34;$current_external_ip_webhook&#34; ] &amp;&amp; [ $current_external_ip_config != $current_external_ip_webhook ]; then
        sed -i .old -e &#34;s/external-ip=$current_external_ip_config/external-ip=$current_external_ip_webhook/g&#34; $turnserver_config
        service turnserver restart
fi</code></pre>
<p>It may not work well enough, if something happen and godns can not send webhook to coturn server. But for now we stick with it.</p>
]]></content:encoded></item><item><title>GPT table corrupt if drive has been formated as ZFS</title><link>https://tuanbui.net/2024/11/10/note-zfs-gpt-table-corrupt/</link><pubDate>Sun, 10 Nov 2024 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2024/11/10/note-zfs-gpt-table-corrupt/</guid><description>&lt;p>&lt;a href="https://forums.FreeBSD.org/threads/gpt-table-corrupt.52102/post-292341">ZFS GPT table corrupt&lt;/a>&lt;/p>
&lt;p>&lt;code>gpart destroy -F ada0&lt;/code>&lt;/p></description><content:encoded><![CDATA[<p><a href="https://forums.FreeBSD.org/threads/gpt-table-corrupt.52102/post-292341">ZFS GPT table corrupt</a></p>
<p><code>gpart destroy -F ada0</code></p>
]]></content:encoded></item><item><title>OpenWrt virtual machine on FreeBSD bhyve if_bridge tap (vmnet), netgraph, VALE and vether</title><link>https://tuanbui.net/2024/11/06/freebsd-bhyve-openwrt/</link><pubDate>Wed, 06 Nov 2024 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2024/11/06/freebsd-bhyve-openwrt/</guid><description>&lt;p>WRITING&lt;/p>
&lt;p>We&amp;rsquo;re buiding our all-in-one server for our business.&lt;/p>
&lt;p>The network is complicate when we want to run serveral virtual machine and still want to connect to outside. The router will be virtualized, it run OpenWrt or OPNsense as we want.&lt;/p>
&lt;p>We have several choice:&lt;/p>
&lt;h2 id="1-traditional-way">1. Traditional way&lt;/h2>
&lt;p>&lt;a href="https://man.freebsd.org/cgi/man.cgi?query=if_bridge&amp;amp;sektion=4&amp;amp;manpath=FreeBSD+14.1-RELEASE">if_bridge&lt;/a> and &lt;a href="https://man.freebsd.org/cgi/man.cgi?query=tap&amp;amp;sektion=4&amp;amp;manpath=FreeBSD+14.1-RELEASE">tap&lt;/a>.&lt;/p>
&lt;p>&lt;img src="if_bridge_and_tap.svg" alt="if_bridge and tap" title="if_bridge and tap">&lt;/p>
&lt;p>It was not so efficiently.&lt;/p>
&lt;p>We test on N4100 CPU:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Client&lt;/th>
&lt;th>Server&lt;/th>
&lt;th>Speed&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>vm2 (Debian)&lt;/td>
&lt;td>vm0 (ImmortalWrt)&lt;/td>
&lt;td>2.11 Gbits/sec&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>vm2 (Debian)&lt;/td>
&lt;td>vm1 (Debian)&lt;/td>
&lt;td>2.17 Gbits/sec&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>vm2 (Debian)&lt;/td>
&lt;td>host (FreeBSD)&lt;/td>
&lt;td>1.35 Gbits/sec&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Our config:&lt;/p></description><content:encoded><![CDATA[<p>WRITING</p>
<p>We&rsquo;re buiding our all-in-one server for our business.</p>
<p>The network is complicate when we want to run serveral virtual machine and still want to connect to outside. The router will be virtualized, it run OpenWrt or OPNsense as we want.</p>
<p>We have several choice:</p>
<h2 id="1-traditional-way">1. Traditional way</h2>
<p><a href="https://man.freebsd.org/cgi/man.cgi?query=if_bridge&amp;sektion=4&amp;manpath=FreeBSD+14.1-RELEASE">if_bridge</a> and <a href="https://man.freebsd.org/cgi/man.cgi?query=tap&amp;sektion=4&amp;manpath=FreeBSD+14.1-RELEASE">tap</a>.</p>
<p><img src="if_bridge_and_tap.svg" alt="if_bridge and tap" title="if_bridge and tap"></p>
<p>It was not so efficiently.</p>
<p>We test on N4100 CPU:</p>
<table>
  <thead>
      <tr>
          <th>Client</th>
          <th>Server</th>
          <th>Speed</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>vm2 (Debian)</td>
          <td>vm0 (ImmortalWrt)</td>
          <td>2.11 Gbits/sec</td>
      </tr>
      <tr>
          <td>vm2 (Debian)</td>
          <td>vm1 (Debian)</td>
          <td>2.17 Gbits/sec</td>
      </tr>
      <tr>
          <td>vm2 (Debian)</td>
          <td>host (FreeBSD)</td>
          <td>1.35 Gbits/sec</td>
      </tr>
  </tbody>
</table>
<p>Our config:</p>






<pre tabindex="0"><code>$ cat /etc/rc.conf
cloned_interfaces=&#34;bridge0 tap0&#34;
ifconfig_bridge0=&#34;dhcp addm em0 addm tap0&#34;
ifconfig_em0=&#34;up&#34;</code></pre>






<pre tabindex="0"><code>$ cat /boot/loader.conf
if_bridge_load=&#34;YES&#34;
if_tap_load=&#34;YES&#34;</code></pre>






<pre tabindex="0"><code>$ cat /pool/vm/openwrt/openwrt.conf 
loader=&#34;uefi&#34;
cpu=2
memory=512M
network0_type=&#34;e1000&#34;
network0_switch=&#34;public&#34;
network0_device=&#34;tap0&#34;
disk0_type=&#34;ahci-hd&#34;
disk0_name=&#34;immortalwrt-23.05.4-x86-64-generic-ext4-combined-efi.img&#34;</code></pre>
<h2 id="2-more-modern-way">2. More modern way</h2>
<p><a href="https://man.freebsd.org/cgi/man.cgi?query=vale&amp;sektion=4&amp;manpath=FreeBSD+14.0-RELEASE">vale</a> and <a href="https://www.freshports.org/net/vether-kmod">vether</a>.</p>
<p>Of course there is <a href="https://man.freebsd.org/cgi/man.cgi?query=epair&amp;manpath=FreeBSD+14.1-RELEASE">epair</a> that we can connect between host and vm.</p>
<p><img src="vale_and_vether.svg" alt="vale and vether" title="vale and vether"></p>
<p>We test on N4100 CPU:</p>
<table>
  <thead>
      <tr>
          <th>Client</th>
          <th>Server</th>
          <th>Speed</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>vm2 (Debian)</td>
          <td>vm0 (ImmortalWrt)</td>
          <td>3.50 Gbits/sec</td>
      </tr>
      <tr>
          <td>vm2 (Debian)</td>
          <td>vm1 (Debian)</td>
          <td>5.30 Gbits/sec</td>
      </tr>
      <tr>
          <td>vm2 (Debian)</td>
          <td>host (FreeBSD)</td>
          <td>1.48 Gbits/sec</td>
      </tr>
  </tbody>
</table>
<p>Our config:</p>






<pre tabindex="0"><code>$ cat /etc/rc.conf
defaultrouter=&#34;192.168.1.1&#34;
cloned_interfaces=&#34;vether0&#34;
ifconfig_vether0=&#34;192.168.1.2/24 up&#34;</code></pre>






<pre tabindex="0"><code>$ cat /boot/loader.conf
if_vether_load=&#34;YES&#34; </code></pre>






<pre tabindex="0"><code>$ cat /pool/vm/openwrt/openwrt.conf 
loader=&#34;uefi&#34;
cpu=2
memory=512M
network0_type=&#34;e1000&#34;
network0_switch=&#34;public&#34;
disk0_type=&#34;ahci-hd&#34;
disk0_name=&#34;immortalwrt-23.05.4-x86-64-generic-ext4-combined-efi.img&#34;</code></pre>
<p>Need to run following script everytime after openwrt vm start (connect host to vm). If you have vm-bhyve you can put it to <code>prestart</code>, remember to <code>chmod +x</code> for it.</p>
<p><code>valectl -h vale-name:vether-name</code>, for example <code>valectl -h vale0:vether0</code>.</p>
<h2 id="3-the-most-complex-thing">3. The most complex thing</h2>
<p><a href="https://man.freebsd.org/cgi/man.cgi?query=netgraph&amp;sektion=4&amp;manpath=FreeBSD+14.1-RELEASE">netgraph</a>.</p>
<p><img src="ng_bridge_and_ng_eiface.svg" alt="netgraph ng_bridge ng_eiface" title="netgraph ng_bridge ng_eiface"></p>
<p>We test on N4100 CPU:</p>
<table>
  <thead>
      <tr>
          <th>Client</th>
          <th>Server</th>
          <th>Speed</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>vm2 (Debian)</td>
          <td>vm0 (ImmortalWrt)</td>
          <td>2.41 Gbits/sec</td>
      </tr>
      <tr>
          <td>vm2 (Debian)</td>
          <td>vm1 (Debian)</td>
          <td>2.47 Gbits/sec</td>
      </tr>
      <tr>
          <td>vm2 (Debian)</td>
          <td>host (FreeBSD)</td>
          <td>2.27 Gbits/sec</td>
      </tr>
  </tbody>
</table>
<p><code>ng_bridge</code> is much faster than <code>vether</code> in this case: vm to host.</p>
<p>We think we could keep VALE as our vm switch, and replace vether by netgraph or epair?</p>
<p>netgraph as host virtual interface and vale as bridge, report bad pkt??? something was not right. we will look into it further and do it manually.</p>
<table>
  <thead>
      <tr>
          <th>Client</th>
          <th>Server</th>
          <th>Speed</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>vm2 (Debian)</td>
          <td>vm0 (ImmortalWrt)</td>
          <td>3.49 Gbits/sec</td>
      </tr>
      <tr>
          <td>vm2 (Debian)</td>
          <td>vm1 (Debian)</td>
          <td>5.25 Gbits/sec</td>
      </tr>
      <tr>
          <td>vm2 (Debian)</td>
          <td>host (FreeBSD)</td>
          <td>1.46 Gbits/sec</td>
      </tr>
  </tbody>
</table>
<p>Ref:</p>
<ul>
<li><a href="https://gitr.daemon.contact/tools/tree/rc.d/ngbridge">rc.d/ngbridge</a></li>
<li><a href="https://forums.freebsd.org/threads/netgraph-bridge.85551">Netgraph Bridge</a></li>
</ul>






<pre tabindex="0"><code>$ cat /boot/loader.conf
ng_eiface_load=&#34;YES&#34;
ng_bridge_load=&#34;YES&#34;
ng_ether_load=&#34;YES&#34;</code></pre>






<pre tabindex="0"><code>$ cat /etc/rc.conf
ngbridge_enable=&#34;YES&#34;
ngbridge_names=&#34;lan&#34;
ngbridge_lan_eifaces=&#34;nge_1u&#34;
ngbridge_nge_1u_mac=&#34;00:37:92:01:02:02&#34;
ngbridge_nge_1u_addr_num=&#34;1&#34;
ngbridge_nge_1u_addr_1=&#34;inet 192.168.1.2/24&#34;
ngbridge_lan_eifaces_keep=&#34;nge_1u&#34;
ngbridge_lan_route_num=1
ngbridge_lan_route_1=&#34;-net default 192.168.1.1&#34;
ngbridge_lan_vlans=&#34;NO&#34;</code></pre>






<pre tabindex="0"><code>$ cat /pool/vm/openwrt/openwrt.conf 
loader=&#34;uefi&#34;
cpu=2
memory=512M
network0_type=&#34;e1000&#34;
network0_switch=&#34;lanbridge&#34;
disk0_type=&#34;ahci-hd&#34;
disk0_name=&#34;immortalwrt-23.05.4-x86-64-generic-ext4-combined-efi.img&#34;</code></pre>
]]></content:encoded></item><item><title>Ghi chép về việc triển khai nhúng live streaming camera video lên website</title><link>https://tuanbui.net/2024/11/01/ghi-chep-live-streaming-camera-video-website/</link><pubDate>Fri, 01 Nov 2024 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2024/11/01/ghi-chep-live-streaming-camera-video-website/</guid><description>&lt;p>BÀI ĐANG VIẾT&lt;/p>
&lt;p>Đây là những ghi chép trong quá trình triển khai nhúng live streaming camera video lên website &lt;a href="https://trangtraihuounai.com">trangtraihuounai.com&lt;/a>.&lt;/p>
&lt;h2 id="1-yêu-cầu">1. Yêu cầu&lt;/h2>
&lt;p>Không quá nhiều điểm đặc biệt:&lt;/p>
&lt;ul>
&lt;li>Nhúng livestream camera của trang trại lên website&lt;/li>
&lt;li>Hệ thống đơn giản, có thể dễ dàng mở rộng khi cần thiết&lt;/li>
&lt;/ul>
&lt;h2 id="2-ý-tưởng">2. Ý tưởng&lt;/h2>
&lt;p>Gọn nhẹ nhất có thể:&lt;/p>
&lt;ul>
&lt;li>Sử dụng máy tính all-in-one cho tất cả các tác vụ&lt;/li>
&lt;li>Lấy luồng RTSP từ camera phát lại bằng &lt;a href="https://github.com/AlexxIT/go2rtc">go2rtc&lt;/a>&lt;/li>
&lt;li>Nhúng trực tiếp vào website hoặc phát kênh Youtube?&lt;/li>
&lt;/ul>
&lt;h3 id="21-thiết-bị-thử-nghiệm">2.1. Thiết bị thử nghiệm&lt;/h3>
&lt;p>Đang có sẵn các thứ sau:&lt;/p></description><content:encoded><![CDATA[<p>BÀI ĐANG VIẾT</p>
<p>Đây là những ghi chép trong quá trình triển khai nhúng live streaming camera video lên website <a href="https://trangtraihuounai.com">trangtraihuounai.com</a>.</p>
<h2 id="1-yêu-cầu">1. Yêu cầu</h2>
<p>Không quá nhiều điểm đặc biệt:</p>
<ul>
<li>Nhúng livestream camera của trang trại lên website</li>
<li>Hệ thống đơn giản, có thể dễ dàng mở rộng khi cần thiết</li>
</ul>
<h2 id="2-ý-tưởng">2. Ý tưởng</h2>
<p>Gọn nhẹ nhất có thể:</p>
<ul>
<li>Sử dụng máy tính all-in-one cho tất cả các tác vụ</li>
<li>Lấy luồng RTSP từ camera phát lại bằng <a href="https://github.com/AlexxIT/go2rtc">go2rtc</a></li>
<li>Nhúng trực tiếp vào website hoặc phát kênh Youtube?</li>
</ul>
<h3 id="21-thiết-bị-thử-nghiệm">2.1. Thiết bị thử nghiệm</h3>
<p>Đang có sẵn các thứ sau:</p>
<ul>
<li>Mini PC N4100: 4x1GB Ethernet</li>
<li>PoE Switch không biết tên</li>
<li>Imou Cruiser 2 (5MP Outdoor Wi-Fi P&amp;T Camera)</li>
</ul>
<p>Thay thế thiết bị khác sau khi chạy thử:</p>
<ul>
<li>All-in-one box: HP / Dell</li>
<li>Camera: Dahua / Hikvision</li>
</ul>
<h3 id="22-sơ-đồ-hệ-thống">2.2. Sơ đồ hệ thống</h3>
<p><img src="system_design.svg" alt="Sơ đồ hệ thống" title="Sơ đồ hệ thống"></p>
<p>Sẽ tách router trong tương lai, hiện tại chạy <a href="https://github.com/immortalwrt/immortalwrt">ImmortalWrt</a> trên môi trường ảo hóa.</p>
<h3 id="23-các-bước-triển-khai">2.3. Các bước triển khai</h3>
<p>Task List</p>
<ul>
<li><input checked="" disabled="" type="checkbox"> Cài đặt FreeBSD box</li>
<li><input checked="" disabled="" type="checkbox"> Cấu hình hệ thống mạng sẵn sàng cho máy ảo</li>
<li><input checked="" disabled="" type="checkbox"> Cài đặt máy ảo chạy ImmortalWrt làm router</li>
<li><input checked="" disabled="" type="checkbox"> Cấu hình ImmortalWrt, quy hoạch VLAN</li>
<li><input checked="" disabled="" type="checkbox"> Cài đặt máy ảo chạy Debian làm docker host</li>
<li><input checked="" disabled="" type="checkbox"> Cài đặt nginx làm reverse proxy, nat port 80/443 từ router</li>
<li><input checked="" disabled="" type="checkbox"> Cài đặt go2rtc lấy luồng RTSP / ONVIF từ camera</li>
<li><input checked="" disabled="" type="checkbox"> Cài đặt nginx làm web server, nhúng stream vào trang web</li>
<li><input checked="" disabled="" type="checkbox"> Chuyển thiết bị đến trang trại, quay pppoe mở port 80/443</li>
<li><input disabled="" type="checkbox"> Chạy thử trong 30 ngày</li>
</ul>
<h2 id="3-các-vấn-đề-gặp-phải">3. Các vấn đề gặp phải</h2>
<ul>
<li>high latency from cameras to web media player: check bridge+tap on bhyve, maybe passthru all nics and use something like vether / epair / vale / netgraph.. done.
<ul>
<li>Deploy TURN server and stream by WebRTC, much better latency</li>
</ul>
</li>
<li>transcoding video requires high CPU load, checked bhyve GPU passthru but it didn&rsquo;t work. moved to Linux instead of FreeBSD. needed something like CUDA</li>
<li>can not pull rtsp stream from EZVIZ camera, they use their own protocol. best camera brand for this job: Dahua, Imou</li>
<li>security risk when expose go2rtc API
<ul>
<li>Deploy a mediamtx WebRTC endpoint, expose via a reverse proxy</li>
</ul>
</li>
</ul>
<h2 id="4-new-proposal">4. New proposal</h2>
<h3 id="41-network">4.1. Network</h3>
<p>Router on baremetal:</p>
<ul>
<li>HINLINK OPC-H28K Board run KWrt</li>
<li>DHCP Static Mappings for services and cameras</li>
</ul>
<h3 id="42-server">4.2. Server</h3>
<p>Hardware:</p>
<ul>
<li>PC: HP EliteDesk 800 G4 SFF Business PC
<ul>
<li><a href="http://h10032.www1.hp.com/ctg/Manual/c06413250.pdf">Hardware Reference Guide</a></li>
<li><a href="http://h10032.www1.hp.com/ctg/Manual/c06472102.pdf">Maintenance and Service Guide</a></li>
<li><a href="http://h10032.www1.hp.com/ctg/Manual/c06696065.pdf">HP PC Commercial BIOS (UEFI) Setup</a></li>
<li><a href="http://h10032.www1.hp.com/ctg/Manual/c06525450.pdf">Interactive BIOS simulator</a></li>
</ul>
</li>
<li>Graphics card: NVIDIA Quadro P600
<ul>
<li><a href="https://www.nvidia.com/content/dam/en-zz/Solutions/design-visualization/productspage/quadro/quadro-desktop/quadro-pascal-p600-data-sheet-us-nv-704532-r1.pdf">Datasheet</a></li>
</ul>
</li>
</ul>
<p>Software:</p>
<ul>
<li>Operating System
<ul>
<li>Ubuntu 24.04 (Noble Numbat)</li>
<li>HP BIOS config
<ul>
<li>Advanced -&gt; Secure Boot Configuration -&gt; Legacy Support Disable and Secure Boot Enable</li>
</ul>
</li>
<li>Install detail
<ul>
<li>Ubuntu Server (minimised)</li>
<li>No LVM</li>
</ul>
</li>
<li>GPU Driver
<ul>
<li><a href="https://docs.nvidia.com/datacenter/tesla/driver-installation-guide">NVIDIA Driver Installation Guide for Linux</a>: use legacy kernel module flavor <code>cuda-drivers</code> for this card</li>
<li><a href="https://docs.nvidia.com/cuda/cuda-installation-guide-linux">NVIDIA CUDA Installation Guide for Linux</a></li>
<li><a href="https://github.com/NVIDIA/nvidia-container-toolkit">NVIDIA Container Toolkit</a></li>
</ul>
</li>
</ul>
</li>
<li>Virtualization
<ul>
<li>Docker
<ul>
<li><a href="https://github.com/louislam/dockge">Dockge</a></li>
</ul>
</li>
<li>KVM</li>
</ul>
</li>
<li>Streaming
<ul>
<li><a href="https://github.com/AlexxIT/go2rtc">go2rtc</a></li>
<li><a href="https://github.com/bluenviron/mediamtx">MediaMTX</a></li>
</ul>
</li>
<li>DDNS
<ul>
<li><a href="https://github.com/TimothyYe/godns">GoDNS</a></li>
</ul>
</li>
<li>STUN/TURN
<ul>
<li><a href="https://github.com/coturn/coturn">Coturn</a></li>
<li><a href="https://github.com/adnanh/webhook">webhook</a> for auto restart turn server when public ip changed</li>
</ul>
</li>
<li>Reverse proxy
<ul>
<li><a href="https://github.com/tobychui/zoraxy">Zoraxy</a></li>
</ul>
</li>
<li>Website
<ul>
<li><a href="https://github.com/getgrav/grav">Grav</a></li>
<li><a href="https://hub.docker.com/r/shinsenter/grav">shinsenter/grav</a></li>
</ul>
</li>
<li>Remote access
<ul>
<li>Tailscale / WireGuard on a KVM virtual machine</li>
</ul>
</li>
</ul>
<h2 id="5-live-website">5. Live website</h2>
<p>(<a href="https://trangtraihuounai.com">https://trangtraihuounai.com</a>)</p>
]]></content:encoded></item><item><title>IP PBX call distributor VoIP Gateway GSM/3G/4G/5G for home and small office</title><link>https://tuanbui.net/2024/10/24/wireless-pbx-system-for-home-and-small-office/</link><pubDate>Thu, 24 Oct 2024 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2024/10/24/wireless-pbx-system-for-home-and-small-office/</guid><description>&lt;h1 id="proof-of-concept">PROOF OF CONCEPT&lt;/h1>
&lt;h2 id="1-requirement-and-concept">1. Requirement and concept&lt;/h2>
&lt;p>We&amp;rsquo;re building a PBX system for our business. It will handle:&lt;/p>
&lt;ul>
&lt;li>automatic distribute incoming calls&lt;/li>
&lt;li>call outside by specific phone numbers, automatic route to reduce call cost&lt;/li>
&lt;li>sms send api for OTP maybe?&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="flow-incoming-call.svg" alt="Flow Incoming Call" title="Flow Incoming Call">&lt;/p>
&lt;p>Since we don&amp;rsquo;t have any SIP trunk from our carriers, we will take incoming calls from regular sim cards.&lt;/p>
&lt;ul>
&lt;li>Disadvantage:
&lt;ul>
&lt;li>&lt;em>&lt;strong>Single-line&lt;/strong>&lt;/em>: only one concurrent call per SIM card at a time&lt;/li>
&lt;li>Need more work to handle system failure due to OS, power..&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Not sure:
&lt;ul>
&lt;li>Voice over LTE (VoLTE) are supported by all carriers.&lt;/li>
&lt;li>Voice over Wi‑Fi (VoWiFi) are supported by VinaPhone, Mobifone?&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>Call flow as bellow:
&lt;img src="gsm-pbx-dia.svg" alt="GSM PBX diagram" title="GSM PBX diagram">&lt;/p></description><content:encoded><![CDATA[<h1 id="proof-of-concept">PROOF OF CONCEPT</h1>
<h2 id="1-requirement-and-concept">1. Requirement and concept</h2>
<p>We&rsquo;re building a PBX system for our business. It will handle:</p>
<ul>
<li>automatic distribute incoming calls</li>
<li>call outside by specific phone numbers, automatic route to reduce call cost</li>
<li>sms send api for OTP maybe?</li>
</ul>
<p><img src="flow-incoming-call.svg" alt="Flow Incoming Call" title="Flow Incoming Call"></p>
<p>Since we don&rsquo;t have any SIP trunk from our carriers, we will take incoming calls from regular sim cards.</p>
<ul>
<li>Disadvantage:
<ul>
<li><em><strong>Single-line</strong></em>: only one concurrent call per SIM card at a time</li>
<li>Need more work to handle system failure due to OS, power..</li>
</ul>
</li>
<li>Not sure:
<ul>
<li>Voice over LTE (VoLTE) are supported by all carriers.</li>
<li>Voice over Wi‑Fi (VoWiFi) are supported by VinaPhone, Mobifone?</li>
</ul>
</li>
</ul>
<p>Call flow as bellow:
<img src="gsm-pbx-dia.svg" alt="GSM PBX diagram" title="GSM PBX diagram"></p>
<p>Our stack:</p>
<ul>
<li>Hardwares:
<ul>
<li>Mini PC / Router: ZOTAC ZBOX CI323 nano</li>
<li>2G/3G/4G/5G module: Quectel EC20CEFRG-MINIPCIE-C (Vietnam frequency bands: B1/B3/B8), PCIE to USB adapter (sim card slot included), antena</li>
<li>VoIP phone: haven&rsquo;t bought yet</li>
</ul>
</li>
<li>Softwares
<ul>
<li>OS: <a href="https://www.truenas.com/docs/scale/24.10">TrueNAS SCALE 24.10 - Electric Eel</a></li>
<li>VM: <a href="https://github.com/Jip-Hop/jailmaker">jailmaker</a></li>
<li>PBX: <a href="https://www.asterisk.org">Asterisk</a></li>
<li>Softphone: <a href="https://linphone.org">Linphone (Linux / iOS)</a>, <a href="https://github.com/baresip/baresip">Baresip (Android)</a>, <a href="https://www.microsip.org">MicroSIP (Windows)</a></li>
</ul>
</li>
</ul>
<h2 id="2-step-by-step">2. Step by step</h2>
<h3 id="21-asterisk-installation">2.1 Asterisk Installation</h3>
<ul>
<li>OS: Debian 12 (&ldquo;Bookworm&rdquo;)</li>
</ul>






<pre tabindex="0"><code>$ mkdir tmp
$ cd tmp/
$ wget https://downloads.asterisk.org/pub/telephony/asterisk/asterisk-22-current.tar.gz
$ tar zxvf asterisk-22-current.tar.gz
$ cd asterisk-22*/
$ sudo ./contrib/scripts/install_prereq install
$ ./configure
$ make menuselect</code></pre>
<p>Most of IP phones are support G722 and / or G729 codecs now.</p>
<ul>
<li>Under Add-ons:
<ul>
<li>Select [*] format_mp3</li>
</ul>
</li>
<li>Under Codec Translators:
<ul>
<li>Select [*] codec_opus</li>
<li>Select [*] codec_silk</li>
<li>Select [*] codec_siren7</li>
<li>Select [*] codec_siren14</li>
<li>Select [*] codec_g729a</li>
</ul>
</li>
<li>Under Core Sound Packages:
<ul>
<li>Deselect [*] CORE-SOUNDS-EN-GSM</li>
<li>Select [*] CORE-SOUNDS-EN-WAV</li>
<li>Select [*] CORE-SOUNDS-EN-G722</li>
<li>Select [*] CORE-SOUNDS-EN-G729</li>
</ul>
</li>
<li>Under Extras Sound Packages:
<ul>
<li>Select [*] EXTRA-SOUNDS-EN-WAV</li>
<li>Select [*] EXTRA-SOUNDS-EN-G722</li>
<li>Select [*] EXTRA-SOUNDS-EN-G729</li>
</ul>
</li>
</ul>






<pre tabindex="0"><code>$ make
$ sudo ./contrib/scripts/get_mp3_source.sh
$ sudo make install
$ sudo make config
$ sudo make samples
$ sudo mkdir /etc/asterisk/samples
$ sudo mv /etc/asterisk/*.*  /etc/asterisk/samples/
$ sudo asterisk -rvvvv</code></pre>
<p>Most important config files:</p>
<ul>
<li>extensions.conf</li>
<li>pjsip.conf</li>
</ul>
<h3 id="22-virtual-machine-vm1">2.2. Virtual machine vm1</h3>
<p>Purpose: VoIP GSM Gateway to handle voice call from and or to 3G/4G/5G MNOs.</p>
<ul>
<li>OS: Debian 12</li>
<li>IP: 192.168.1.11</li>
<li>Software: Asterisk</li>
<li>Driver: asterisk-chan-quectel</li>
</ul>
<h3 id="23-virtual-machine-vm0">2.3. Virtual machine vm0</h3>
<p>Purpose: Local PBX.</p>
<ul>
<li>OS: Debian 12</li>
<li>IP: 192.168.1.10</li>
<li>Software: MikoPBX</li>
</ul>
<h2 id="3-resource">3. Resource</h2>
<p>Softwares:</p>
<ul>
<li>Servers:
<ul>
<li><a href="https://www.asterisk.org">Asterisk</a>
<ul>
<li><a href="https://github.com/IchthysMaranatha/asterisk-chan-quectel">asterisk-chan-quectel</a></li>
<li><a href="https://github.com/wdoekes/asterisk-chan-dongle">asterisk-chan-dongle</a></li>
</ul>
</li>
<li><a href="https://signalwire.com/freeswitch">FreeSWITCH</a></li>
<li><a href="https://www.kamailio.org">Kamailio</a></li>
<li><a href="https://yate.ro">Yate</a></li>
<li><a href="https://routr.io">Routr</a></li>
<li><a href="https://www.freepbx.org">FreePBX</a></li>
<li><a href="https://www.mikopbx.com">MikoPBX</a></li>
<li><a href="https://github.com/mlan/docker-asterisk">docker-asterisk</a></li>
<li><a href="https://github.com/joni1802/asterisk-sound-generator">asterisk-sound-generator</a></li>
<li><a href="https://github.com/NTT123/light-speed">Light Speed</a> for text-to-speech</li>
</ul>
</li>
<li>Clients:
<ul>
<li><a href="https://github.com/baresip/baresip">Baresip</a></li>
<li><a href="https://linphone.org">Linphone</a></li>
<li><a href="https://jami.net">Jami</a></li>
<li><a href="https://www.microsip.org">MicroSIP</a></li>
</ul>
</li>
<li>Docs:
<ul>
<li><a href="https://docs.asterisk.org">Official Asterisk documentation</a></li>
<li><a href="https://www.youtube.com/asteriskvideos">Official Asterisk YouTube Channel</a></li>
<li><a href="https://a.co/d/6xQn7n8">Asterisk: The Definitive Guide</a></li>
</ul>
</li>
<li>Ref:
<ul>
<li><a href="https://blog.sparktour.me/posts/2022/10/08/quectel-ec20-asterisk-freepbx-gsm-gateway">使用EC20模块配合asterisk及freepbx实现短信转发和网络电话</a></li>
<li><a href="https://iam.lc/2024/01/how-to-get-rid-of-sim.ping">如何把SIM卡从手机中取出</a></li>
<li><a href="https://blog.naver.com/jhnotepad/223505876518">Quectel LTE 모뎀으로 Asterisk에서 KT, SKT, LG U+ VoLTE 전화 사용하기</a></li>
<li><a href="https://github.com/IchthysMaranatha/asterisk-chan-quectel/discussions/13">Simple test bed set-up</a></li>
<li><a href="https://voz.vn/t/thuc-trang-mang-di-dong-vien-thong-tai-viet-nam.900378">Thực trạng mạng di động viễn thông tại Việt Nam</a></li>
</ul>
</li>
</ul>
<h2 id="4-miscellaneous">4. Miscellaneous</h2>
<ul>
<li>Linux KVM is much more complicated than FreeBSD Bhyve so maybe we sould run FreeBSD host and Linux guest</li>
<li>Debian has no Asterisk in their repo, we moved our guest to Ubuntu since we don&rsquo;t want to complie Asterisk ourself</li>
<li>Run MikoPBX as a Local PBX, Asterisk as a trunk provider. We found that MikoPBX is better than FreePBX in our case.</li>
<li>Call quality seems not good enough, need to check <a href="https://t4rd15.github.io/asterisk-chan-quectel/#gain-control-and-jitter-buffer">jitter buffer</a> again</li>
<li><a href="https://github.com/RoEdAl/asterisk-chan-quectel">https://github.com/RoEdAl/asterisk-chan-quectel</a></li>
</ul>






<pre tabindex="0"><code>test@vm0:~$ cat /etc/asterisk/pjsip.conf 
;===============TRANSPORT

[system-udp]
type=transport
protocol=udp
bind=0.0.0.0

;===============TRUNK

[miko0]
type=aor
contact=sip:192.168.1.11:5060

[miko0]
type=endpoint
context=quectel-incoming
disallow=all
allow=ulaw
aors=miko0

[miko0]
type=identify
endpoint=miko0
match=192.168.1.11</code></pre>






<pre tabindex="0"><code>test@vm0:~$ cat /etc/asterisk/extensions.conf 
[quectel-incoming]
exten =&gt; _+84XXXXXXXXX,1,Dial(PJSIP/200@miko0)
exten =&gt; s,n,Hangup()</code></pre>
<p>edit <code> /etc/asterisk/indications.conf</code> on vm1</p>






<pre tabindex="0"><code>#!/bin/bash 

configPath=&#34;$1&#34; # Path to the original config file

sed -i &#39;s/country=ru/country=vn/g&#39; &#34;$configPath&#34;
cat &gt;&gt; &#34;$configPath&#34; &lt;&lt;EOF

[vn]
description = Vietnam
; Clone from China
ringcadence = 1000,4000
dial = 450
busy = 450/350,0/350
ring = 450/1000,0/4000
congestion = 450/700,0/700
callwaiting = 450/400,0/4000
dialrecall = 450
record = 950/400,0/10000
info = 450/100,0/100,450/100,0/100,450/100,0/100,450/400,0/400
stutter = 450+425
EOF</code></pre>
<h2 id="5-test">5. Test</h2>
<p>Number: 0918026858</p>
<ul>
<li><input checked="" disabled="" type="checkbox"> Voice</li>
<li><input disabled="" type="checkbox"> SMS</li>
<li><input disabled="" type="checkbox"> 4G internet?</li>
</ul>
]]></content:encoded></item><item><title>Mail Server for small businesses FreeBSD OpenSMTPD and Dovecot</title><link>https://tuanbui.net/2024/10/10/mail-server-for-small-businesses-freebsd-opensmtpd-and-dovecot/</link><pubDate>Thu, 10 Oct 2024 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2024/10/10/mail-server-for-small-businesses-freebsd-opensmtpd-and-dovecot/</guid><description>&lt;p>We&amp;rsquo;ve been running mail server for a quite of time. The last software was &lt;a href="https://github.com/stalwartlabs/mail-server">Stalwart Mail Server&lt;/a>, unfortunately it&amp;rsquo;s under heavy development. They move so fast then the maintain job is a pain.&lt;/p>
&lt;p>We decided to going back to a more mature solution.&lt;/p>
&lt;p>&lt;img src="OpenSMTPDrspamdDovecot.png" alt="OpenSMTPDrspamdDovecot" title="OpenSMTPDrspamdDovecot">&lt;/p>
&lt;h2 id="1-should-know">1. Should know&lt;/h2>
&lt;p>Some information that we think you should know when you want to run your own mail server.&lt;/p>
&lt;h3 id="11-protocols">1.1. Protocols:&lt;/h3>
&lt;ul>
&lt;li>&lt;a href="https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol">Simple Mail Transfer Protocol (SMTP)&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://en.wikipedia.org/wiki/Internet_Message_Access_Protocol">Internet Message Access Protocol (IMAP)&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://en.wikipedia.org/wiki/Local_Mail_Transfer_Protocol">Local Mail Transfer Protocol (LMTP)&lt;/a>&lt;/li>
&lt;/ul>
&lt;h3 id="12-formats">1.2. Formats:&lt;/h3>
&lt;ul>
&lt;li>&lt;a href="https://en.wikipedia.org/wiki/Maildir">Maildir&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://en.wikipedia.org/wiki/Mbox">Mbox&lt;/a>&lt;/li>
&lt;/ul>
&lt;h3 id="13-software-components">1.3. Software components:&lt;/h3>
&lt;ul>
&lt;li>&lt;a href="https://en.wikipedia.org/wiki/Message_transfer_agent">Message transfer agent (MTA)&lt;/a>: &lt;a href="https://www.proofpoint.com/us/products/email-protection/open-source-email-solution">Sendmail&lt;/a>, &lt;a href="https://www.exim.org">Exim&lt;/a>, &lt;a href="http://www.postfix.org">Postfix&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://en.wikipedia.org/wiki/Message_delivery_agent">Message delivery agent (MDA)&lt;/a>: &lt;a href="https://github.com/dovecot/core">Dovecot&lt;/a>, &lt;a href="https://github.com/cyrusimap/cyrus-imapd">Cyrus IMAP&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://en.wikipedia.org/wiki/Email_filtering">Email filtering&lt;/a>: &lt;a href="https://github.com/rspamd/rspamd">Rspamd&lt;/a>, &lt;a href="https://spamassassin.apache.org">SpamAssassin&lt;/a>, &lt;a href="https://github.com/Cisco-Talos/clamav">ClamAV&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://en.wikipedia.org/wiki/Webmail">Webmail&lt;/a>: &lt;a href="https://github.com/roundcube/roundcubemail">Roundcube&lt;/a>, &lt;a href="https://github.com/cypht-org/cypht">Cypht&lt;/a>, &lt;a href="https://github.com/the-djmaze/snappymail">SnappyMail&lt;/a>&lt;/li>
&lt;/ul>
&lt;h3 id="14-resources">1.4. Resources:&lt;/h3>
&lt;ul>
&lt;li>&lt;a href="https://github.com/jonathandion/awesome-emails">awesome-emails&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/Mindbaz/awesome-opensource-email">awesome-opensource-email&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="2-software-stack">2. Software stack&lt;/h2>
&lt;p>We&amp;rsquo;re running FreeBSD so we want to run our mail server on it. The solution need to be easy to deploy and maintain.&lt;/p></description><content:encoded><![CDATA[<p>We&rsquo;ve been running mail server for a quite of time. The last software was <a href="https://github.com/stalwartlabs/mail-server">Stalwart Mail Server</a>, unfortunately it&rsquo;s under heavy development. They move so fast then the maintain job is a pain.</p>
<p>We decided to going back to a more mature solution.</p>
<p><img src="OpenSMTPDrspamdDovecot.png" alt="OpenSMTPDrspamdDovecot" title="OpenSMTPDrspamdDovecot"></p>
<h2 id="1-should-know">1. Should know</h2>
<p>Some information that we think you should know when you want to run your own mail server.</p>
<h3 id="11-protocols">1.1. Protocols:</h3>
<ul>
<li><a href="https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol">Simple Mail Transfer Protocol (SMTP)</a></li>
<li><a href="https://en.wikipedia.org/wiki/Internet_Message_Access_Protocol">Internet Message Access Protocol (IMAP)</a></li>
<li><a href="https://en.wikipedia.org/wiki/Local_Mail_Transfer_Protocol">Local Mail Transfer Protocol (LMTP)</a></li>
</ul>
<h3 id="12-formats">1.2. Formats:</h3>
<ul>
<li><a href="https://en.wikipedia.org/wiki/Maildir">Maildir</a></li>
<li><a href="https://en.wikipedia.org/wiki/Mbox">Mbox</a></li>
</ul>
<h3 id="13-software-components">1.3. Software components:</h3>
<ul>
<li><a href="https://en.wikipedia.org/wiki/Message_transfer_agent">Message transfer agent (MTA)</a>: <a href="https://www.proofpoint.com/us/products/email-protection/open-source-email-solution">Sendmail</a>, <a href="https://www.exim.org">Exim</a>, <a href="http://www.postfix.org">Postfix</a></li>
<li><a href="https://en.wikipedia.org/wiki/Message_delivery_agent">Message delivery agent (MDA)</a>: <a href="https://github.com/dovecot/core">Dovecot</a>, <a href="https://github.com/cyrusimap/cyrus-imapd">Cyrus IMAP</a></li>
<li><a href="https://en.wikipedia.org/wiki/Email_filtering">Email filtering</a>: <a href="https://github.com/rspamd/rspamd">Rspamd</a>, <a href="https://spamassassin.apache.org">SpamAssassin</a>, <a href="https://github.com/Cisco-Talos/clamav">ClamAV</a></li>
<li><a href="https://en.wikipedia.org/wiki/Webmail">Webmail</a>: <a href="https://github.com/roundcube/roundcubemail">Roundcube</a>, <a href="https://github.com/cypht-org/cypht">Cypht</a>, <a href="https://github.com/the-djmaze/snappymail">SnappyMail</a></li>
</ul>
<h3 id="14-resources">1.4. Resources:</h3>
<ul>
<li><a href="https://github.com/jonathandion/awesome-emails">awesome-emails</a></li>
<li><a href="https://github.com/Mindbaz/awesome-opensource-email">awesome-opensource-email</a></li>
</ul>
<h2 id="2-software-stack">2. Software stack</h2>
<p>We&rsquo;re running FreeBSD so we want to run our mail server on it. The solution need to be easy to deploy and maintain.</p>
<ul>
<li><a href="https://www.freebsd.org">FreeBSD</a> (operating system)</li>
<li><a href="https://www.opensmtpd.org">OpenSMTPD</a> (SMTP)</li>
<li><a href="https://www.dovecot.org">Dovecot</a> (IMAP)</li>
<li><a href="https://rspamd.com">Rspamd</a> (spam filtering)</li>
</ul>
<p>For small businesses we will not store usernames and passwords in LDAP or SQL databases, we store such information in flat-file databases.</p>
<h3 id="21-freebsd-operating-system">2.1. <a href="https://www.freebsd.org">FreeBSD</a> (operating system)</h3>
<p>There is nothing to say. FreeBSD is quite boring, it just works ^^.</p>
<p>We&rsquo;re going to run our mail server in a FreeBSD jail (managed by <a href="https://github.com/DtxdF/AppJail">AppJail</a>). We already have <a href="https://www.haproxy.org">HAProxy</a> as our Load Balancer.</p>
<p>! FreeBSD is a good operating system. Please <a href="https://freebsdfoundation.org/donate">donate</a> to their work.</p>
<p><img src="FreeBSD_Hz.svg" alt="FreeBSD_Hz" title="FreeBSD_Hz"></p>
<p>We just need to make sure our FreeBSD server is up to date.</p>






<pre tabindex="0"><code># pkg update
# pkg upgrade</code></pre>
<p>Create <code>vmail</code> user and <code>vmail</code> group. This is the user/group that’s used to access the mails.</p>






<pre tabindex="0"><code># pw useradd vmail -u 5000 -d /home/vmail -s /usr/sbin/nologin -m</code></pre>
<p>Get a free TLS/SSL certificate for your domain from a certificate authority (<a href="https://zerossl.com">ZeroSSL</a>, <a href="https://letsencrypt.org">Let&rsquo;s Encrypt</a>..) by <a href="https://github.com/acmesh-official/acme.sh">acme.sh</a> or <a href="https://github.com/certbot/certbot">Certbot</a>.</p>
<h3 id="22-opensmtpd-smtp">2.2. <a href="https://www.opensmtpd.org">OpenSMTPD</a> (SMTP)</h3>
<p>We consider Postfix which is more popular but we found that OpenSMTPD is easier to config so we will choose it as our <a href="https://en.wikipedia.org/wiki/Message_transfer_agent">MTA</a>.</p>
<p>We love the config syntax, it remind us about <a href="https://www.openbsd.org/faq/pf/index.html">PF</a>.</p>
<p><img src="opensmtpd.png" alt="opensmtpd" title="opensmtpd"></p>
<h4 id="221-install-opensmtpd">2.2.1. Install OpenSMTPD</h4>






<pre tabindex="0"><code># pkg install opensmtpd opensmtpd-extras</code></pre>
<h4 id="222-config-opensmtpd">2.2.2. Config OpenSMTPD</h4>
<h5 id="2221-smtpdconf">2.2.2.1. smtpd.conf</h5>
<p>Please read:</p>
<ul>
<li><a href="https://man.openbsd.org/smtpd.conf">smtpd.conf — SMTP daemon configuration file</a></li>
</ul>
<p>Please read it carefully and make your own config file. It&rsquo;s very important.</p>
<p>Modify your file <code>/usr/local/etc/mail/smtpd.conf</code>:</p>
<h6 id="22211-tlsssl-certificate">2.2.2.1.1. TLS/SSL certificate</h6>
<p>declare your certificate as follow, pkiname named <code>mail.example.com</code>:</p>






<pre tabindex="0"><code>pki mail.example.com cert &#34;/usr/local/etc/certs/example.com/fullchain.pem&#34;
pki mail.example.com key &#34;/usr/local/etc/certs/example.com/key.pem</code></pre>
<h6 id="22212-tables">2.2.2.1.2. Tables</h6>
<p>declare your tables</p>






<pre tabindex="0"><code>table aliases file:/usr/local/etc/mail/aliases
table virtuals file:/usr/local/etc/mail/virtuals
table domains file:/usr/local/etc/mail/domains
table credentials file:/usr/local/etc/mail/credentials
table secrets file:/usr/local/etc/mail/secrets
table passwds file:/usr/local/etc/mail/passwds</code></pre>
<p>We will have: <code>aliases</code>, <code>virtuals</code>, <code>domains</code>, <code>credentials</code>, <code>secrets</code> and <code>passwds</code>.</p>
<p>See OpenSMTPD tables below for more information.</p>
<h6 id="22213-bind">2.2.2.1.3. Bind</h6>






<pre tabindex="0"><code># STARTTLS port 25
listen on 0.0.0.0 port 25 tls pki mail.example.com
# SMTPS port 465
listen on 0.0.0.0 port 465 smtps pki mail.example.com auth &lt;credentials&gt;
# SUBMISSION port 587
listen on 0.0.0.0 port 587 tls-require pki mail.example.com auth &lt;credentials&gt;</code></pre>
<p>Listen to IPv4 only <code>0.0.0.0</code>.</p>
<p>For SMTPS (port 465),  SUBMISSION (port 587) users must provide their credentials. TLS is mandatory for both, pkiname = <code>mail.example.com</code>.</p>
<p>For SMTP (port 25) we will not require it because we want our server receive email from other MTA (other mail server). TLS is not mandatory, pkiname = <code>mail.example.com</code>.</p>
<h6 id="22214-define-actions">2.2.2.1.4. Define actions</h6>






<pre tabindex="0"><code>action &#34;recv&#34; lmtp &#34;/var/run/dovecot/lmtp&#34; rcpt-to virtual &lt;virtuals&gt;
action &#34;send&#34; relay host smtp+tls://mailjet@in-v3.mailjet.com auth &lt;secrets&gt;</code></pre>
<p>For simple use case, we define only 2 &ldquo;action&quot;s. For <code>receive</code> and <code>send</code> emails.</p>
<p><code>recv</code> deliver emails to <code>dovecot</code> via <code>lmtp</code> socket.</p>
<p><code>send</code> deliver emails to mailjet SMPT relay.</p>
<h6 id="22214-define-matchs">2.2.2.1.4. Define matchs</h6>






<pre tabindex="0"><code>match from any for domain &lt;domains&gt; action &#34;recv&#34;
match from any auth for any action &#34;send&#34;</code></pre>
<p>For any emails to our defined domains, OpenSMTPD will do action <code>recv</code>, which is deliver to Dovecot.</p>
<p>For any email from authenticated users to any where, OpenSMTPD will do action <code>send</code>, which is deliver to mailjet relay.</p>
<details>
<summary>MY CONFIG</summary>
<pre>
<code>
root@mailserver:~ # cat /usr/local/etc/mail/smtpd.conf
#	$OpenBSD: smtpd.conf,v 1.10 2018/05/24 11:40:17 gilles Exp $

# This is the smtpd server system-wide configuration file.
# See smtpd.conf(5) for more information.

# table aliases file:/etc/mail/aliases

# To also accept external mail over IPv4 or IPv6,
# respectively replace "listen on localhost" with:
#
# listen on 0.0.0.0
# listen on ::
# listen on localhost

# action "local" maildir alias &lt;aliases>
# action "relay" relay

# Uncomment the following to accept external mail for domain "example.org"
#
# match from any for domain "example.org" action "local"
# match for local action "local"
# match from local for any action "relay"

# USER
# CONFIG
# BELOW
# Public key infrastructure
pki mail.example.com cert "/usr/local/etc/certs/example.com/fullchain.pem"
pki mail.example.com key "/usr/local/etc/certs/example.com/key.pem"

# Tables
table aliases file:/usr/local/etc/mail/aliases
table virtuals file:/usr/local/etc/mail/virtuals
table domains file:/usr/local/etc/mail/domains
table credentials file:/usr/local/etc/mail/credentials
table secrets file:/usr/local/etc/mail/secrets
table passwds file:/usr/local/etc/mail/passwds

# STARTTLS port 25
listen on 0.0.0.0 port 25 tls pki mail.example.com
# SMTPS port 465
listen on 0.0.0.0 port 465 smtps pki mail.example.com auth &lt;credentials>
# SUBMISSION port 587
listen on 0.0.0.0 port 587 tls-require pki mail.example.com auth &lt;credentials>

action "recv" lmtp "/var/run/dovecot/lmtp" rcpt-to virtual &lt;virtuals>
# https://dev.mailjet.com/smtp-relay/configuration
action "send" relay host smtp+tls://mailjet@in-v3.mailjet.com auth &lt;secrets>

match from any for domain &lt;domains> action "recv"
match from any auth for any action "send"
</code>
</pre>
</details>
<h5 id="2222-opensmtpd-tables">2.2.2.2. OpenSMTPD tables</h5>
<table>
  <thead>
      <tr>
          <th>Table</th>
          <th><a href="https://man.openbsd.org/table">OpenSMTPD</a></th>
          <th><a href="https://doc.dovecot.org/2.3/configuration_manual/authentication/passwd_file">Dovecot</a></th>
          <th>Remark</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://man.openbsd.org/table#Aliasing_tables">aliases</a></td>
          <td>✔️</td>
          <td>-</td>
          <td>primary domain aliases</td>
      </tr>
      <tr>
          <td><a href="https://man.openbsd.org/table#Aliasing_tables">virtuals</a></td>
          <td>✔️</td>
          <td>-</td>
          <td>virtual domain mapping</td>
      </tr>
      <tr>
          <td><a href="https://man.openbsd.org/table#Domain_tables">domains</a></td>
          <td>✔️</td>
          <td>-</td>
          <td>lists of domains</td>
      </tr>
      <tr>
          <td><a href="https://man.openbsd.org/table#Credentials_tables">credentials</a></td>
          <td>✔️</td>
          <td>-</td>
          <td>mappings of credentials</td>
      </tr>
      <tr>
          <td><a href="https://man.openbsd.org/table#Credentials_tables">secrets</a></td>
          <td>✔️</td>
          <td>-</td>
          <td>credentials of external relays</td>
      </tr>
      <tr>
          <td><a href="https://doc.dovecot.org/2.3/configuration_manual/authentication/passwd_file">passwds</a></td>
          <td>-</td>
          <td>✔️</td>
          <td>Dovecot authentication</td>
      </tr>
  </tbody>
</table>
<p>OpenSMTPD and Dovecot can share a same passwd-like file as their authentication table (<code>credentials</code> or <code>passwds</code>). If you don&rsquo;t want separate authentication databases so just choose one, make sure its format compatible with both OpenSMTPD and Dovecot.</p>
<details>
<summary>FOR EXAMPLE</summary>
<pre>
one `credentials` file for both.
<code>
root@mailserver:~ # cat /usr/local/etc/mail/credentials 
john@example.com:$6$0UWgneRDOFLnWRnh$UbNpTu4UexIDRaI8hhFMnV8r7Z6x4y4fKH8q5AUD39seTX0EyiG124F7GZHdfxzhF87RpbWR7/A9FmZvR60MN0
</code>
</pre>
</details>
<h6 id="22221-aliases">2.2.2.2.1. aliases</h6>
<p>we simply copy from <code>/etc/mail/aliases</code> to <code>/usr/local/etc/mail/aliases</code>.</p>
<p>add <code>vmail: /dev/null</code> to the end of <code>/usr/local/etc/mail/aliases</code>.</p>
<details>
<summary>MY CONFIG</summary>
<pre>
<code>
root@mailserver:~ # cat /usr/local/etc/mail/aliases
#	@(#)aliases	5.3 (Berkeley) 5/24/90
#
#  Aliases in this file will NOT be expanded in the header from
#  Mail, but WILL be visible over networks.
#
#	>>>>>>>>>>	The program "newaliases" must be run after
#	>> NOTE >>	this file is updated for any changes to
#	>>>>>>>>>>	show through to sendmail.
#
#
# See also RFC 2142, `MAILBOX NAMES FOR COMMON SERVICES, ROLES
# AND FUNCTIONS', May 1997
# 	http://tools.ietf.org/html/rfc2142

# Pretty much everything else in this file points to "root", so
# you would do well in either reading root's mailbox or forwarding
# root's email from here.

# root:	john@example.com

# Basic system aliases -- these MUST be present
MAILER-DAEMON: postmaster
postmaster: root

# General redirections for pseudo accounts
_dhcp:	root
_pflogd: root
auditdistd:	root
bin:	root
bind:	root
daemon:	root
games:	root
hast:	root
kmem:	root
mailnull: postmaster
man:	root
news:	root
nobody:	root
operator: root
pop:	root
proxy:	root
smmsp:	postmaster
sshd:	root
system:	root
toor:	root
tty:	root
usenet: news
uucp:	root

# Well-known aliases -- these should be filled in!
# manager:
# dumper:

# BUSINESS-RELATED MAILBOX NAMES
# info:
# marketing:
# sales:
# support:

# NETWORK OPERATIONS MAILBOX NAMES
abuse:	root
# noc:		root
security:	root

# SUPPORT MAILBOX NAMES FOR SPECIFIC INTERNET SERVICES
ftp: 		root
ftp-bugs: 	ftp
# hostmaster: 	root
# webmaster: 	root
# www: 		webmaster

# NOTE: /var/msgs and /var/msgs/bounds must be owned by sendmail's
#	DefaultUser (defaults to mailnull) for the msgs alias to work.
#
# msgs: "| /usr/bin/msgs -s"

# bit-bucket: /dev/null
# dev-null: bit-bucket
vmail: /dev/null
</code>
</pre>
</details>
<h6 id="22222-virtuals">2.2.2.2.2. virtuals</h6>
<p>add <code>@: vmail</code> to <code>/usr/local/etc/mail/virtuals</code>.</p>
<details>
<summary>MY CONFIG</summary>
<pre>
<code>
root@mailserver:~ # cat /usr/local/etc/mail/virtuals
@: vmail
</code>
</pre>
</details>
<h6 id="22223-domains">2.2.2.2.3. domains</h6>
<p>add your domains line by line to <code>/usr/local/etc/mail/domains</code>.</p>
<details>
<summary>MY CONFIG</summary>
<pre>
<code>
root@mailserver:~ # cat /usr/local/etc/mail/domains
example.com
example.net
</code>
</pre>
</details>
<h6 id="22224-credentials">2.2.2.2.4. credentials</h6>
<p>get your password, hash it by <code>smtpctl</code> then write username and hashed password into <code>/usr/local/etc/mail/credentials</code>.</p>
<details>
<summary>MY CONFIG</summary>
<pre>
<code>
root@mailserver:~ # cat /usr/local/etc/mail/credentials
john@example.com: $6$0UWgneRDOFLnWRnh$UbNpTu4UexIDRaI8hhFMnV8r7Z6x4y4fKH8q5AUD39seTX0EyiG124F7GZHdfxzhF87RpbWR7/A9FmZvR60MN0
</code>
</pre>
</details>
<h6 id="22225-secrets">2.2.2.2.5. secrets</h6>
<p>supply mailjet relay account in <code>/usr/local/etc/mail/secrets</code>.</p>
<details>
<summary>MY CONFIG</summary>
<pre>
<code>
root@mailserver:~ # cat /usr/local/etc/mail/secrets
mailjet username:password
</code>
</pre>
</details>
<h6 id="22226-passwds">2.2.2.2.6. passwds</h6>
<p>see dovecot config <code>/usr/local/etc/mail/passwds</code>.</p>
<h6 id="22227-check-config">2.2.2.2.7. Check config</h6>
<p>Test our config:</p>






<pre tabindex="0"><code>root@mailserver:~ # smtpd -n
configuration OK</code></pre>
<details>
<summary>MY CONFIG</summary>
<pre>
<code>
root@mailserver:~ # cat /usr/local/etc/mail/passwds 
john@example.com:$6$0UWgneRDOFLnWRnh$UbNpTu4UexIDRaI8hhFMnV8r7Z6x4y4fKH8q5AUD39seTX0EyiG124F7GZHdfxzhF87RpbWR7/A9FmZvR60MN0::::::
catchall@example.com:$6$ltEXtOxtMOY/5VxD$Ad/7Cce0Rsk5EDMLcZTXBzsB.AxcumZwUHUVpWaSYohL99pjsOTYrLqK40DiYwSd6Mup8Be/Hoo6mZDhZ2CVc::::::
</code>
</pre>
</details>
<h3 id="23-dovecot-imap">2.3. <a href="https://www.dovecot.org">Dovecot</a> (IMAP)</h3>
<p>Of course Dovecot for our <a href="https://en.wikipedia.org/wiki/Message_delivery_agent">MDA</a>, the best we know.</p>
<p><img src="Dovecot-BLUE-2048x1463.png" alt="Dovecot-BLUE-2048x1463" title="Dovecot-BLUE-2048x1463"></p>
<h4 id="231-install-dovecot">2.3.1. Install Dovecot</h4>






<pre tabindex="0"><code># pkg install dovecot
# cp -R /usr/local/etc/dovecot/example-config/* /usr/local/etc/dovecot</code></pre>
<h4 id="232-config-dovecot">2.3.2. Config Dovecot</h4>
<h5 id="2321-dovecotconf">2.3.2.1. dovecot.conf</h5>
<p>Please read:</p>
<ul>
<li><a href="https://doc.dovecot.org/2.3/configuration_manual">Configuration Manual</a>.</li>
</ul>
<p>Edit file <code>/usr/local/etc/dovecot/dovecot.conf</code>, change / add as below:</p>
<p>We will need <code>imap</code> of course, and <code>lmtp</code> for listen to mail messages from OpenSMTPD.</p>






<pre tabindex="0"><code>protocols = imap lmtp</code></pre>
<p>We will not use IPv6 so listen only on IPv4.</p>






<pre tabindex="0"><code>listen = *</code></pre>
<details>
<summary>MY CONFIG</summary>
<pre>
<code>
root@mailserver:~ # cat /usr/local/etc/dovecot/dovecot.conf
## Dovecot configuration file

# If you're in a hurry, see http://wiki2.dovecot.org/QuickConfiguration

# "doveconf -n" command gives a clean output of the changed settings. Use it
# instead of copy&pasting files when posting to the Dovecot mailing list.

# '#' character and everything after it is treated as comments. Extra spaces
# and tabs are ignored. If you want to use either of these explicitly, put the
# value inside quotes, eg.: key = "# char and trailing whitespace  "

# Most (but not all) settings can be overridden by different protocols and/or
# source/destination IPs by placing the settings inside sections, for example:
# protocol imap { }, local 127.0.0.1 { }, remote 10.0.0.0/8 { }

# Default values are shown for each setting, it's not required to uncomment
# those. These are exceptions to this though: No sections (e.g. namespace {})
# or plugin settings are added by default, they're listed only as examples.
# Paths are also just examples with the real defaults being based on configure
# options. The paths listed here are for configure --prefix=/usr
# --sysconfdir=/usr/local/etc --localstatedir=/var

# Protocols we want to be serving.
#protocols = imap pop3 lmtp submission
protocols = imap lmtp

# A comma separated list of IPs or hosts where to listen in for connections. 
# "*" listens in all IPv4 interfaces, "::" listens in all IPv6 interfaces.
# If you want to specify non-default ports or anything more complex,
# edit conf.d/master.conf.
#listen = *, ::
listen = *

# Base directory where to store runtime data.
#base_dir = /var/run/dovecot/

# Name of this instance. In multi-instance setup doveadm and other commands
# can use -i instance_name> to select which instance is used (an alternative
# to -c &lt;config_path>). The instance name is also added to Dovecot processes
# in ps output.
#instance_name = dovecot

# Greeting message for clients.
#login_greeting = Dovecot ready.

# Space separated list of trusted network ranges. Connections from these
# IPs are allowed to override their IP addresses and ports (for logging and
# for authentication checks). disable_plaintext_auth is also ignored for
# these networks. Typically you'd specify your IMAP proxy servers here.
#login_trusted_networks =

# Space separated list of login access check sockets (e.g. tcpwrap)
#login_access_sockets = 

# With proxy_maybe=yes if proxy destination matches any of these IPs, don't do
# proxying. This isn't necessary normally, but may be useful if the destination
# IP is e.g. a load balancer's IP.
#auth_proxy_self =

# Show more verbose process titles (in ps). Currently shows user name and
# IP address. Useful for seeing who are actually using the IMAP processes
# (eg. shared mailboxes or if same uid is used for multiple accounts).
#verbose_proctitle = no

# Should all processes be killed when Dovecot master process shuts down.
# Setting this to "no" means that Dovecot can be upgraded without
# forcing existing client connections to close (although that could also be
# a problem if the upgrade is e.g. because of a security fix).
#shutdown_clients = yes

# If non-zero, run mail commands via this many connections to doveadm server,
# instead of running them directly in the same process.
#doveadm_worker_count = 0
# UNIX socket or host:port used for connecting to doveadm server
#doveadm_socket_path = doveadm-server

# Space separated list of environment variables that are preserved on Dovecot
# startup and passed down to all of its child processes. You can also give
# key=value pairs to always set specific settings.
#import_environment = TZ

##
## Dictionary server settings
##

# Dictionary can be used to store key=value lists. This is used by several
# plugins. The dictionary can be accessed either directly or though a
# dictionary server. The following dict block maps dictionary names to URIs
# when the server is used. These can then be referenced using URIs in format
# "proxy::&lt;name>".

dict {
  #quota = mysql:/usr/local/etc/dovecot/dovecot-dict-sql.conf.ext
}

# Most of the actual configuration gets included below. The filenames are
# first sorted by their ASCII value and parsed in that order. The 00-prefixes
# in filenames are intended to make it easier to understand the ordering.
!include conf.d/*.conf

# A config file can also tried to be included without giving an error if
# it's not found:
!include_try local.conf
</code>
</pre>
</details>
<h5 id="2322-10-mailconf">2.3.2.2. 10-mail.conf</h5>
<p>Please read:</p>
<ul>
<li><a href="https://doc.dovecot.org/2.3/configuration_manual/mail_location">Mail Location Settings</a>.</li>
</ul>
<p>Edit file <code>/usr/local/etc/dovecot/conf.d/10-mail.conf</code>, change / add as below:</p>
<p>We will use <code>Maildir</code> as our email format.</p>
<p><code>mail_location = maildir:~/Maildir</code></p>
<details>
<summary>MY CONFIG</summary>
<pre>
<code>
root@mailserver:~ # cat /usr/local/etc/dovecot/conf.d/10-mail.conf
##
## Mailbox locations and namespaces
##

# Location for users' mailboxes. The default is empty, which means that Dovecot
# tries to find the mailboxes automatically. This won't work if the user
# doesn't yet have any mail, so you should explicitly tell Dovecot the full
# location.
#
# If you're using mbox, giving a path to the INBOX file (eg. /var/mail/%u)
# isn't enough. You'll also need to tell Dovecot where the other mailboxes are
# kept. This is called the "root mail directory", and it must be the first
# path given in the mail_location setting.
#
# There are a few special variables you can use, eg.:
#
#   %u - username
#   %n - user part in user@domain, same as %u if there's no domain
#   %d - domain part in user@domain, empty if there's no domain
#   %h - home directory
#
# See doc/wiki/Variables.txt for full list. Some examples:
#
#   mail_location = maildir:~/Maildir
#   mail_location = mbox:~/mail:INBOX=/var/mail/%u
#   mail_location = mbox:/var/mail/%d/%1n/%n:INDEX=/var/indexes/%d/%1n/%n
#
# &lt;doc/wiki/MailLocation.txt>
#
mail_location = maildir:~/Maildir

# If you need to set multiple mailbox locations or want to change default
# namespace settings, you can do it by defining namespace sections.
#
# You can have private, shared and public namespaces. Private namespaces
# are for user's personal mails. Shared namespaces are for accessing other
# users' mailboxes that have been shared. Public namespaces are for shared
# mailboxes that are managed by sysadmin. If you create any shared or public
# namespaces you'll typically want to enable ACL plugin also, otherwise all
# users can access all the shared mailboxes, assuming they have permissions
# on filesystem level to do so.
namespace inbox {
  # Namespace type: private, shared or public
  #type = private

  # Hierarchy separator to use. You should use the same separator for all
  # namespaces or some clients get confused. '/' is usually a good one.
  # The default however depends on the underlying mail storage format.
  #separator = 

  # Prefix required to access this namespace. This needs to be different for
  # all namespaces. For example "Public/".
  #prefix = 

  # Physical location of the mailbox. This is in same format as
  # mail_location, which is also the default for it.
  #location =

  # There can be only one INBOX, and this setting defines which namespace
  # has it.
  inbox = yes

  # If namespace is hidden, it's not advertised to clients via NAMESPACE
  # extension. You'll most likely also want to set list=no. This is mostly
  # useful when converting from another server with different namespaces which
  # you want to deprecate but still keep working. For example you can create
  # hidden namespaces with prefixes "~/mail/", "~%u/mail/" and "mail/".
  #hidden = no

  # Show the mailboxes under this namespace with LIST command. This makes the
  # namespace visible for clients that don't support NAMESPACE extension.
  # "children" value lists child mailboxes, but hides the namespace prefix.
  #list = yes

  # Namespace handles its own subscriptions. If set to "no", the parent
  # namespace handles them (empty prefix should always have this as "yes")
  #subscriptions = yes

  # See 15-mailboxes.conf for definitions of special mailboxes.
}

# Example shared namespace configuration
#namespace {
  #type = shared
  #separator = /

  # Mailboxes are visible under "shared/user@domain/"
  # %%n, %%d and %%u are expanded to the destination user.
  #prefix = shared/%%u/

  # Mail location for other users' mailboxes. Note that %variables and ~/
  # expands to the logged in user's data. %%n, %%d, %%u and %%h expand to the
  # destination user's data.
  #location = maildir:%%h/Maildir:INDEX=~/Maildir/shared/%%u

  # Use the default namespace for saving subscriptions.
  #subscriptions = no

  # List the shared/ namespace only if there are visible shared mailboxes.
  #list = children
#}
# Should shared INBOX be visible as "shared/user" or "shared/user/INBOX"?
#mail_shared_explicit_inbox = no

# System user and group used to access mails. If you use multiple, userdb
# can override these by returning uid or gid fields. You can use either numbers
# or names. &lt;doc/wiki/UserIds.txt>
#mail_uid =
#mail_gid =

# Group to enable temporarily for privileged operations. Currently this is
# used only with INBOX when either its initial creation or dotlocking fails.
# Typically this is set to "mail" to give access to /var/mail.
#mail_privileged_group =

# Grant access to these supplementary groups for mail processes. Typically
# these are used to set up access to shared mailboxes. Note that it may be
# dangerous to set these if users can create symlinks (e.g. if "mail" group is
# set here, ln -s /var/mail ~/mail/var could allow a user to delete others'
# mailboxes, or ln -s /secret/shared/box ~/mail/mybox would allow reading it).
#mail_access_groups =

# Allow full filesystem access to clients. There's no access checks other than
# what the operating system does for the active UID/GID. It works with both
# maildir and mboxes, allowing you to prefix mailboxes names with eg. /path/
# or ~user/.
#mail_full_filesystem_access = no

# Dictionary for key=value mailbox attributes. This is used for example by
# URLAUTH and METADATA extensions.
#mail_attribute_dict =

# A comment or note that is associated with the server. This value is
# accessible for authenticated users through the IMAP METADATA server
# entry "/shared/comment". 
#mail_server_comment = ""

# Indicates a method for contacting the server administrator. According to
# RFC 5464, this value MUST be a URI (e.g., a mailto: or tel: URL), but that
# is currently not enforced. Use for example mailto:admin@example.com. This
# value is accessible for authenticated users through the IMAP METADATA server
# entry "/shared/admin".
#mail_server_admin = 

##
## Mail processes
##

# Don't use mmap() at all. This is required if you store indexes to shared
# filesystems (NFS or clustered filesystem).
#mmap_disable = no

# Rely on O_EXCL to work when creating dotlock files. NFS supports O_EXCL
# since version 3, so this should be safe to use nowadays by default.
#dotlock_use_excl = yes

# When to use fsync() or fdatasync() calls:
#   optimized (default): Whenever necessary to avoid losing important data
#   always: Useful with e.g. NFS when write()s are delayed
#   never: Never use it (best performance, but crashes can lose data)
#mail_fsync = optimized

# Locking method for index files. Alternatives are fcntl, flock and dotlock.
# Dotlocking uses some tricks which may create more disk I/O than other locking
# methods. NFS users: flock doesn't work, remember to change mmap_disable.
#lock_method = fcntl

# Directory where mails can be temporarily stored. Usually it's used only for
# mails larger than >= 128 kB. It's used by various parts of Dovecot, for
# example LDA/LMTP while delivering large mails or zlib plugin for keeping
# uncompressed mails.
#mail_temp_dir = /tmp

# Valid UID range for users, defaults to 500 and above. This is mostly
# to make sure that users can't log in as daemons or other system users.
# Note that denying root logins is hardcoded to dovecot binary and can't
# be done even if first_valid_uid is set to 0.
#first_valid_uid = 500
#last_valid_uid = 0

# Valid GID range for users, defaults to non-root/wheel. Users having
# non-valid GID as primary group ID aren't allowed to log in. If user
# belongs to supplementary groups with non-valid GIDs, those groups are
# not set.
#first_valid_gid = 1
#last_valid_gid = 0

# Maximum allowed length for mail keyword name. It's only forced when trying
# to create new keywords.
#mail_max_keyword_length = 50

# ':' separated list of directories under which chrooting is allowed for mail
# processes (ie. /var/mail will allow chrooting to /var/mail/foo/bar too).
# This setting doesn't affect login_chroot, mail_chroot or auth chroot
# settings. If this setting is empty, "/./" in home dirs are ignored.
# WARNING: Never add directories here which local users can modify, that
# may lead to root exploit. Usually this should be done only if you don't
# allow shell access for users. &lt;doc/wiki/Chrooting.txt>
#valid_chroot_dirs = 

# Default chroot directory for mail processes. This can be overridden for
# specific users in user database by giving /./ in user's home directory
# (eg. /home/./user chroots into /home). Note that usually there is no real
# need to do chrooting, Dovecot doesn't allow users to access files outside
# their mail directory anyway. If your home directories are prefixed with
# the chroot directory, append "/." to mail_chroot. &lt;doc/wiki/Chrooting.txt>
#mail_chroot = 

# UNIX socket path to master authentication server to find users.
# This is used by imap (for shared users) and lda.
#auth_socket_path = /var/run/dovecot/auth-userdb

# Directory where to look up mail plugins.
#mail_plugin_dir = /usr/lib/dovecot

# Space separated list of plugins to load for all services. Plugins specific to
# IMAP, LDA, etc. are added to this list in their own .conf files.
#mail_plugins = 

##
## Mailbox handling optimizations
##

# Mailbox list indexes can be used to optimize IMAP STATUS commands. They are
# also required for IMAP NOTIFY extension to be enabled.
#mailbox_list_index = yes

# Trust mailbox list index to be up-to-date. This reduces disk I/O at the cost
# of potentially returning out-of-date results after e.g. server crashes.
# The results will be automatically fixed once the folders are opened.
#mailbox_list_index_very_dirty_syncs = yes

# Should INBOX be kept up-to-date in the mailbox list index? By default it's
# not, because most of the mailbox accesses will open INBOX anyway.
#mailbox_list_index_include_inbox = no

# The minimum number of mails in a mailbox before updates are done to cache
# file. This allows optimizing Dovecot's behavior to do less disk writes at
# the cost of more disk reads.
#mail_cache_min_mail_count = 0

# When IDLE command is running, mailbox is checked once in a while to see if
# there are any new mails or other changes. This setting defines the minimum
# time to wait between those checks. Dovecot can also use inotify and
# kqueue to find out immediately when changes occur.
#mailbox_idle_check_interval = 30 secs

# Save mails with CR+LF instead of plain LF. This makes sending those mails
# take less CPU, especially with sendfile() syscall with Linux and FreeBSD.
# But it also creates a bit more disk I/O which may just make it slower.
# Also note that if other software reads the mboxes/maildirs, they may handle
# the extra CRs wrong and cause problems.
#mail_save_crlf = no

# Max number of mails to keep open and prefetch to memory. This only works with
# some mailbox formats and/or operating systems.
#mail_prefetch_count = 0

# How often to scan for stale temporary files and delete them (0 = never).
# These should exist only after Dovecot dies in the middle of saving mails.
#mail_temp_scan_interval = 1w

# How many slow mail accesses sorting can perform before it returns failure.
# With IMAP the reply is: NO [LIMIT] Requested sort would have taken too long.
# The untagged SORT reply is still returned, but it's likely not correct.
#mail_sort_max_read_count = 0

protocol !indexer-worker {
  # If folder vsize calculation requires opening more than this many mails from
  # disk (i.e. mail sizes aren't in cache already), return failure and finish
  # the calculation via indexer process. Disabled by default. This setting must
  # be 0 for indexer-worker processes.
  #mail_vsize_bg_after_count = 0
}

##
## Maildir-specific settings
##

# By default LIST command returns all entries in maildir beginning with a dot.
# Enabling this option makes Dovecot return only entries which are directories.
# This is done by stat()ing each entry, so it causes more disk I/O.
# (For systems setting struct dirent->d_type, this check is free and it's
# done always regardless of this setting)
#maildir_stat_dirs = no

# When copying a message, do it with hard links whenever possible. This makes
# the performance much better, and it's unlikely to have any side effects.
#maildir_copy_with_hardlinks = yes

# Assume Dovecot is the only MUA accessing Maildir: Scan cur/ directory only
# when its mtime changes unexpectedly or when we can't find the mail otherwise.
#maildir_very_dirty_syncs = no

# If enabled, Dovecot doesn't use the S=&lt;size> in the Maildir filenames for
# getting the mail's physical size, except when recalculating Maildir++ quota.
# This can be useful in systems where a lot of the Maildir filenames have a
# broken size. The performance hit for enabling this is very small.
#maildir_broken_filename_sizes = no

# Always move mails from new/ directory to cur/, even when the \Recent flags
# aren't being reset.
#maildir_empty_new = no

##
## mbox-specific settings
##

# Which locking methods to use for locking mbox. There are four available:
#  dotlock: Create &lt;mailbox>.lock file. This is the oldest and most NFS-safe
#           solution. If you want to use /var/mail/ like directory, the users
#           will need write access to that directory.
#  dotlock_try: Same as dotlock, but if it fails because of permissions or
#               because there isn't enough disk space, just skip it.
#  fcntl  : Use this if possible. Works with NFS too if lockd is used.
#  flock  : May not exist in all systems. Doesn't work with NFS.
#  lockf  : May not exist in all systems. Doesn't work with NFS.
#
# You can use multiple locking methods; if you do the order they're declared
# in is important to avoid deadlocks if other MTAs/MUAs are using multiple
# locking methods as well. Some operating systems don't allow using some of
# them simultaneously.
#mbox_read_locks = fcntl
#mbox_write_locks = dotlock fcntl

# Maximum time to wait for lock (all of them) before aborting.
#mbox_lock_timeout = 5 mins

# If dotlock exists but the mailbox isn't modified in any way, override the
# lock file after this much time.
#mbox_dotlock_change_timeout = 2 mins

# When mbox changes unexpectedly we have to fully read it to find out what
# changed. If the mbox is large this can take a long time. Since the change
# is usually just a newly appended mail, it'd be faster to simply read the
# new mails. If this setting is enabled, Dovecot does this but still safely
# fallbacks to re-reading the whole mbox file whenever something in mbox isn't
# how it's expected to be. The only real downside to this setting is that if
# some other MUA changes message flags, Dovecot doesn't notice it immediately.
# Note that a full sync is done with SELECT, EXAMINE, EXPUNGE and CHECK 
# commands.
#mbox_dirty_syncs = yes

# Like mbox_dirty_syncs, but don't do full syncs even with SELECT, EXAMINE,
# EXPUNGE or CHECK commands. If this is set, mbox_dirty_syncs is ignored.
#mbox_very_dirty_syncs = no

# Delay writing mbox headers until doing a full write sync (EXPUNGE and CHECK
# commands and when closing the mailbox). This is especially useful for POP3
# where clients often delete all mails. The downside is that our changes
# aren't immediately visible to other MUAs.
#mbox_lazy_writes = yes

# If mbox size is smaller than this (e.g. 100k), don't write index files.
# If an index file already exists it's still read, just not updated.
#mbox_min_index_size = 0

# Mail header selection algorithm to use for MD5 POP3 UIDLs when
# pop3_uidl_format=%m. For backwards compatibility we use apop3d inspired
# algorithm, but it fails if the first Received: header isn't unique in all
# mails. An alternative algorithm is "all" that selects all headers.
#mbox_md5 = apop3d

##
## mdbox-specific settings
##

# Maximum dbox file size until it's rotated.
#mdbox_rotate_size = 10M

# Maximum dbox file age until it's rotated. Typically in days. Day begins
# from midnight, so 1d = today, 2d = yesterday, etc. 0 = check disabled.
#mdbox_rotate_interval = 0

# When creating new mdbox files, immediately preallocate their size to
# mdbox_rotate_size. This setting currently works only in Linux with some
# filesystems (ext4, xfs).
#mdbox_preallocate_space = no

##
## Mail attachments
##

# sdbox and mdbox support saving mail attachments to external files, which
# also allows single instance storage for them. Other backends don't support
# this for now.

# Directory root where to store mail attachments. Disabled, if empty.
#mail_attachment_dir =

# Attachments smaller than this aren't saved externally. It's also possible to
# write a plugin to disable saving specific attachments externally.
#mail_attachment_min_size = 128k

# Filesystem backend to use for saving attachments:
#  posix : No SiS done by Dovecot (but this might help FS's own deduplication)
#  sis posix : SiS with immediate byte-by-byte comparison during saving
#  sis-queue posix : SiS with delayed comparison and deduplication
#mail_attachment_fs = sis posix

# Hash format to use in attachment filenames. You can add any text and
# variables: %{md4}, %{md5}, %{sha1}, %{sha256}, %{sha512}, %{size}.
# Variables can be truncated, e.g. %{sha256:80} returns only first 80 bits
#mail_attachment_hash = %{sha1}

# Settings to control adding $HasAttachment or $HasNoAttachment keywords.
# By default, all MIME parts with Content-Disposition=attachment, or inlines
# with filename parameter are consired attachments.
#   add-flags - Add the keywords when saving new mails or when fetching can
#      do it efficiently.
#   content-type=type or !type - Include/exclude content type. Excluding will
#     never consider the matched MIME part as attachment. Including will only
#     negate an exclusion (e.g. content-type=!foo/* content-type=foo/bar).
#   exclude-inlined - Exclude any Content-Disposition=inline MIME part.
#mail_attachment_detection_options =
</code>
</pre>
</details>
<h5 id="2323-10-sslconf">2.3.2.3. 10-ssl.conf</h5>
<p>Please read:</p>
<ul>
<li><a href="https://doc.dovecot.org/2.3/configuration_manual/dovecot_ssl_configuration">Dovecot SSL configuration</a>.</li>
</ul>
<p>Edit file <code>/usr/local/etc/dovecot/conf.d/10-ssl.conf</code>, change / add as below:</p>






<pre tabindex="0"><code>ssl = required</code></pre>






<pre tabindex="0"><code>ssl_key = &lt;/your/ssl/key.pem
ssl_cert = &lt;/your/ssl/etc//fullchain.pem</code></pre>
<details>
<summary>MY CONFIG</summary>
<pre>
<code>
root@mailserver:~ # cat /usr/local/etc/dovecot/conf.d/10-ssl.conf
you have mail
##
## SSL settings
##

# SSL/TLS support: yes, no, required. &lt;doc/wiki/SSL.txt>
ssl = required

# PEM encoded X.509 SSL/TLS certificate and private key. They're opened before
# dropping root privileges, so keep the key file unreadable by anyone but
# root. Included doc/mkcert.sh can be used to easily generate self-signed
# certificate, just make sure to update the domains in dovecot-openssl.cnf
ssl_cert = &lt;/usr/local/etc/certs/example.com/fullchain.pem
ssl_key = &lt;/usr/local/etc/certs/example.com/key.pem

# If key file is password protected, give the password here. Alternatively
# give it when starting dovecot with -p parameter. Since this file is often
# world-readable, you may want to place this setting instead to a different
# root owned 0600 file by using ssl_key_password = &lt;path.
#ssl_key_password =

# PEM encoded trusted certificate authority. Set this only if you intend to use
# ssl_verify_client_cert=yes. The file should contain the CA certificate(s)
# followed by the matching CRL(s). (e.g. ssl_ca = &lt;/etc/ssl/certs/ca.pem)
#ssl_ca = 

# Require that CRL check succeeds for client certificates.
#ssl_require_crl = yes

# Directory and/or file for trusted SSL CA certificates. These are used only
# when Dovecot needs to act as an SSL client (e.g. imapc backend or
# submission service). The directory is usually /etc/ssl/certs in
# Debian-based systems and the file is /etc/pki/tls/cert.pem in
# RedHat-based systems. Note that ssl_client_ca_file isn't recommended with
# large CA bundles, because it leads to excessive memory usage.
#ssl_client_ca_dir =
#ssl_client_ca_file =

# Require valid cert when connecting to a remote server
#ssl_client_require_valid_cert = yes

# Request client to send a certificate. If you also want to require it, set
# auth_ssl_require_client_cert=yes in auth section.
#ssl_verify_client_cert = no

# Which field from certificate to use for username. commonName and
# x500UniqueIdentifier are the usual choices. You'll also need to set
# auth_ssl_username_from_cert=yes.
#ssl_cert_username_field = commonName

# SSL DH parameters
# Generate new params with `openssl dhparam -out /usr/local/etc/dovecot/dh.pem 4096`
# Or migrate from old ssl-parameters.dat file with the command dovecot
# gives on startup when ssl_dh is unset.
#ssl_dh = &lt;/usr/local/etc/dovecot/dh.pem

# Minimum SSL protocol version to use. Potentially recognized values are SSLv3,
# TLSv1, TLSv1.1, TLSv1.2 and TLSv1.3, depending on the OpenSSL version used.
#
# Dovecot also recognizes values ANY and LATEST. ANY matches with any protocol
# version, and LATEST matches with the latest version supported by library.
#ssl_min_protocol = TLSv1.2

# SSL ciphers to use, the default is:
#ssl_cipher_list = ALL:!kRSA:!SRP:!kDHd:!DSS:!aNULL:!eNULL:!EXPORT:!DES:!3DES:!MD5:!PSK:!RC4:!ADH:!LOW@STRENGTH
# To disable non-EC DH, use:
#ssl_cipher_list = ALL:!DH:!kRSA:!SRP:!kDHd:!DSS:!aNULL:!eNULL:!EXPORT:!DES:!3DES:!MD5:!PSK:!RC4:!ADH:!LOW@STRENGTH

# Colon separated list of elliptic curves to use. Empty value (the default)
# means use the defaults from the SSL library. P-521:P-384:P-256 would be an
# example of a valid value.
#ssl_curve_list =

# Prefer the server's order of ciphers over client's.
#ssl_prefer_server_ciphers = no

# SSL crypto device to use, for valid values run "openssl engine"
#ssl_crypto_device =

# SSL extra options. Currently supported options are:
#   compression - Enable compression.
#   no_ticket - Disable SSL session tickets.
#ssl_options =
</code>
</pre>
</details>
<h5 id="2324-15-mailboxesconf">2.3.2.4. 15-mailboxes.conf</h5>
<p>Please read:</p>
<ul>
<li><a href="https://doc.dovecot.org/2.3/configuration_manual/namespace/#mailbox-settings">Mailbox Settings</a>.</li>
</ul>
<p>Dovecot uses some standard mailboxes like Drafts, Junk, Trash and Sent additional to the Inbox. These are not created automatically, which will possibly yield some error messages when connecting a mail client. So we can set up Dovecot to create those mailboxes automatically.</p>
<p>Edit file <code>/usr/local/etc/dovecot/conf.d/15-mailboxes.conf</code>, and add <code>auto = create</code> to the standard mailboxes:</p>
<details>
<summary>MY CONFIG</summary>
<pre>
<code>
root@mailserver:~ # cat /usr/local/etc/dovecot/conf.d/15-mailboxes.conf
##
## Mailbox definitions
##

# Each mailbox is specified in a separate mailbox section. The section name
# specifies the mailbox name. If it has spaces, you can put the name
# "in quotes". These sections can contain the following mailbox settings:
#
# auto:
#   Indicates whether the mailbox with this name is automatically created
#   implicitly when it is first accessed. The user can also be automatically
#   subscribed to the mailbox after creation. The following values are
#   defined for this setting:
# 
#     no        - Never created automatically.
#     create    - Automatically created, but no automatic subscription.
#     subscribe - Automatically created and subscribed.
#  
# special_use:
#   A space-separated list of SPECIAL-USE flags (RFC 6154) to use for the
#   mailbox. There are no validity checks, so you could specify anything
#   you want in here, but it's not a good idea to use flags other than the
#   standard ones specified in the RFC:
#
#     \All       - This (virtual) mailbox presents all messages in the
#                  user's message store.
#     \Archive   - This mailbox is used to archive messages.
#     \Drafts    - This mailbox is used to hold draft messages.
#     \Flagged   - This (virtual) mailbox presents all messages in the
#                  user's message store marked with the IMAP \Flagged flag.
#     \Important - This (virtual) mailbox presents all messages in the
#                  user's message store deemed important to user.
#     \Junk      - This mailbox is where messages deemed to be junk mail
#                  are held.
#     \Sent      - This mailbox is used to hold copies of messages that
#                  have been sent.
#     \Trash     - This mailbox is used to hold messages that have been
#                  deleted.
#
# comment:
#   Defines a default comment or note associated with the mailbox. This
#   value is accessible through the IMAP METADATA mailbox entries
#   "/shared/comment" and "/private/comment". Users with sufficient
#   privileges can override the default value for entries with a custom
#   value.

# NOTE: Assumes "namespace inbox" has been defined in 10-mail.conf.
namespace inbox {
  # These mailboxes are widely used and could perhaps be created automatically:
  mailbox Drafts {
    special_use = \Drafts
    auto = create
  }
  mailbox Junk {
    special_use = \Junk
    auto = create
  }
  mailbox Trash {
    special_use = \Trash
    auto = create
  }

  # For \Sent mailboxes there are two widely used names. We'll mark both of
  # them as \Sent. User typically deletes one of them if duplicates are created.
  mailbox Sent {
    special_use = \Sent
    auto = create
  }
  mailbox "Sent Messages" {
    special_use = \Sent
  }

  # If you have a virtual "All messages" mailbox:
  #mailbox virtual/All {
  #  special_use = \All
  #  comment = All my messages
  #}

  # If you have a virtual "Flagged" mailbox:
  #mailbox virtual/Flagged {
  #  special_use = \Flagged
  #  comment = All my flagged messages
  #}

  # If you have a virtual "Important" mailbox:
  #mailbox virtual/Important {
  #  special_use = \Important
  #  comment = All my important messages
  #}
}
</code>
</pre>
</details>
<h5 id="2325-10-authconf">2.3.2.5. 10-auth.conf</h5>
<p>Please read:</p>
<ul>
<li><a href="https://doc.dovecot.org/2.3/configuration_manual/authentication">Authentication</a>.</li>
</ul>
<p>We will configure Dovecot to use <a href="https://doc.dovecot.org/2.3/configuration_manual/virtual_users">virtual users</a>. Dovecot supports many <a href="https://doc.dovecot.org/2.3/configuration_manual/authentication">different password databases and user databases</a>. With virtual users the most commonly used ones are LDAP, SQL and passwd-file.</p>
<p>Since we have only several users so we will auth by <a href="https://doc.dovecot.org/2.3/configuration_manual/authentication/passwd_file">Passwd-file</a>.</p>
<p>Edit file <code>/usr/local/etc/dovecot/conf.d/10-auth.conf</code>, change / add as below:</p>
<p>Comment out <code>!include auth-system.conf.ext</code> to disable system user auth.</p>






<pre tabindex="0"><code>#!include auth-system.conf.ext</code></pre>
<p>Dovecot splits all authentication lookups into two categories:</p>
<ul>
<li><a href="https://doc.dovecot.org/2.3/configuration_manual/authentication/password_databases_passdb">passdb</a></li>
<li><a href="https://doc.dovecot.org/2.3/configuration_manual/authentication/user_databases_userdb">userdb</a></li>
</ul>
<p><a href="https://doc.dovecot.org/2.3/configuration_manual/authentication/password_databases_passdb">passdb</a> lookup most importantly authenticate the user. They also provide any other pre-login information needed for users, such as:</p>
<ul>
<li>Which server user is proxied to.</li>
<li>If user should be allowed to log in at all (temporarily or permanently).</li>
</ul>
<p>We will use <a href="https://doc.dovecot.org/2.3/configuration_manual/authentication/passwd_file">Passwd-file</a> with our encrypted password. <a href="https://doc.dovecot.org/2.3/configuration_manual/authentication/password_schemes">Password scheme</a> = CRYPT.</p>






<pre tabindex="0"><code>passdb {
  driver = passwd-file
  args = scheme=CRYPT /usr/local/etc/mail/passwds
}</code></pre>
<p>Of course we will need a <a href="https://doc.dovecot.org/2.3/configuration_manual/authentication/passwd_file">passwd-like file</a> in <code>/usr/local/etc/mail/passwds</code>.</p>
<p>FOR EXAMPLE</p>
<p>Get your encrypted password by <a href="https://man.openbsd.org/smtpctl#encrypt">smtpctl</a> or <a href="https://doc.dovecot.org/main/core/man/doveadm-pw.1.html">doveadm-pw</a>:</p>






<pre tabindex="0"><code>$ smtpctl encrypt passw0rd</code></pre>
<p>or:</p>






<pre tabindex="0"><code>$ doveadm pw -p passw0rd</code></pre>
<p>Note that change <code>passw0rd</code> to your actual password.</p>
<p>Then add <code>username</code> and this <code>password</code> to your <code>passwds</code> file.</p>






<pre tabindex="0"><code>root@mailserver:~ # cat /usr/local/etc/mail/passwds 
john@example.com:$6$0UWgneRDOFLnWRnh$UbNpTu4UexIDRaI8hhFMnV8r7Z6x4y4fKH8q5AUD39seTX0EyiG124F7GZHdfxzhF87RpbWR7/A9FmZvR60MN0::::::
catchall@example.com:$6$ltEXtOxtMOY/5VxD$Ad/7Cce0Rsk5EDMLcZTXBzsB.AxcumZwUHUVpWaSYohL99pjsOTYrLqK40DiYwSd6Mup8Be/Hoo6mZDhZ2CVc::::::</code></pre>
<p><a href="https://doc.dovecot.org/2.3/configuration_manual/authentication/user_databases_userdb">userdb</a> lookup retrieves post-login information specific to this user. This may include:</p>
<ul>
<li>Mailbox location information</li>
<li>Quota limit</li>
<li>Overriding settings for the user (almost any setting can be overridden)</li>
</ul>
<p>We already set our email format and <code>mail_location = maildir:~/Maildir</code> before.</p>
<p>We also need to set <a href="https://doc.dovecot.org/2.3/configuration_manual/virtual_users/#home-directories">Home directories</a> to <code>/home/vmail/%u</code>. <code>%u</code> for full username and <code>%d</code> for domain as <a href="https://doc.dovecot.org/2.3/configuration_manual/config_file/config_variables">the docs</a>.</p>






<pre tabindex="0"><code>userdb {
  driver = static
  args = uid=vmail gid=vmail home=/home/vmail/%u
}</code></pre>
<p>This makes Dovecot look up the mails from <code>/home/vmail/&lt;user&gt;/Maildir/</code> directory, which should be owned by <code>vmail</code> user and <code>vmail</code> group.</p>
<p>If you have many domains so you may want to change it to <code>home=/home/vmail/%d/%n</code> ~ <code>/home/vmail/&lt;domain&gt;/&lt;user&gt;/Maildir/</code>.</p>
<details>
<summary>MY CONFIG</summary>
<pre>
<code>
root@mailserver:~ # cat /usr/local/etc/dovecot/conf.d/10-auth.conf
##
## Authentication processes
##

# Disable LOGIN command and all other plaintext authentications unless
# SSL/TLS is used (LOGINDISABLED capability). Note that if the remote IP
# matches the local IP (ie. you're connecting from the same computer), the
# connection is considered secure and plaintext authentication is allowed.
# See also ssl=required setting.
#disable_plaintext_auth = yes

# Authentication cache size (e.g. 10M). 0 means it's disabled. Note that
# bsdauth and PAM require cache_key to be set for caching to be used.
#auth_cache_size = 0
# Time to live for cached data. After TTL expires the cached record is no
# longer used, *except* if the main database lookup returns internal failure.
# We also try to handle password changes automatically: If user's previous
# authentication was successful, but this one wasn't, the cache isn't used.
# For now this works only with plaintext authentication.
#auth_cache_ttl = 1 hour
# TTL for negative hits (user not found, password mismatch).
# 0 disables caching them completely.
#auth_cache_negative_ttl = 1 hour

# Space separated list of realms for SASL authentication mechanisms that need
# them. You can leave it empty if you don't want to support multiple realms.
# Many clients simply use the first one listed here, so keep the default realm
# first.
#auth_realms =

# Default realm/domain to use if none was specified. This is used for both
# SASL realms and appending @domain to username in plaintext logins.
#auth_default_realm = 

# List of allowed characters in username. If the user-given username contains
# a character not listed in here, the login automatically fails. This is just
# an extra check to make sure user can't exploit any potential quote escaping
# vulnerabilities with SQL/LDAP databases. If you want to allow all characters,
# set this value to empty.
#auth_username_chars = abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-_@

# Username character translations before it's looked up from databases. The
# value contains series of from -> to characters. For example "#@/@" means
# that '#' and '/' characters are translated to '@'.
#auth_username_translation =

# Username formatting before it's looked up from databases. You can use
# the standard variables here, eg. %Lu would lowercase the username, %n would
# drop away the domain if it was given, or "%n-AT-%d" would change the '@' into
# "-AT-". This translation is done after auth_username_translation changes.
#auth_username_format = %Lu

# If you want to allow master users to log in by specifying the master
# username within the normal username string (ie. not using SASL mechanism's
# support for it), you can specify the separator character here. The format
# is then &lt;username>&lt;separator>&lt;master username>. UW-IMAP uses "*" as the
# separator, so that could be a good choice.
#auth_master_user_separator =

# Username to use for users logging in with ANONYMOUS SASL mechanism
#auth_anonymous_username = anonymous

# Maximum number of dovecot-auth worker processes. They're used to execute
# blocking passdb and userdb queries (eg. MySQL and PAM). They're
# automatically created and destroyed as needed.
#auth_worker_max_count = 30

# Host name to use in GSSAPI principal names. The default is to use the
# name returned by gethostname(). Use "$ALL" (with quotes) to allow all keytab
# entries.
#auth_gssapi_hostname =

# Kerberos keytab to use for the GSSAPI mechanism. Will use the system
# default (usually /etc/krb5.keytab) if not specified. You may need to change
# the auth service to run as root to be able to read this file.
#auth_krb5_keytab = 

# Do NTLM and GSS-SPNEGO authentication using Samba's winbind daemon and
# ntlm_auth helper. &lt;doc/wiki/Authentication/Mechanisms/Winbind.txt>
#auth_use_winbind = no

# Path for Samba's ntlm_auth helper binary.
#auth_winbind_helper_path = /usr/bin/ntlm_auth

# Time to delay before replying to failed authentications.
#auth_failure_delay = 2 secs

# Require a valid SSL client certificate or the authentication fails.
#auth_ssl_require_client_cert = no

# Take the username from client's SSL certificate, using 
# X509_NAME_get_text_by_NID() which returns the subject's DN's
# CommonName. 
#auth_ssl_username_from_cert = no

# Space separated list of wanted authentication mechanisms:
#   plain login digest-md5 cram-md5 ntlm rpa apop anonymous gssapi otp
#   gss-spnego
# NOTE: See also disable_plaintext_auth setting.
auth_mechanisms = plain

##
## Password and user databases
##

#
# Password database is used to verify user's password (and nothing more).
# You can have multiple passdbs and userdbs. This is useful if you want to
# allow both system users (/etc/passwd) and virtual users to login without
# duplicating the system users into virtual database.
#
# &lt;doc/wiki/PasswordDatabase.txt>
#
# User database specifies where mails are located and what user/group IDs
# own them. For single-UID configuration use "static" userdb.
#
# &lt;doc/wiki/UserDatabase.txt>

#!include auth-deny.conf.ext
#!include auth-master.conf.ext

#!include auth-system.conf.ext
#!include auth-sql.conf.ext
#!include auth-ldap.conf.ext
#!include auth-passwdfile.conf.ext
#!include auth-checkpassword.conf.ext
#!include auth-static.conf.ext

# Authentication configuration:
auth_verbose = yes
passdb {
  driver = passwd-file
  args = scheme=CRYPT /usr/local/etc/mail/passwds
}
userdb {
  driver = static
  args = uid=vmail gid=vmail home=/home/vmail/%u
}
</code>
</pre>
</details>
<h3 id="24-rspamd-spam-filtering">2.4. <a href="https://rspamd.com">Rspamd</a> (spam filtering)</h3>
<p>And Rspamd for spam filtering system.</p>
<p><img src="rspamd.png" alt="rspamd" title="rspamd"></p>
<h4 id="241-install-and-config-rspamd">2.4.1. Install and config Rspamd</h4>






<pre tabindex="0"><code># pkg install rspamd opensmtpd-filter-rspamd</code></pre>
<p>create <code>local.d</code> folder</p>






<pre tabindex="0"><code># mkdir /usr/local/etc/rspamd/local.d</code></pre>
<p>create a new file <code>/usr/local/etc/rspamd/local.d/redis.conf</code> with folowing code:</p>






<pre tabindex="0"><code>servers = &#34;your_redis_ip&#34;;
password = &#34;your_redis_password&#34;;</code></pre>
<p>Make sure your Redis server listen to an appropriate network.</p>
<h4 id="242-configure-opensmtpd">2.4.2. Configure OpenSMTPD</h4>
<p>Modify your OpenSMTPD config at <code>/usr/local/etc/mail/smtpd.conf</code>:</p>
<p>delare &ldquo;rspamd&rdquo; filter:</p>






<pre tabindex="0"><code>filter &#34;rspamd&#34; proc-exec &#34;/usr/local/libexec/opensmtpd/opensmtpd-filter-rspamd&#34;</code></pre>
<p>attach this filter to listeners:</p>






<pre tabindex="0"><code>listen on 0.0.0.0 port 25 tls pki mail.example.com filter &#34;rspamd&#34;
listen on 0.0.0.0 port 465 smtps pki mail.example.com auth &lt;credentials&gt; filter &#34;rspamd&#34;
listen on 0.0.0.0 port 587 tls-require pki mail.example.com auth &lt;credentials&gt; filter &#34;rspamd&#34;</code></pre>
<h4 id="243-fix-rspamd-bug-in-freebsd-jail">2.4.3. Fix Rspamd bug in FreeBSD jail</h4>
<p>When we read Rspamd log, there are some errors:</p>






<pre tabindex="0"><code>2024-10-16 15:27:05 #25098(controller) &lt;zdedds&gt;; monitored; rspamd_monitored_dns_mon: cannot make request to resolve 1.0.0.127.bl.spameatingmonkey.net (bl.spameatingmonkey.net monitored url)</code></pre>
<p>It happens because Rspamd use dns nameserver from <code>/etc/resolv.conf</code>. It may not being config correctly in FreeBSD jail.</p>
<p>Of course you can edit <code>/etc/resolv.conf</code>, set it into a public dns server (for example: <code>8.8.8.8</code>).</p>
<p>Or we prefer change it in the config file <code>/usr/local/etc/rspamd/local.d/options.inc</code>, add code bellow:</p>






<pre tabindex="0"><code>dns {
    timeout = 1s;
    sockets = 16;
    retransmits = 5;
    nameserver = &#34;master-slave:127.0.0.1:53:10,8.8.8.8:53:1&#34;;
}</code></pre>
<details>
<summary>MY CONFIG</summary>
<pre>
<code>
root@mailserver:~ # cat /usr/local/etc/rspamd/local.d/options.inc 
# DNS options
dns {
    timeout = 1s;
    sockets = 16;
    retransmits = 5;
    nameserver = "master-slave:127.0.0.1:53:10,8.8.8.8:53:1";
}
</code>
</pre>
</details>
<p>Please read:</p>
<ul>
<li><a href="https://rspamd.com/doc/configuration/options.html#dns-options">DNS options</a></li>
</ul>
<h3 id="25-dns-configuration">2.5. DNS configuration</h3>
<h4 id="251-mx-record">2.5.1. <a href="https://en.wikipedia.org/wiki/MX_record">MX record</a></h4>
<p>Please read:</p>
<ul>
<li><a href="https://www.cloudflare.com/learning/dns/dns-records/dns-mx-record">DNS MX record</a></li>
</ul>
<p>Just need to tell people that which server is your mail server.</p>
<p>For example:</p>
<table>
  <thead>
      <tr>
          <th>Type</th>
          <th>Name (required)</th>
          <th>Mail server (required)</th>
          <th>TTL</th>
          <th>Priority (required)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>MX</td>
          <td>example.com</td>
          <td>mail.example.com</td>
          <td>Auto</td>
          <td>10</td>
      </tr>
  </tbody>
</table>
<h4 id="252-spf">2.5.2. <a href="https://datatracker.ietf.org/doc/rfc7208">SPF</a></h4>
<p>Sender Policy Framework (SPF) is a way for a domain to list all the servers they send emails from.</p>
<p>Please read:</p>
<ul>
<li><a href="https://documentation.mailjet.com/hc/en-us/articles/360042412734-Authenticating-Domains-with-SPF-DKIM">Authenticating Domains with SPF &amp; DKIM</a></li>
<li><a href="https://www.cloudflare.com/learning/dns/dns-records/dns-spf-record">DNS SPF record</a></li>
</ul>
<p>We use Mailjet as our relay so we need to set DNS records to Mailjet.</p>
<h4 id="253-dkim">2.5.3. <a href="https://datatracker.ietf.org/doc/rfc6376">DKIM</a></h4>
<p>DomainKeys Identified Mail (DKIM) enables domain owners to automatically &ldquo;sign&rdquo; emails from their domain, just as the signature on a check helps confirm who wrote the check.</p>
<p>Please read:</p>
<ul>
<li><a href="https://documentation.mailjet.com/hc/en-us/articles/360042412734-Authenticating-Domains-with-SPF-DKIM">Authenticating Domains with SPF &amp; DKIM</a></li>
<li><a href="https://www.cloudflare.com/learning/dns/dns-records/dns-dkim-record">DNS DKIM record</a></li>
<li><a href="https://rspamd.com/doc/modules/dkim_signing.html">Rspamd DKIM signing module</a></li>
<li><a href="https://palant.info/2020/11/09/adding-dkim-support-to-opensmtpd-with-custom-filters">Adding DKIM support to OpenSMTPD with custom filters</a></li>
</ul>
<p>We use Mailjet as our relay so we need to set DNS records to Mailjet. They will sign all outgoing emails automaticly.</p>
<p>Of course we can also sign messages ourself by <a href="https://github.com/poolpOrg/filter-rspamd">filter-rspamd</a> or <a href="https://github.com/palant/opensmtpd-filters">opensmtpd-filters</a>.</p>
<p>See below:</p>
<h5 id="2531-rspamd-dkim-signing-module">2.5.3.1 <a href="https://www.rspamd.com/doc/modules/dkim_signing.html">Rspamd DKIM signing module</a></h5>
<p>To generate DKIM keys for your domain, utilize the in-built rspamadm dkim_keygen utility, For an RSA key of 2048 bits:</p>






<pre tabindex="0"><code>$ mkdir /usr/local/etc/mail/dkim
$ cd /usr/local/etc/mail/dkim
$ rspamadm dkim_keygen -s &#39;selector1&#39; -b 2048 -d example.com -k example.key &gt; example.txt</code></pre>
<ul>
<li>&lt; example.txt re-directs the DNS TXT record output to example.txt</li>
<li>-k example.key saves your private key to the file example.key</li>
<li>-d example.com specifies the domain as example.com (currently meaningless)</li>
<li>-b 2048 specifies a 2048 bit key size (the standard default 1024 bit size is weak)</li>
<li>-s &lsquo;selector1&rsquo; names the selector selector1 i.e. selector1._domainkey</li>
</ul>
<p>Or by OpenSSL:</p>






<pre tabindex="0"><code>$ openssl genrsa -out /usr/local/etc/mail/dkim/example.key 2048
$ openssl rsa -in /usr/local/etc/mail/dkim/domain.key -pubout -out /usr/local/etc/mail/dkim/domain.crt</code></pre>
<p>Add DNS TXT record as their guide in <code>example.txt</code> or just create it manually:</p>
<table>
  <thead>
      <tr>
          <th>example.com</th>
          <th>record type:</th>
          <th>value:</th>
          <th>TTL</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>selector1._domainkey</td>
          <td>TXT</td>
          <td>v=DKIM1; k=rsa; p=your_pub_key</td>
          <td>Auto</td>
      </tr>
  </tbody>
</table>
<blockquote>
<p>&lt;your_pub_key&gt; is the content of <code>domain.crt</code>, something like:</p></blockquote>






<pre tabindex="0"><code>-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlNFNiVKm/eyVz+kBh+mr
LhlulQlFNa1u3YWQa08HsiDg0pO3y/FQHfsnmMwZ9eIzIupIb3zEGudEzGYMUnOm
B8kxbe8b6FKt82r4mt+y+EHoWeIZQWWMwCZ/2WetBsw28CezAU4nB59RNq6Z1uE/
OwnS9K9pS8WXxgmoel4m4KiOo9ZeiC1aBffg2jmStyavvp12p0XW6VsRPVCPQPjt
COjVjjRJ+UXgWqJkEqwn9csXQu/cJO6lTpQD0bukUJlrByBJDO5peuoj+q7ntviL
Cc3UHcxYrUVmEOLnn6gx0vdC1Xup+sjZrpxRN7ZYMHUD5Re3YB2+sYicWHQzla+t
VQIDAQAB
-----END PUBLIC KEY-----</code></pre>
<p>Change ownership of your KEYS so Rspamd can read it:</p>






<pre tabindex="0"><code># chown -R rspamd:rspamd /usr/local/etc/mail/dkim</code></pre>
<p>Add / modify Rspamd DKIM signing config file in <code>/usr/local/etc/rspamd/local.d/dkim_signing.conf</code>:</p>






<pre tabindex="0"><code>allow_username_mismatch = true;

domain {
  example.com {
    path = &#34;/usr/local/etc/mail/dkim/example.key&#34;;
    selector = &#34;selector1&#34;;
  }
}</code></pre>
<p>Restart Rspamd and check if it works.</p>
<h4 id="254-dmarc">2.5.4. <a href="https://datatracker.ietf.org/doc/rfc7489">DMARC</a></h4>
<p>A DMARC policy allows a sender&rsquo;s domain to indicate that their email messages are protected by SPF and/or DKIM, and tells a receiver what to do if neither of those authentication methods passes – such as to reject the message or quarantine it. The policy can also specify how an email receiver can report back to the sender&rsquo;s domain about messages that pass and/or fail.</p>
<p>Please read:</p>
<ul>
<li><a href="https://www.cloudflare.com/learning/dns/dns-records/dns-dmarc-record">DMARC DNS records</a></li>
</ul>
<h4 id="255-dane-for-smtp">2.5.5. <a href="https://datatracker.ietf.org/doc/rfc7672">DANE for SMTP</a></h4>
<p>RFC 7672 introduced the ability for DNS records to declare the encryption capabilities of a mail server. Utilising DNSSEC, mail server operators are able to publish a hash of their TLS certificate, thereby mitigating the possibility of unencrypted communications.</p>
<p>DANE is not implement on OpenSMTPD I guess.</p>
<h4 id="256-mta-sts-and-smtp-tls-reporting">2.5.6. <a href="https://datatracker.ietf.org/doc/rfc8461">MTA-STS</a> and <a href="https://datatracker.ietf.org/doc/rfc8460">SMTP TLS Reporting</a></h4>
<p>SMTP MTA Strict Transport Security (MTA-STS) is a mechanism enabling mail service providers (SPs) to declare their ability to receive Transport Layer Security (TLS) secure SMTP connections and to specify whether sending SMTP servers should refuse to deliver to MX hosts that do not offer TLS with a trusted server certificate.</p>
<p>RFC 8460 &ldquo;SMTP TLS Reporting&rdquo; describes a reporting mechanism and format for sharing statistics and specific information about potential failures with recipient domains. Recipient domains can then use this information to both detect potential attacks and diagnose unintentional misconfigurations.</p>
<p>Please read Microsoft and / or Google articles:</p>
<ul>
<li><a href="https://learn.microsoft.com/en-us/purview/enhancing-mail-flow-with-mta-sts">Enhancing mail flow with MTA-STS</a></li>
<li><a href="https://support.google.com/a/answer/9261504">Increase email security with MTA-STS and TLS reporting</a></li>
</ul>
<p>Turn on MTA-STS and TLS reporting:</p>
<ul>
<li>Create a mailbox to get reports</li>
<li>Update DNS Records</li>
<li>Verify MTA-STS and TLS reporting are turned on</li>
</ul>
<p>MTA-STS policy publishing:</p>
<ul>
<li>Create a MTA-STS policy file (<code>mta-sts.txt</code>)</li>
<li>Hosts it by some web server (nginx / apache)</li>
<li>Verify at <code>https://mta-sts.&lt;domain name&gt;/.well-known/mta-sts.txt</code></li>
</ul>
<h3 id="26-behind-proxy">2.6. Behind proxy</h3>
<p>Please read:</p>
<ul>
<li><a href="https://github.com/haproxy/haproxy/blob/master/doc/proxy-protocol.txt">PROXY protocol</a></li>
<li><a href="https://docker-mailserver.github.io/docker-mailserver/edge/examples/tutorials/mailserver-behind-proxy">Mailserver behind Proxy</a></li>
<li><a href="https://doc.dovecot.org/2.3/configuration_manual/forwarding_parameters">Forwarding parameters in IMAP/POP3/LMTP/SMTP proxying</a></li>
</ul>
<p>It&rsquo;s a common practice we run a mail server behind a load balancer / proxy server (<a href="https://www.haproxy.org">HAProxy</a>, <a href="https://traefik.io">Traefik</a>..).</p>
<p>We lost information about originating IP address of clients connecting to our server (through a proxy server) that needed for spam filtering.</p>
<p>It might be complicated in Layer 7 (something like <code>X-Forwarded-For</code> in HTTP protocol) but it&rsquo;s possible?</p>
<p>Instead we can do it easy in Layer 4 with <a href="https://github.com/haproxy/haproxy/blob/master/doc/proxy-protocol.txt">PROXY protocol</a> which are supported by both OpenSMTPD and Dovecot.</p>
<h4 id="261-haproxy">2.6.1. HAProxy</h4>
<p>Please read:</p>
<ul>
<li><a href="https://www.haproxy.com/documentation/haproxy-configuration-tutorials/client-ip-preservation/enable-proxy-protocol">HAProxy Enable the Proxy Protocol</a></li>
</ul>
<p>Add <code>send-proxy-v2</code> in <code>haproxy.conf</code> to send a Proxy Protocol version 2 header (binary format) to the backend servers.</p>
<p>You may want to increase timeout value for IMAP IDLE (<a href="https://www.rfc-editor.org/rfc/rfc2177">clients using IDLE are advised to terminate the IDLE and re-issue it at least every 29 minutes</a>).</p>
<details>
<summary>MY CONFIG</summary>
<pre>
<code>
root@proxy:~ # cat /usr/local/etc/haproxy/haproxy.conf 
global

defaults
  log global
  mode tcp
  timeout connect 10s
  timeout client 30m # keep TCP connection for IMAP IDLE
  timeout server 30m # keep TCP conectionfor IMAP IDLE

frontend rspamd
  bind :11334
  mode tcp
  default_backend rspamd

backend rspamd
  mode tcp
  server rspamd 10.0.0.2

frontend opensmtpd
  bind :25,:465,:587
  mode tcp
  default_backend opensmtpd

backend opensmtpd
  mode tcp
  server opensmtpd 10.0.0.2 send-proxy-v2

frontend dovecot
  bind :143,:993
  mode tcp
  default_backend dovecot

backend dovecot
  mode tcp
  server imap 10.0.0.2 send-proxy-v2
</code>
</pre>
</details>
<h4 id="262-opensmtpd">2.6.2. OpenSMTPD</h4>
<p>Please read:</p>
<ul>
<li><a href="https://man.openbsd.org/smtpd.conf#proxy-v2">OpenSMTPD PROXYv2 protocol</a></li>
</ul>
<p>Add <code>proxy-v2</code> to listen blocks in <code>/usr/local/etc/mail/smtpd.conf</code>.</p>
<details>
<summary>FOR EXAMPLE</summary>
<pre>
<code>
# STARTTLS port 25
listen on 0.0.0.0 port 25 tls pki mail.example.com proxy-v2 filter "rspamd"
# SMTPS port 465
listen on 0.0.0.0 port 465 smtps pki mail.example.com proxy-v2 auth &lt;credentials> filter "rspamd"
# submission port 587
listen on 0.0.0.0 port 587 tls-require pki mail.example.com proxy-v2 auth &lt;credentials> filter "rspamd"
</code>
</pre>
</details>
<h4 id="261-dovecot">2.6.1. Dovecot</h4>
<p>Please read:</p>
<ul>
<li><a href="https://dovecot.org/mailman3/archives/list/dovecot@dovecot.org/thread/5MM5YW4X272UI2DSCUC4DFC6PL6HFIUR">Dovecot PROXY protocol</a></li>
</ul>
<p>Add <code>haproxy_trusted_networks = &quot;YOUR_PROXY_NETWORK&quot;</code> and <code>haproxy = yes</code> in <code>/usr/local/etc/dovecot/conf.d/10-master.conf</code>.</p>
<p>FOR EXAMPLE:</p>






<pre tabindex="0"><code>haproxy_trusted_networks = &#34;10.0.0.0/8&#34;

service imap-login {
  inet_listener imaps {
    haproxy = yes
  }
}</code></pre>
<details>
<summary>MY CONFIG</summary>
<pre>
<code>
root@mailserver:~ # cat /usr/local/etc/dovecot/conf.d/10-master.conf 
#default_process_limit = 100
#default_client_limit = 1000

# Default VSZ (virtual memory size) limit for service processes. This is mainly
# intended to catch and kill processes that leak memory before they eat up
# everything.
#default_vsz_limit = 256M

# Login user is internally used by login processes. This is the most untrusted
# user in Dovecot system. It shouldn't have access to anything at all.
#default_login_user = dovenull

# Internal user is used by unprivileged processes. It should be separate from
# login user, so that login processes can't disturb other processes.
#default_internal_user = dovecot

haproxy_trusted_networks = "10.0.0.0/8"

service imap-login {
  inet_listener imap {
    #port = 143
  }
  inet_listener imaps {
    # port = 993
    # ssl = yes
    haproxy = yes
  }

  # Number of connections to handle before starting a new process. Typically
  # the only useful values are 0 (unlimited) or 1. 1 is more secure, but 0
  # is faster. &lt;doc/wiki/LoginProcess.txt>
  #service_count = 1

  # Number of processes to always keep waiting for more connections.
  #process_min_avail = 0

  # If you set service_count=0, you probably need to grow this.
  #vsz_limit = $default_vsz_limit
}

service pop3-login {
  inet_listener pop3 {
    #port = 110
  }
  inet_listener pop3s {
    #port = 995
    #ssl = yes
  }
}

service submission-login {
  inet_listener submission {
    #port = 587
  }
  inet_listener submissions {
    #port = 465
  }
}

service lmtp {
  unix_listener lmtp {
    #mode = 0666
  }

  # Create inet listener only if you can't use the above UNIX socket
  #inet_listener lmtp {
    # Avoid making LMTP visible for the entire internet
    #address =
    #port = 
  #}
}

service imap {
  # Most of the memory goes to mmap()ing files. You may need to increase this
  # limit if you have huge mailboxes.
  #vsz_limit = $default_vsz_limit

  # Max. number of IMAP processes (connections)
  #process_limit = 1024
}

service pop3 {
  # Max. number of POP3 processes (connections)
  #process_limit = 1024
}

service submission {
  # Max. number of SMTP Submission processes (connections)
  #process_limit = 1024
}

service auth {
  # auth_socket_path points to this userdb socket by default. It's typically
  # used by dovecot-lda, doveadm, possibly imap process, etc. Users that have
  # full permissions to this socket are able to get a list of all usernames and
  # get the results of everyone's userdb lookups.
  #
  # The default 0666 mode allows anyone to connect to the socket, but the
  # userdb lookups will succeed only if the userdb returns an "uid" field that
  # matches the caller process's UID. Also if caller's uid or gid matches the
  # socket's uid or gid the lookup succeeds. Anything else causes a failure.
  #
  # To give the caller full permissions to lookup all users, set the mode to
  # something else than 0666 and Dovecot lets the kernel enforce the
  # permissions (e.g. 0777 allows everyone full permissions).
  unix_listener auth-userdb {
    #mode = 0666
    #user = 
    #group = 
  }

  # Postfix smtp-auth
  #unix_listener /var/spool/postfix/private/auth {
  #  mode = 0666
  #}

  # Auth process is run as this user.
  #user = $default_internal_user
}

service auth-worker {
  # Auth worker process is run as root by default, so that it can access
  # /etc/shadow. If this isn't necessary, the user should be changed to
  # $default_internal_user.
  #user = root
}

service dict {
  # If dict proxy is used, mail processes should have access to its socket.
  # For example: mode=0660, group=vmail and global mail_access_groups=vmail
  unix_listener dict {
    #mode = 0600
    #user = 
    #group = 
  }
}
</code>
</pre>
</details>
<h3 id="27-dont-know-yet">2.7. Don&rsquo;t know yet</h3>
<p>We will consider a <a href="https://en.wikipedia.org/wiki/Webmail">Webmail</a> but for now we don&rsquo;t need it.</p>
<p>We&rsquo;re using some traditional <a href="https://en.wikipedia.org/wiki/Email_client">Email client (MUA)</a> like <a href="https://www.thunderbird.net">Thunderbird</a> which speak <a href="https://en.wikipedia.org/wiki/Internet_Message_Access_Protocol">IMAP</a> and <a href="https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol">SMTP</a> directly to our server.</p>
]]></content:encoded></item><item><title>ZFS snapshot check holds and release</title><link>https://tuanbui.net/2024/09/24/cai-dat-truenas-scale-luu-tru-noi-mang-nas-va-zfs-raid-and-snapshot/</link><pubDate>Tue, 24 Sep 2024 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2024/09/24/cai-dat-truenas-scale-luu-tru-noi-mang-nas-va-zfs-raid-and-snapshot/</guid><description>&lt;p>The following command will show all snapshots of [pool] (&amp;lt;-replace this with your pool name) that have holds&lt;/p>
&lt;pre tabindex="0">&lt;code>zfs list -H -o name -t snapshot -r pool | xargs -n1 zfs holds -H&lt;/code>&lt;/pre>
&lt;p>the properties will be listed as &lt;code>property:stuff&lt;/code>&lt;/p>
&lt;p>with that information we can free the snapshots..&lt;/p>
&lt;pre tabindex="0">&lt;code>zfs list -H -o name -t snapshot -r pool | xargs -n1 zfs holds -H | awk &amp;#39;{print $1}&amp;#39; | xargs -n1 zfs release property:stuff&lt;/code>&lt;/pre>
&lt;p>(replace &amp;lsquo;property:stuff&amp;rsquo; with whatever is holding your dataset)&lt;/p></description><content:encoded><![CDATA[<p>The following command will show all snapshots of [pool] (&lt;-replace this with your pool name) that have holds</p>






<pre tabindex="0"><code>zfs list -H -o name -t snapshot -r pool | xargs -n1 zfs holds -H</code></pre>
<p>the properties will be listed as <code>property:stuff</code></p>
<p>with that information we can free the snapshots..</p>






<pre tabindex="0"><code>zfs list -H -o name -t snapshot -r pool | xargs -n1 zfs holds -H | awk &#39;{print $1}&#39; | xargs -n1 zfs release property:stuff</code></pre>
<p>(replace &lsquo;property:stuff&rsquo; with whatever is holding your dataset)</p>
<p>..and finally delete them</p>






<pre tabindex="0"><code>zfs destroy -r [pool]/[dataset][@snapshot]</code></pre>
<p>source: <a href="https://serverfault.com/questions/456301/how-to-check-that-all-zfs-snapshots-within-a-pool-are-without-holds-before-destr">How to check that all ZFS snapshots within a pool are without holds before destroying that pool</a></p>
]]></content:encoded></item><item><title>Good LDAP book: LDAP for Rocket Scientists by ZyTrax</title><link>https://tuanbui.net/2024/09/05/good-ldap-book-ldap-for-rocket-scientists-by-zytrax/</link><pubDate>Thu, 05 Sep 2024 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2024/09/05/good-ldap-book-ldap-for-rocket-scientists-by-zytrax/</guid><description>&lt;p>We were finding a LDAP book on internet so we can deploy our LDAP service in our network.&lt;/p>
&lt;p>&lt;img src="OpenLDAP-logo.png" alt="OpenLDAP-logo" title="OpenLDAP-logo">&lt;/p>
&lt;p>There are many LDAP tutorials we found but we don&amp;rsquo;t think it&amp;rsquo;s good enough. They wrote about how deploy OpenLDAP instead of LDAP protocol itself.&lt;/p>
&lt;p>LDAP is a complex topic so we can&amp;rsquo;t just install some software, It&amp;rsquo;s better if we can understand the protocol so we can using it in a proper way.&lt;/p></description><content:encoded><![CDATA[<p>We were finding a LDAP book on internet so we can deploy our LDAP service in our network.</p>
<p><img src="OpenLDAP-logo.png" alt="OpenLDAP-logo" title="OpenLDAP-logo"></p>
<p>There are many LDAP tutorials we found but we don&rsquo;t think it&rsquo;s good enough. They wrote about how deploy OpenLDAP instead of LDAP protocol itself.</p>
<p>LDAP is a complex topic so we can&rsquo;t just install some software, It&rsquo;s better if we can understand the protocol so we can using it in a proper way.</p>
<p>We found a book that we think it&rsquo;s good. You can read it if you want to know about LDAP.</p>
<ul>
<li><a href="https://zytrax.com/books/ldap">LDAP for Rocket Scientists by ZyTrax</a></li>
</ul>
]]></content:encoded></item><item><title>NTFS External USB HDD/SDD unable to mount on Linux Mint 22 ''Wilma''</title><link>https://tuanbui.net/2024/08/31/cai-dat-truenas-scale-luu-tru-noi-mang-nas-va-zfs-raid-and-snapshot/</link><pubDate>Sat, 31 Aug 2024 00:00:00 +0000</pubDate><author>me@tuanbui.net (Bui Anh Tuan)</author><guid>https://tuanbui.net/2024/08/31/cai-dat-truenas-scale-luu-tru-noi-mang-nas-va-zfs-raid-and-snapshot/</guid><description>&lt;p>My customer unable to mount on Linux Mint 22 &amp;lsquo;Wilma&amp;rsquo;&lt;/p>
&lt;p>So we tried to mount it manually:&lt;/p>
&lt;pre tabindex="0">&lt;code>sudo mount /dev/sdb1 /media&lt;/code>&lt;/pre>
&lt;p>It still gave an error:&lt;/p>
&lt;pre tabindex="0">&lt;code>Error mounting /dev/sdb1 at /media/: Command-line `mount -t &amp;#34;ntfs&amp;#34; -o &amp;#34;uhelper=udisks2,nodev,nosuid,uid=1000,gid=1000&amp;#34; &amp;#34;/dev/sdb1&amp;#34; &amp;#34;/media/sorin/LICENTA&amp;#34;&amp;#39; exited with non-zero exit status 13: $MFTMirr does not match $MFT (record 0).
Failed to mount &amp;#39;/dev/sdb1&amp;#39;: Input/output error
NTFS is either inconsistent, or there is a hardware fault, or it&amp;#39;s a
SoftRAID/FakeRAID hardware. In the first case run chkdsk /f on Windows
then reboot into Windows twice. The usage of the /f parameter is very
important! If the device is a SoftRAID/FakeRAID then first activate
it and mount a different device under the /dev/mapper/ directory, (e.g.
/dev/mapper/nvidia_eahaabcc1). Please see the &amp;#39;dmraid&amp;#39; documentation
for more details.&lt;/code>&lt;/pre>
&lt;p>This is how we fix:&lt;/p></description><content:encoded><![CDATA[<p>My customer unable to mount on Linux Mint 22 &lsquo;Wilma&rsquo;</p>
<p>So we tried to mount it manually:</p>






<pre tabindex="0"><code>sudo mount /dev/sdb1 /media</code></pre>
<p>It still gave an error:</p>






<pre tabindex="0"><code>Error mounting /dev/sdb1 at /media/: Command-line `mount -t &#34;ntfs&#34; -o &#34;uhelper=udisks2,nodev,nosuid,uid=1000,gid=1000&#34; &#34;/dev/sdb1&#34; &#34;/media/sorin/LICENTA&#34;&#39; exited with non-zero exit status 13: $MFTMirr does not match $MFT (record 0).
Failed to mount &#39;/dev/sdb1&#39;: Input/output error
NTFS is either inconsistent, or there is a hardware fault, or it&#39;s a
SoftRAID/FakeRAID hardware. In the first case run chkdsk /f on Windows
then reboot into Windows twice. The usage of the /f parameter is very
important! If the device is a SoftRAID/FakeRAID then first activate
it and mount a different device under the /dev/mapper/ directory, (e.g.
/dev/mapper/nvidia_eahaabcc1). Please see the &#39;dmraid&#39; documentation
for more details.</code></pre>
<p>This is how we fix:</p>






<pre tabindex="0"><code>sudo ntfsfix /dev/sdb1</code></pre>
<p>Now we can mount this HDD manual but the GUI mount still not work when he plug it to the machine.</p>
<p>So we fix it as follow:</p>
<ul>
<li>Open Disks (<code>GNOME Disks</code>), select the external disk, <code>Edit Mount Options</code>.</li>
<li>Disable the <code>User Session Defaults</code> and click <code>OK</code> in the <code>Mount Options</code> dialog box.</li>
</ul>
<p><img src="GNOME-Disks-Linux-Mint.png" alt="GNOME-Disks-Linux-Mint" title="GNOME-Disks-Linux-Mint"></p>
<p><img src="GNOME-Disk-Edit-Mount-Options.png" alt="GNOME-Disk-Edit-Mount-Options" title="GNOME-Disk-Edit-Mount-Options"></p>
<p><img src="GNOME-Disks-Mount-Options.png" alt="GNOME-Disks-Mount-Options" title="GNOME-Disks-Mount-Options"></p>
<p>Then it works!</p>
]]></content:encoded></item></channel></rss>