<?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>Podman on Tarragon</title><link>https://tarrragon.github.io/blog/tags/podman/</link><description>Recent content in Podman on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 26 Oct 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/podman/index.xml" rel="self" type="application/rss+xml"/><item><title>Podman 與 rootless Docker 發佈連接埠的實測紀錄：rootful Podman 在 ufw 與 firewalld 上的轉送路徑、rootless Docker 與 rootless Podman 由主機程式接收的連接埠與容器看到的來源位址</title><link>https://tarrragon.github.io/blog/devops/infra/03-network-foundation/hands-on/podman-rootless-ports-walkthrough/</link><pubDate>Mon, 26 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/devops/infra/03-network-foundation/hands-on/podman-rootless-ports-walkthrough/</guid><description>&lt;blockquote>
&lt;p>&lt;strong>事件說明&lt;/strong>：2026-10-26，在本機 macOS 的 OrbStack 2.2.5 建的兩台機器上實測（Arm 架構，核心是 OrbStack 自己的 &lt;code>7.2.12-orbstack&lt;/code>）。一台是 Ubuntu 24.04.5：ufw 0.36.2、iptables 1.8.10（nf_tables 後端）、Podman 4.9.3 與 netavark 1.4.0、slirp4netns 1.2.1、Docker CE 29.9.2 加 &lt;code>docker-ce-rootless-extras&lt;/code>。另一台是 Oracle Linux 10.2：firewalld 2.4.3（nftables 後端）、Podman 5.8.2（netavark）、Docker CE 29.9.2。測試容器是 &lt;code>busybox:1.37&lt;/code>。&lt;/p>&lt;/blockquote>
&lt;p>這篇記錄 Docker 以外的兩種容器執行方式發佈連接埠時，主機防火牆管不管得到：rootful Podman（以 root 執行、用 netavark 寫防火牆規則）在 Ubuntu 的 ufw 與 Oracle Linux 的 firewalld 上得到相反結果的原因、ufw 的 route 規則放行它的寫法；rootless Docker 與 rootless Podman（以一般使用者執行，核心的網路設定由使用者命名空間裡的程式代辦）發佈的連接埠由主機上的程式接收、受主機防火牆的入站規則管，以及代價：容器看到的來源位址不是真正的來源；最後是清理。以 root 執行的 Docker 在 ufw 主機上的行為見 &lt;a href="https://tarrragon.github.io/blog/devops/infra/03-network-foundation/hands-on/ufw-docker-walkthrough/" data-link-title="ufw 主機上 Docker 發佈連接埠的實測紀錄：ufw 規則的效果、DOCKER-USER 規則的存續與 after.rules、IPv6 的 docker-proxy、compose 內建 DNS 的出站路徑" data-link-desc="在 Ubuntu 的 ufw 主機上用 Docker 發佈連接埠、想只放行 Cloudflare 的位址、或想限制容器送出的 DNS 查詢時，查 ufw 規則對發佈連接埠的效果、DOCKER-USER 規則在 reload 與重開機之後還在不在、IPv6 那一份發佈由誰接收，以及 compose 容器的 DNS 封包從哪裡送出">ufw 主機上 Docker 發佈連接埠的實測紀錄：ufw 規則的效果、DOCKER-USER 規則的存續與 after.rules、IPv6 的 docker-proxy、compose 內建 DNS 的出站路徑&lt;/a>，在 firewalld 主機上見 &lt;a href="https://tarrragon.github.io/blog/devops/infra/03-network-foundation/hands-on/origin-firewall-walkthrough/" data-link-title="源站防火牆只放行 Cloudflare 位址的實測紀錄：firewalld、Docker 發佈的連接埠、DOCKER-USER、nftables 表與出站 DNS" data-link-desc="要在 Linux 主機上設定只放行 Cloudflare 位址的防火牆、確認 Docker 發佈的連接埠有沒有繞過 firewalld、在 DOCKER-USER、StrictForwardPorts 與 nftables 表之間選一種限制容器的來源、或限制出站 DNS 時，查 Oracle Linux 10 上實際跑過的指令、輸出，以及規則在重新載入、dockerd 重啟與重開機之後還在不在">源站防火牆只放行 Cloudflare 位址的實測紀錄：firewalld、Docker 發佈的連接埠、DOCKER-USER、nftables 表與出站 DNS&lt;/a>，只放行 CDN 位址的取捨見概念文章 &lt;a href="https://tarrragon.github.io/blog/devops/infra/03-network-foundation/origin-host-firewall/" data-link-title="CDN 後面的源站主機防火牆：只放行 CDN 來源的位置與套用時機、Docker 發佈連接埠的封包路徑、出站 DNS 的限制與地理封鎖" data-link-desc="網站放在 CDN（例如 Cloudflare）後面時，源站主機的入站與出站限制：請求避開 CDN 直連源站的成因、來源限制放在雲端防火牆、主機防火牆或 CDN 端驗證的差別、只放行 CDN 位址的套用時機、Docker 發佈的連接埠繞過 ufw 與 firewalld 的路徑與各種限制寫法、CDN 位址送來的請求為什麼不等於 CDN 替自己的網域轉送、出站 DNS 限制的效果與漏洞、地理封鎖放在 CDN 邊緣的理由">CDN 後面的源站主機防火牆：只放行 CDN 來源的位置與套用時機、Docker 發佈連接埠的封包路徑、出站 DNS 的限制與地理封鎖&lt;/a>。Kubernetes 的 NodePort 沒有測。&lt;/p>
&lt;h2 id="前置條件與測試來源">前置條件與測試來源&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>兩台主機&lt;/strong>：一台用 ufw 的 Ubuntu、一台用 firewalld 的 RHEL 系（這次是 Oracle Linux）。OrbStack 的建法是 &lt;code>orb create ubuntu:noble verify-ufw&lt;/code> 與 &lt;code>orb create oracle:10 verify-fwd&lt;/code>。兩台的 Docker 都照 Docker 官方套件庫的步驟裝，Podman 用發行版自己的套件（&lt;code>apt-get install podman&lt;/code>、&lt;code>dnf install podman&lt;/code>），所以兩台的 Podman 版本不同（4.9.3 與 5.8.2）。下文 rootful Podman 在兩台的結果不同，成因可能是防火牆、也可能是 Podman 的版本，這次沒有拿同一個版本在兩種防火牆上比較來分開兩者。&lt;/li>
&lt;li>&lt;strong>兩個測試來源&lt;/strong>：每台都照 &lt;a href="https://tarrragon.github.io/blog/devops/infra/03-network-foundation/hands-on/ufw-docker-walkthrough/" data-link-title="ufw 主機上 Docker 發佈連接埠的實測紀錄：ufw 規則的效果、DOCKER-USER 規則的存續與 after.rules、IPv6 的 docker-proxy、compose 內建 DNS 的出站路徑" data-link-desc="在 Ubuntu 的 ufw 主機上用 Docker 發佈連接埠、想只放行 Cloudflare 的位址、或想限制容器送出的 DNS 查詢時，查 ufw 規則對發佈連接埠的效果、DOCKER-USER 規則在 reload 與重開機之後還在不在、IPv6 那一份發佈由誰接收，以及 compose 容器的 DNS 封包從哪裡送出">ufw 主機上 Docker 發佈連接埠的實測紀錄：ufw 規則的效果、DOCKER-USER 規則的存續與 after.rules、IPv6 的 docker-proxy、compose 內建 DNS 的出站路徑&lt;/a> 的〈共用的函式與兩個測試來源〉建 &lt;code>verify-cf&lt;/code>（&lt;code>173.245.48.10&lt;/code>，在 Cloudflare 範圍內）與 &lt;code>verify-other&lt;/code>（&lt;code>203.0.113.10&lt;/code>，範圍外）兩個 &lt;a href="https://tarrragon.github.io/blog/devops/infra/knowledge-cards/network-namespace/" data-link-title="Network Namespace（網路命名空間）" data-link-desc="容器的網路為什麼跟主機分開、要在一台主機上模擬另一個來源位址做防火牆實測、或看到 ip netns 與 veth 時，查 Linux 核心的網路隔離單位與它跟 Kubernetes namespace 的差別">network namespace&lt;/a>，用同一個 &lt;code>probe &amp;lt;連接埠&amp;gt;&lt;/code> 函式從兩個來源各連一次。&lt;code>rc=0 200&lt;/code> 是連上，&lt;code>rc=7&lt;/code> 是立即回拒，&lt;code>rc=28 000&lt;/code> 是逾時（兩種失敗的分辨見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/connection-refused-vs-timeout/" data-link-title="Connection Refused vs. Timeout（連線被拒與逾時）" data-link-desc="遠端連不上、要判斷是封包到不了還是服務拒絕、決定往網路層還是服務層除錯時回來讀">Connection Refused vs. Timeout（連線被拒與逾時）&lt;/a>）。&lt;/li>
&lt;li>&lt;strong>網頁檔&lt;/strong>：&lt;code>/var/tmp/verify-fw/www/index.html&lt;/code>，內容隨意（&lt;code>mkdir -p /var/tmp/verify-fw/www; echo ok &amp;gt; /var/tmp/verify-fw/www/index.html&lt;/code>）。&lt;/li>
&lt;li>&lt;strong>起始狀態&lt;/strong>：Ubuntu 那一台從 ufw 那一篇的〈DOCKER-USER 規則的效果…〉之前的狀態接著做：ufw 已經啟用（&lt;code>ufw default deny incoming&lt;/code>、&lt;code>ufw --force enable&lt;/code>；雲端主機先 &lt;code>ufw allow 22/tcp&lt;/code>），Docker 的 &lt;code>verify-web&lt;/code> 發佈著 8081（下文比較 Docker 與 Podman 時用到）。從一台新機器開始時，照那一篇的〈共用的函式與兩個測試來源〉與〈ufw 的入站規則與 route 規則對發佈連接埠的效果〉的前兩個區塊做到這個狀態。Oracle Linux 那一台只要 firewalld 執行中、&lt;code>public&lt;/code> zone 是預設值。&lt;/li>
&lt;/ul>
&lt;h2 id="rootful-podman-在-ufw-上netavark-的轉送鏈ufw-的預設-routed-政策route-規則與-docker-user">rootful Podman 在 ufw 上：netavark 的轉送鏈、ufw 的預設 routed 政策、route 規則與 DOCKER-USER&lt;/h2>
&lt;p>ufw 已經啟用、預設拒絕入站，&lt;code>default routed&lt;/code> 是 Ubuntu 預設的 &lt;code>deny&lt;/code>（&lt;code>ufw default deny routed&lt;/code>），沒有任何一條規則提到 8082：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p><strong>事件說明</strong>：2026-10-26，在本機 macOS 的 OrbStack 2.2.5 建的兩台機器上實測（Arm 架構，核心是 OrbStack 自己的 <code>7.2.12-orbstack</code>）。一台是 Ubuntu 24.04.5：ufw 0.36.2、iptables 1.8.10（nf_tables 後端）、Podman 4.9.3 與 netavark 1.4.0、slirp4netns 1.2.1、Docker CE 29.9.2 加 <code>docker-ce-rootless-extras</code>。另一台是 Oracle Linux 10.2：firewalld 2.4.3（nftables 後端）、Podman 5.8.2（netavark）、Docker CE 29.9.2。測試容器是 <code>busybox:1.37</code>。</p></blockquote>
<p>這篇記錄 Docker 以外的兩種容器執行方式發佈連接埠時，主機防火牆管不管得到：rootful Podman（以 root 執行、用 netavark 寫防火牆規則）在 Ubuntu 的 ufw 與 Oracle Linux 的 firewalld 上得到相反結果的原因、ufw 的 route 規則放行它的寫法；rootless Docker 與 rootless Podman（以一般使用者執行，核心的網路設定由使用者命名空間裡的程式代辦）發佈的連接埠由主機上的程式接收、受主機防火牆的入站規則管，以及代價：容器看到的來源位址不是真正的來源；最後是清理。以 root 執行的 Docker 在 ufw 主機上的行為見 <a href="/blog/devops/infra/03-network-foundation/hands-on/ufw-docker-walkthrough/" data-link-title="ufw 主機上 Docker 發佈連接埠的實測紀錄：ufw 規則的效果、DOCKER-USER 規則的存續與 after.rules、IPv6 的 docker-proxy、compose 內建 DNS 的出站路徑" data-link-desc="在 Ubuntu 的 ufw 主機上用 Docker 發佈連接埠、想只放行 Cloudflare 的位址、或想限制容器送出的 DNS 查詢時，查 ufw 規則對發佈連接埠的效果、DOCKER-USER 規則在 reload 與重開機之後還在不在、IPv6 那一份發佈由誰接收，以及 compose 容器的 DNS 封包從哪裡送出">ufw 主機上 Docker 發佈連接埠的實測紀錄：ufw 規則的效果、DOCKER-USER 規則的存續與 after.rules、IPv6 的 docker-proxy、compose 內建 DNS 的出站路徑</a>，在 firewalld 主機上見 <a href="/blog/devops/infra/03-network-foundation/hands-on/origin-firewall-walkthrough/" data-link-title="源站防火牆只放行 Cloudflare 位址的實測紀錄：firewalld、Docker 發佈的連接埠、DOCKER-USER、nftables 表與出站 DNS" data-link-desc="要在 Linux 主機上設定只放行 Cloudflare 位址的防火牆、確認 Docker 發佈的連接埠有沒有繞過 firewalld、在 DOCKER-USER、StrictForwardPorts 與 nftables 表之間選一種限制容器的來源、或限制出站 DNS 時，查 Oracle Linux 10 上實際跑過的指令、輸出，以及規則在重新載入、dockerd 重啟與重開機之後還在不在">源站防火牆只放行 Cloudflare 位址的實測紀錄：firewalld、Docker 發佈的連接埠、DOCKER-USER、nftables 表與出站 DNS</a>，只放行 CDN 位址的取捨見概念文章 <a href="/blog/devops/infra/03-network-foundation/origin-host-firewall/" data-link-title="CDN 後面的源站主機防火牆：只放行 CDN 來源的位置與套用時機、Docker 發佈連接埠的封包路徑、出站 DNS 的限制與地理封鎖" data-link-desc="網站放在 CDN（例如 Cloudflare）後面時，源站主機的入站與出站限制：請求避開 CDN 直連源站的成因、來源限制放在雲端防火牆、主機防火牆或 CDN 端驗證的差別、只放行 CDN 位址的套用時機、Docker 發佈的連接埠繞過 ufw 與 firewalld 的路徑與各種限制寫法、CDN 位址送來的請求為什麼不等於 CDN 替自己的網域轉送、出站 DNS 限制的效果與漏洞、地理封鎖放在 CDN 邊緣的理由">CDN 後面的源站主機防火牆：只放行 CDN 來源的位置與套用時機、Docker 發佈連接埠的封包路徑、出站 DNS 的限制與地理封鎖</a>。Kubernetes 的 NodePort 沒有測。</p>
<h2 id="前置條件與測試來源">前置條件與測試來源</h2>
<ul>
<li><strong>兩台主機</strong>：一台用 ufw 的 Ubuntu、一台用 firewalld 的 RHEL 系（這次是 Oracle Linux）。OrbStack 的建法是 <code>orb create ubuntu:noble verify-ufw</code> 與 <code>orb create oracle:10 verify-fwd</code>。兩台的 Docker 都照 Docker 官方套件庫的步驟裝，Podman 用發行版自己的套件（<code>apt-get install podman</code>、<code>dnf install podman</code>），所以兩台的 Podman 版本不同（4.9.3 與 5.8.2）。下文 rootful Podman 在兩台的結果不同，成因可能是防火牆、也可能是 Podman 的版本，這次沒有拿同一個版本在兩種防火牆上比較來分開兩者。</li>
<li><strong>兩個測試來源</strong>：每台都照 <a href="/blog/devops/infra/03-network-foundation/hands-on/ufw-docker-walkthrough/" data-link-title="ufw 主機上 Docker 發佈連接埠的實測紀錄：ufw 規則的效果、DOCKER-USER 規則的存續與 after.rules、IPv6 的 docker-proxy、compose 內建 DNS 的出站路徑" data-link-desc="在 Ubuntu 的 ufw 主機上用 Docker 發佈連接埠、想只放行 Cloudflare 的位址、或想限制容器送出的 DNS 查詢時，查 ufw 規則對發佈連接埠的效果、DOCKER-USER 規則在 reload 與重開機之後還在不在、IPv6 那一份發佈由誰接收，以及 compose 容器的 DNS 封包從哪裡送出">ufw 主機上 Docker 發佈連接埠的實測紀錄：ufw 規則的效果、DOCKER-USER 規則的存續與 after.rules、IPv6 的 docker-proxy、compose 內建 DNS 的出站路徑</a> 的〈共用的函式與兩個測試來源〉建 <code>verify-cf</code>（<code>173.245.48.10</code>，在 Cloudflare 範圍內）與 <code>verify-other</code>（<code>203.0.113.10</code>，範圍外）兩個 <a href="/blog/devops/infra/knowledge-cards/network-namespace/" data-link-title="Network Namespace（網路命名空間）" data-link-desc="容器的網路為什麼跟主機分開、要在一台主機上模擬另一個來源位址做防火牆實測、或看到 ip netns 與 veth 時，查 Linux 核心的網路隔離單位與它跟 Kubernetes namespace 的差別">network namespace</a>，用同一個 <code>probe &lt;連接埠&gt;</code> 函式從兩個來源各連一次。<code>rc=0  200</code> 是連上，<code>rc=7</code> 是立即回拒，<code>rc=28  000</code> 是逾時（兩種失敗的分辨見 <a href="/blog/linux/dotfile/knowledge-cards/connection-refused-vs-timeout/" data-link-title="Connection Refused vs. Timeout（連線被拒與逾時）" data-link-desc="遠端連不上、要判斷是封包到不了還是服務拒絕、決定往網路層還是服務層除錯時回來讀">Connection Refused vs. Timeout（連線被拒與逾時）</a>）。</li>
<li><strong>網頁檔</strong>：<code>/var/tmp/verify-fw/www/index.html</code>，內容隨意（<code>mkdir -p /var/tmp/verify-fw/www; echo ok &gt; /var/tmp/verify-fw/www/index.html</code>）。</li>
<li><strong>起始狀態</strong>：Ubuntu 那一台從 ufw 那一篇的〈DOCKER-USER 規則的效果…〉之前的狀態接著做：ufw 已經啟用（<code>ufw default deny incoming</code>、<code>ufw --force enable</code>；雲端主機先 <code>ufw allow 22/tcp</code>），Docker 的 <code>verify-web</code> 發佈著 8081（下文比較 Docker 與 Podman 時用到）。從一台新機器開始時，照那一篇的〈共用的函式與兩個測試來源〉與〈ufw 的入站規則與 route 規則對發佈連接埠的效果〉的前兩個區塊做到這個狀態。Oracle Linux 那一台只要 firewalld 執行中、<code>public</code> zone 是預設值。</li>
</ul>
<h2 id="rootful-podman-在-ufw-上netavark-的轉送鏈ufw-的預設-routed-政策route-規則與-docker-user">rootful Podman 在 ufw 上：netavark 的轉送鏈、ufw 的預設 routed 政策、route 規則與 DOCKER-USER</h2>
<p>ufw 已經啟用、預設拒絕入站，<code>default routed</code> 是 Ubuntu 預設的 <code>deny</code>（<code>ufw default deny routed</code>），沒有任何一條規則提到 8082：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">podman run -d --name verify-pod-web -p 8082:80 -v /var/tmp/verify-fw/www:/www:ro <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  docker.io/library/busybox:1.37 httpd -f -p <span class="m">80</span> -h /www
</span></span><span class="line"><span class="ln">3</span><span class="cl">probe <span class="m">8082</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">ss -ltnp <span class="p">|</span> grep <span class="m">8082</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">iptables -t nat -S <span class="p">|</span> grep <span class="s1">&#39;dport 8082 -j DNAT&#39;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">iptables -S NETAVARK_FORWARD</span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">  verify-cf     -&gt; :8082  rc=28  000
</span></span><span class="line"><span class="ln">2</span><span class="cl">  verify-other  -&gt; :8082  rc=28  000
</span></span><span class="line"><span class="ln">3</span><span class="cl">LISTEN 0  4096   0.0.0.0:8082   0.0.0.0:*   users:((&#34;conmon&#34;,pid=2929,fd=5))
</span></span><span class="line"><span class="ln">4</span><span class="cl">-A NETAVARK-DN-1D8721804F16F -p tcp -m tcp --dport 8082 -j DNAT --to-destination 10.88.0.2:80
</span></span><span class="line"><span class="ln">5</span><span class="cl">-A NETAVARK_FORWARD -d 10.88.0.0/16 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
</span></span><span class="line"><span class="ln">6</span><span class="cl">-A NETAVARK_FORWARD -s 10.88.0.0/16 -j ACCEPT</span></span></code></pre></div><p>Podman 用全名 <code>docker.io/library/busybox:1.37</code>，因為它不像 Docker 預設到 Docker Hub 找短名稱的映像。netavark 跟 Docker 一樣用 <a href="/blog/devops/infra/knowledge-cards/dnat/" data-link-title="DNAT（目的位址轉換）" data-link-desc="容器發佈的連接埠沒被主機防火牆擋下、port forwarding 的限制規則要寫在位址轉換之前還是之後、或要比對位址轉換之前的連接埠時，查 DNAT 改寫封包目的地的時間點與它對防火牆規則的影響">DNAT</a> 把 8082 改寫成容器的 80，封包走轉送路徑。差別在轉送鏈：netavark 的 <code>NETAVARK_FORWARD</code> 只放行「送往容器網路的回程」與「容器送出的」，新的入站連線不在其中，封包往下落到 ufw 的鏈，最後被 FORWARD 的預設政策 DROP 丟掉，兩個來源都逾時。<code>conmon</code>（Podman 監看容器的程式）聽著 <code>0.0.0.0:8082</code>，它只是佔住這個連接埠，連線不經過它。</p>
<p>實測的順序跟上面相反：容器建好時這台的 routed 是 <code>allow</code>（FORWARD 的預設政策是 ACCEPT），第一次 <code>probe 8082</code> 兩個來源都拿到 200；改成 <code>deny routed</code> 之後才量到上面的兩個逾時。所以 routed 政策是 <code>allow</code> 時兩個來源都連得到，是 <code>deny</code> 時都連不到。</p>
<p>要只放行 Cloudflare，用 ufw 的 route 規則寫在轉送路徑上，目的地寫容器的位址與容器內的連接埠（位址轉換之後的值）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">ufw default deny routed
</span></span><span class="line"><span class="ln">2</span><span class="cl">ufw route allow proto tcp from 173.245.48.0/20 to 10.88.0.2 port <span class="m">80</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">probe <span class="m">8082</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">  verify-cf     -&gt; :8082  rc=0  200
</span></span><span class="line"><span class="ln">2</span><span class="cl">  verify-other  -&gt; :8082  rc=28  000</span></span></code></pre></div><p>只有範圍內的來源連得到。<code>10.88.0.2</code> 是容器的位址，取自上面那條 DNAT 規則的 <code>--to-destination</code>；容器重建之後位址可能變，規則要跟著改，這是 route 規則寫在位址轉換之後的代價。同一台主機上 Docker 的 8081 在 <code>deny routed</code> 下照樣對兩個來源開放（Docker 的 <code>DOCKER-FORWARD</code> 排在 ufw 的鏈之前、先接受了），所以同一台主機上 Docker 與 Podman 對同一條 ufw 設定的結果相反。</p>
<p>主機上也裝了 Docker 時，Podman 的轉送封包也經過 <a href="/blog/devops/infra/knowledge-cards/docker-user/" data-link-title="DOCKER-USER（Docker 留給使用者的轉送規則鏈）" data-link-desc="要在 Docker 發佈的連接埠上加來源限制、寫進 DOCKER-USER 的規則在防火牆重新載入或重開機之後不見、或規則擋到了容器自己往外的連線時，查這條鏈在轉送路徑上的位置、比對原始連接埠的寫法與規則的存續條件"><code>DOCKER-USER</code></a>。為了只看 <code>DOCKER-USER</code> 的效果，先把 route 規則與 8082 的容器拿掉、routed 改回 <code>allow</code>（否則 FORWARD 的 DROP 會先把兩個來源都擋下），再發佈一個 8089 並看跳轉順序：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">podman rm -f verify-pod-web
</span></span><span class="line"><span class="ln">2</span><span class="cl">ufw route delete allow proto tcp from 173.245.48.0/20 to 10.88.0.2 port <span class="m">80</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">ufw default allow routed
</span></span><span class="line"><span class="ln">4</span><span class="cl">podman run -d --name verify-pod-web -p 8089:80 -v /var/tmp/verify-fw/www:/www:ro <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  docker.io/library/busybox:1.37 httpd -f -v -p <span class="m">80</span> -h /www
</span></span><span class="line"><span class="ln">6</span><span class="cl">iptables -S FORWARD <span class="p">|</span> grep -- <span class="s1">&#39;-j&#39;</span> <span class="p">|</span> head -3
</span></span><span class="line"><span class="ln">7</span><span class="cl">iptables -I DOCKER-USER -p tcp -m conntrack --ctorigdstport <span class="m">8089</span> --ctdir ORIGINAL ! -s 173.245.48.0/20 -j DROP
</span></span><span class="line"><span class="ln">8</span><span class="cl">probe <span class="m">8089</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl">iptables -D DOCKER-USER -p tcp -m conntrack --ctorigdstport <span class="m">8089</span> --ctdir ORIGINAL ! -s 173.245.48.0/20 -j DROP</span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">-A FORWARD -m comment --comment &#34;netavark firewall plugin rules&#34; -j NETAVARK_FORWARD
</span></span><span class="line"><span class="ln">2</span><span class="cl">-A FORWARD -j DOCKER-USER
</span></span><span class="line"><span class="ln">3</span><span class="cl">-A FORWARD -j DOCKER-FORWARD
</span></span><span class="line"><span class="ln">4</span><span class="cl">  verify-cf     -&gt; :8089  rc=0  200
</span></span><span class="line"><span class="ln">5</span><span class="cl">  verify-other  -&gt; :8089  rc=28  000</span></span></code></pre></div><p>FORWARD 先跳 <code>NETAVARK_FORWARD</code>，那裡不接受新的入站連線，封包回到 FORWARD 往下走，接著跳 Docker 的 <code>DOCKER-USER</code>；這一條跳轉沒有比對條件，所有轉送的封包都會進去。所以同一條比對原始目的連接埠的規則，對 Podman 發佈的連接埠一樣有效，範圍外的來源被擋。主機上沒有裝 Docker 時沒有 <code>DOCKER-USER</code> 這條鏈，這個寫法用不了（依鏈的建立者推論，沒有在沒裝 Docker 的主機上實跑）。量完以 <code>ufw default deny routed</code> 改回 routed 政策。</p>
<h2 id="rootful-podman-在-firewalld-上netavark-的-nftables-表trusted-zone-與-firewalld-的轉送鏈">rootful Podman 在 firewalld 上：netavark 的 nftables 表、trusted zone 與 firewalld 的轉送鏈</h2>
<p>Oracle Linux 那一台的 <code>public</code> zone 沒有任何規則提到 8089：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">podman run -d --name verify-pod-web -p 8089:80 -v /var/tmp/verify-fw/www:/www:ro,Z <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  docker.io/library/busybox:1.37 httpd -f -p <span class="m">80</span> -h /www
</span></span><span class="line"><span class="ln">3</span><span class="cl">probe <span class="m">8089</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">nft list tables
</span></span><span class="line"><span class="ln">5</span><span class="cl">firewall-cmd --get-active-zones
</span></span><span class="line"><span class="ln">6</span><span class="cl">nft list chain inet netavark FORWARD
</span></span><span class="line"><span class="ln">7</span><span class="cl">nft list chain inet firewalld filter_FORWARD <span class="p">|</span> sed -n 2,5p
</span></span><span class="line"><span class="ln">8</span><span class="cl">iptables -S FORWARD <span class="p">|</span> head -1</span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">  verify-cf     -&gt; :8089  rc=0  200
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  verify-other  -&gt; :8089  rc=0  200
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">table ip nat
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">table ip filter
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">table ip6 nat
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">table ip6 filter
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">table ip raw
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">table inet firewalld
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">table inet netavark
</span></span><span class="line"><span class="ln">10</span><span class="cl">docker
</span></span><span class="line"><span class="ln">11</span><span class="cl">  interfaces: docker0
</span></span><span class="line"><span class="ln">12</span><span class="cl">public (default)
</span></span><span class="line"><span class="ln">13</span><span class="cl">  interfaces: eth0
</span></span><span class="line"><span class="ln">14</span><span class="cl">trusted
</span></span><span class="line"><span class="ln">15</span><span class="cl">  sources: 10.88.0.0/16
</span></span><span class="line"><span class="ln">16</span><span class="cl">	chain FORWARD {
</span></span><span class="line"><span class="ln">17</span><span class="cl">		type filter hook forward priority filter; policy accept;
</span></span><span class="line"><span class="ln">18</span><span class="cl">		jump NETAVARK-ISOLATION-1
</span></span><span class="line"><span class="ln">19</span><span class="cl">		ip daddr 10.88.0.0/16 ct state established,related accept
</span></span><span class="line"><span class="ln">20</span><span class="cl">		ip saddr 10.88.0.0/16 accept
</span></span><span class="line"><span class="ln">21</span><span class="cl">	}
</span></span><span class="line"><span class="ln">22</span><span class="cl">		type filter hook forward priority filter + 10; policy accept;
</span></span><span class="line"><span class="ln">23</span><span class="cl">		ct state { established, related } accept
</span></span><span class="line"><span class="ln">24</span><span class="cl">		ct status dnat accept
</span></span><span class="line"><span class="ln">25</span><span class="cl">		iifname &#34;lo&#34; accept
</span></span><span class="line"><span class="ln">26</span><span class="cl">-P FORWARD ACCEPT</span></span></code></pre></div><p><code>-v</code> 的 <code>:Z</code> 讓 Podman 替掛進去的目錄改 SELinux 標籤（Oracle Linux 預設啟用 SELinux）；Ubuntu 沒有 SELinux，不需要。兩個來源都連得到。Podman 5.8.2 的 netavark 在這台用 nftables 寫自己的表 <code>inet netavark</code>，並把容器網路 <code>10.88.0.0/16</code> 加進 firewalld 的 <code>trusted</code> zone 的來源。一個轉送的封包要通過每一張表接在 forward hook 上的鏈，三張都放它過去：netavark 的 FORWARD 鏈預設政策是 accept、iptables 的 FORWARD 鏈（這台裝了 Docker，表是 Docker 經由 iptables-nft 建的）預設政策是 ACCEPT、firewalld 的轉送鏈有 <code>ct status dnat accept</code>。</p>
<p>Ubuntu 那一台被擋，是因為 ufw 把 iptables 的 FORWARD 預設政策設成 DROP；這一台的 firewalld 對經過位址轉換的連線一律接受。兩台的 Podman 版本也不同（見〈前置條件與測試來源〉），版本與防火牆的影響沒有分開。限制的寫法依同樣的路徑推論跟 Docker 在 firewalld 上的相同（自己的 nftables 表掛在位址轉換之前、雲端防火牆），沒有對 Podman 實跑。這台也裝了 Docker，iptables 的 FORWARD 鏈有跳到 <code>DOCKER-USER</code> 的那一條，依 Ubuntu 那一台的結果推論 <code>DOCKER-USER</code> 的規則同樣管得到 Podman 的轉送封包，在這一台沒有實跑。firewalld 的 <code>StrictForwardPorts=yes</code> 對 Podman 的效果沒有測。</p>
<p>從兩個帶 IPv6 位址的來源對主機的 IPv6 位址連 8089（namespace 加 IPv6 位址的做法同 ufw 那一篇的〈IPv6 那一份發佈：docker-proxy 的接收路徑與 ufw 入站規則的效果〉），兩個都立即被回拒：<code>ss -ltnp</code> 裡 <code>conmon</code> 只聽 <code>0.0.0.0:8089</code>，Podman 沒有替 IPv6 發佈這個連接埠，連線落到 <code>public</code> zone 的回拒。</p>
<h2 id="rootless-dockerrootlesskit-的接收路徑ufw-入站規則與容器看到的來源位址">rootless Docker：rootlesskit 的接收路徑、ufw 入站規則與容器看到的來源位址</h2>
<p>rootless 模式的 dockerd 以一般使用者執行，它不能改主機的 iptables，發佈連接埠改由 RootlessKit（rootless 模式替 dockerd 代辦網路的程式，屬於 <a href="/blog/devops/infra/knowledge-cards/userland-proxy/" data-link-title="Userland Proxy（使用者空間的連接埠轉送程式）" data-link-desc="ss 列出 docker-proxy、rootlesskit 或 rootlessport 聽著容器發佈的連接埠、主機防火牆的入站規則對容器的連接埠有效、或容器的存取紀錄只看得到閘道位址而沒有真實來源時，查這類轉送程式接收連線的路徑與它換掉來源位址的代價">Userland Proxy</a> 這一類）在主機上聽那個連接埠、收下連線再轉進容器。安裝與啟動（以 root 建使用者、以那個使用者執行設定工具）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl">apt-get install -y docker-ce-rootless-extras uidmap
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">useradd -m -s /bin/bash verifyrl
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">loginctl enable-linger verifyrl             <span class="c1"># 讓這個使用者的 systemd user 服務在沒有登入時也執行</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nv">U</span><span class="o">=</span>verifyrl<span class="p">;</span> <span class="nv">X</span><span class="o">=</span>/run/user/<span class="k">$(</span>id -u <span class="nv">$U</span><span class="k">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="nb">cd</span> /tmp                                     <span class="c1"># 一般使用者讀不了 /root，原因寫在指令之後</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">sudo -u <span class="nv">$U</span> <span class="nv">XDG_RUNTIME_DIR</span><span class="o">=</span><span class="nv">$X</span> <span class="nv">DBUS_SESSION_BUS_ADDRESS</span><span class="o">=</span>unix:path<span class="o">=</span><span class="nv">$X</span>/bus dockerd-rootless-setuptool.sh install
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">sudo -u <span class="nv">$U</span> <span class="nv">XDG_RUNTIME_DIR</span><span class="o">=</span><span class="nv">$X</span> <span class="nv">DOCKER_HOST</span><span class="o">=</span>unix://<span class="nv">$X</span>/docker.sock bash -lc <span class="s1">&#39;cd ~; mkdir -p www; echo ok &gt; www/index.html;
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s1">  docker run -d --name verify-rl-web -p 8083:80 -v ~/www:/www:ro busybox:1.37 httpd -f -v -p 80 -h /www&#39;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">probe <span class="m">8083</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">ss -ltnp <span class="p">|</span> grep 8083<span class="p">;</span> iptables -t nat -S <span class="p">|</span> grep -c <span class="m">8083</span></span></span></code></pre></div><p><code>verifyrl</code> 是這次建的測試使用者，換成實際要跑 rootless Docker 的帳號。<code>XDG_RUNTIME_DIR</code> 與 <code>DBUS_SESSION_BUS_ADDRESS</code> 指向那個使用者的 systemd user 執行環境，設定工具要用它們建立 user 服務；<code>DOCKER_HOST</code> 指到 rootless dockerd 的 socket，否則 <code>docker</code> 指令會去連 root 的那一個。httpd 的 <code>-v</code> 讓它逐筆印存取紀錄，下面用它看容器收到的來源位址。這台 OrbStack 的核心沒有 AppArmor（<code>aa-status</code> 回 <code>apparmor not present</code>），所以碰不到 Ubuntu 24.04 對非特權 user namespace 的限制；Docker 的〈Rootless mode troubleshooting〉寫明，在 Ubuntu 24.04 以後的發行版核心上，以 deb 套件裝 <code>docker-ce-rootless-extras</code> 時 rootlesskit 的 AppArmor profile 已經隨 <code>apparmor</code> 套件附上（這台的 <code>/etc/apparmor.d/</code> 也有 <code>rootlesskit</code> 這個檔），改用安裝腳本裝時要照那一頁手動加 profile，在發行版核心上沒有實跑。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">  verify-cf     -&gt; :8083  rc=28  000
</span></span><span class="line"><span class="ln">2</span><span class="cl">  verify-other  -&gt; :8083  rc=28  000
</span></span><span class="line"><span class="ln">3</span><span class="cl">LISTEN 0  4096   0.0.0.0:8083   0.0.0.0:*   users:((&#34;rootlesskit&#34;,pid=4085,fd=13))
</span></span><span class="line"><span class="ln">4</span><span class="cl">LISTEN 0  4096      [::]:8083      [::]:*   users:((&#34;rootlesskit&#34;,pid=4085,fd=15))
</span></span><span class="line"><span class="ln">5</span><span class="cl">0</span></span></code></pre></div><p>nat 表裡沒有 8083 的位址轉換（<code>0</code>），聽 8083 的是主機上的 <code>rootlesskit</code>。連線送到主機上的程式，走的是 input 路徑，ufw 的預設拒絕入站擋下兩個來源。加一條入站規則：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">ufw allow from 173.245.48.0/20 to any port <span class="m">8083</span> proto tcp
</span></span><span class="line"><span class="ln">2</span><span class="cl">probe <span class="m">8083</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">sudo -u verifyrl <span class="nv">XDG_RUNTIME_DIR</span><span class="o">=</span>/run/user/<span class="k">$(</span>id -u verifyrl<span class="k">)</span> <span class="nv">DOCKER_HOST</span><span class="o">=</span>unix:///run/user/<span class="k">$(</span>id -u verifyrl<span class="k">)</span>/docker.sock <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  bash -lc <span class="s1">&#39;docker logs verify-rl-web 2&gt;&amp;1 | tail -1&#39;</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">  verify-cf     -&gt; :8083  rc=0  200
</span></span><span class="line"><span class="ln">2</span><span class="cl">  verify-other  -&gt; :8083  rc=28  000
</span></span><span class="line"><span class="ln">3</span><span class="cl">[::ffff:172.17.0.1]:54596: response:200</span></span></code></pre></div><p>ufw 的入站規則對 rootless Docker 發佈的連接埠有效。IPv6 那一份也由 rootlesskit 聽著（<code>[::]:8083</code>），依同一條路徑推論受 ufw 的 IPv6 入站規則管，沒有實跑。代價在最後一行：busybox httpd 記下的連線來源是 <code>172.17.0.1</code>（rootless dockerd 自己網路裡的閘道），不是 <code>173.245.48.10</code>。預設設定下來源位址沒有保留，容器裡的網站程式依來源位址做的事（存取紀錄、速率限制、只信任來自 CDN 的 <code>CF-Connecting-IP</code>）都拿不到真正的來源。Docker 的〈Rootless mode troubleshooting〉列了保留來源位址的做法，這次都沒有實跑：RootlessKit v3.0 以後，預設的 <code>builtin</code> 連接埠轉送在 <code>~/.config/docker/daemon.json</code> 寫 <code>{&quot;userland-proxy&quot;: false}</code> 並重啟之後保留來源位址（文件寫明可能還要載入 <code>br_netfilter</code> 模組）；更早的版本改用 <code>slirp4netns</code> 轉送方式（環境變數 <code>DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=slirp4netns</code>），或 <code>pasta</code> 網路加 <code>implicit</code> 轉送方式（<code>DOCKERD_ROOTLESS_ROOTLESSKIT_NET=pasta</code>、<code>DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=implicit</code>，文件標為實驗性、吞吐量比 slirp4netns 好）。環境變數寫在 <code>~/.config/systemd/user/docker.service.d/override.conf</code> 的 <code>[Service]</code> 底下。</p>
<h2 id="rootless-podmanrootlessport-的接收路徑ufw-入站規則與容器看到的來源位址">rootless Podman：rootlessport 的接收路徑、ufw 入站規則與容器看到的來源位址</h2>
<p>網頁檔沿用〈rootless Docker：rootlesskit 的接收路徑、ufw 入站規則與容器看到的來源位址〉在 <code>verifyrl</code> 家目錄建的 <code>~/www</code>；跳過那一節時，在 <code>cd ~;</code> 之後先 <code>mkdir -p www; echo ok &gt; www/index.html</code>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">cd</span> /tmp
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo -u verifyrl <span class="nv">XDG_RUNTIME_DIR</span><span class="o">=</span>/run/user/<span class="k">$(</span>id -u verifyrl<span class="k">)</span> bash -lc <span class="s1">&#39;cd ~;
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s1">  podman run -d --name verify-rlp-web -p 8084:80 -v ~/www:/www:ro docker.io/library/busybox:1.37 httpd -f -v -p 80 -h /www&#39;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">probe 8084<span class="p">;</span> ss -ltnp <span class="p">|</span> grep <span class="m">8084</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">ufw allow from 173.245.48.0/20 to any port <span class="m">8084</span> proto tcp<span class="p">;</span> probe <span class="m">8084</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">sudo -u verifyrl <span class="nv">XDG_RUNTIME_DIR</span><span class="o">=</span>/run/user/<span class="k">$(</span>id -u verifyrl<span class="k">)</span> bash -lc <span class="s1">&#39;cd ~; podman logs verify-rlp-web 2&gt;&amp;1 | tail -1&#39;</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">  verify-cf     -&gt; :8084  rc=28  000
</span></span><span class="line"><span class="ln">2</span><span class="cl">  verify-other  -&gt; :8084  rc=28  000
</span></span><span class="line"><span class="ln">3</span><span class="cl">LISTEN 0  4096   *:8084   *:*   users:((&#34;rootlessport&#34;,pid=4921,fd=11))
</span></span><span class="line"><span class="ln">4</span><span class="cl">  verify-cf     -&gt; :8084  rc=0  200
</span></span><span class="line"><span class="ln">5</span><span class="cl">  verify-other  -&gt; :8084  rc=28  000
</span></span><span class="line"><span class="ln">6</span><span class="cl">[::ffff:10.0.2.100]:59274: response:200</span></span></code></pre></div><p><code>podman logs</code> 印出的存取紀錄裡，來源是 <code>10.0.2.100</code>（<code>::ffff:</code> 是 IPv4 位址寫成 IPv6 格式時的前綴，httpd 聽的是 IPv6 的 socket）。那是 slirp4netns（Podman 4.9 文件寫的 rootless 預設網路）在 rootless 網路命名空間裡那張 tap 介面的位址，rootlessport 從這個位址把連線轉進容器。這台的 <code>podman info</code> 取不出 rootless 網路的種類，預設是 slirp4netns 是依文件，不是從輸出確認的。跟 rootless Docker 一樣，ufw 的入站規則管得到，容器看不到真正的來源。Podman 5 的 rootless 預設網路改成 pasta，pasta 預設保留來源位址（Podman 5.0 的發行說明），沒有實跑。</p>
<p>兩個 rootless 的指令都先 <code>cd /tmp</code>、在使用者的 shell 裡再 <code>cd ~</code>：在 <code>/root</code> 底下以 <code>sudo -u</code> 執行時，第一次跑 rootless Podman 得到 <code>cannot chdir to /root: Permission denied</code> 與 <code>Error: setting up the process</code>，容器沒有啟動。原因是一般使用者讀不了 <code>/root</code>，而 Podman 在這個使用者的身分下要用目前的工作目錄。</p>
<h2 id="清理兩台主機上的容器測試使用者ufw-規則namespace-與暫存目錄">清理：兩台主機上的容器、測試使用者、ufw 規則、namespace 與暫存目錄</h2>
<p>Ubuntu 那一台：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl">podman rm -f verify-pod-web
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nb">cd</span> /tmp
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">sudo -u verifyrl <span class="nv">XDG_RUNTIME_DIR</span><span class="o">=</span>/run/user/<span class="k">$(</span>id -u verifyrl<span class="k">)</span> bash -lc <span class="s1">&#39;cd ~; podman rm -f verify-rlp-web;
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s1">  DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock docker rm -f verify-rl-web;
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s1">  dockerd-rootless-setuptool.sh uninstall&#39;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">loginctl disable-linger verifyrl
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">while</span> pgrep -u verifyrl &gt;/dev/null<span class="p">;</span> <span class="k">do</span> sleep 1<span class="p">;</span> <span class="k">done</span>      <span class="c1"># 等 systemd 停掉這個使用者的程序</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">userdel -r verifyrl
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">ufw delete allow from 173.245.48.0/20 to any port <span class="m">8083</span> proto tcp
</span></span><span class="line"><span class="ln">10</span><span class="cl">ufw delete allow from 173.245.48.0/20 to any port <span class="m">8084</span> proto tcp</span></span></code></pre></div><p>8082 的 route 規則已經在 8089 那一步刪掉。namespace、<code>verify-web</code>、<code>/var/tmp/verify-fw</code> 與其他 ufw 設定，照 ufw 那一篇的〈清理：容器、ufw 規則、after.rules、namespace 與暫存目錄〉清。Oracle Linux 那一台這一篇只建了 <code>verify-pod-web</code> 與兩個 namespace：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">podman rm -f verify-pod-web
</span></span><span class="line"><span class="ln">2</span><span class="cl">firewall-cmd --zone<span class="o">=</span>trusted --list-sources                 <span class="c1"># 還列得出 10.88.0.0/16 時接下一行</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">firewall-cmd --zone<span class="o">=</span>trusted --remove-source<span class="o">=</span>10.88.0.0/16   <span class="c1"># 只移除執行中的設定；沒有實跑</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">ip link del vcf-h<span class="p">;</span> ip link del voth-h<span class="p">;</span> ip netns del verify-cf<span class="p">;</span> ip netns del verify-other
</span></span><span class="line"><span class="ln">5</span><span class="cl">rm -rf /var/tmp/verify-fw</span></span></code></pre></div><p>firewalld 那一篇的清理段刪的是那一篇自己建的資源（靜態的 dockerd、ipset、nftables 表、<code>/var/lib/docker</code>），不要照它整段跑。這兩段清理指令跟實測時的順序不同：實測時兩台的清理分在幾次跑，沒有照這兩段一次跑完。</p>
<p><code>disable-linger</code> 之後，systemd 要過一會兒才停掉那個使用者的 user 服務；實測緊接著跑 <code>userdel</code> 時回 <code>userdel: user verifyrl is currently used by process 2749</code>、使用者沒有刪掉，所以中間等到 <code>pgrep -u verifyrl</code> 沒有輸出。<code>dockerd-rootless-setuptool.sh uninstall</code> 只移除 user 服務，印出的提示寫明不刪 Docker 的資料；<code>userdel -r</code> 刪掉家目錄時，那些資料（<code>~/.local/share/docker</code>）一起刪掉。</p>
<p>firewalld 那一台的 <code>trusted</code> zone 裡 <code>10.88.0.0/16</code> 的來源由 netavark 在容器網路存在時加入，最後一個用到預設網路的容器刪掉之後是否移除沒有確認。留著的影響是之後任何從 <code>10.88.0.0/16</code> 來的連線都由 <code>trusted</code> zone 處理、一律放行；這段位址只在主機內部的 Podman 網路上，外部來源用不到它。OrbStack 的機器整台刪掉時兩台一起刪：<code>orb delete verify-ufw verify-fwd</code>。</p>
<h2 id="rootful-與-rootless-的-dockerpodman接收路徑入站規則與容器看到的來源位址">rootful 與 rootless 的 Docker、Podman：接收路徑、入站規則與容器看到的來源位址</h2>
<table>
  <thead>
      <tr>
          <th>執行方式</th>
          <th>接收連線的路徑</th>
          <th>主機防火牆的入站規則是否管得到</th>
          <th>容器看到的來源位址</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>rootful Docker（參考，見 ufw 與 firewalld 那兩篇）</td>
          <td>IPv4 位址轉換後轉送；IPv6 由 docker-proxy 接收（容器網路沒開 IPv6、userland-proxy 預設開著時）</td>
          <td>IPv4 管不到；IPv6 管得到（ufw 與 firewalld 都量過）</td>
          <td>IPv4 是真正的來源；IPv6 是預設網路的閘道 <code>172.17.0.1</code>（ufw 那一篇量的）</td>
      </tr>
      <tr>
          <td>rootful Podman 4.9.3，ufw</td>
          <td>位址轉換後轉送；routed 是 deny 時被 FORWARD 的 DROP 擋下，是 allow 時放行</td>
          <td>管不到；ufw route 規則管得到，主機也裝了 Docker 時 <code>DOCKER-USER</code> 也管得到</td>
          <td>沒有記錄</td>
      </tr>
      <tr>
          <td>rootful Podman 5.8.2，firewalld</td>
          <td>位址轉換後轉送，firewalld 接受</td>
          <td>管不到</td>
          <td>沒有記錄</td>
      </tr>
      <tr>
          <td>rootless Docker 29.9.2</td>
          <td>主機上的 <code>rootlesskit</code></td>
          <td>管得到</td>
          <td><code>172.17.0.1</code></td>
      </tr>
      <tr>
          <td>rootless Podman 4.9.3</td>
          <td>主機上的 <code>rootlessport</code></td>
          <td>管得到</td>
          <td><code>10.0.2.100</code></td>
      </tr>
  </tbody>
</table>
<p>判斷一種執行方式落在哪一列，先查有沒有規則把那個連接埠的目的地改寫掉，再看誰在聽。改寫規則用 <code>nft list ruleset | grep 'dport &lt;連接埠&gt;'</code> 查：它同時列出經由 iptables-nft 寫的表（Docker、Podman 4.9 的 netavark）與原生 nftables 的表（Podman 5.8.2 的 <code>inet netavark</code>）。只看 <code>iptables -t nat -S</code> 會漏掉後一種：在 Oracle Linux 那一台另發佈一個 8090，<code>iptables -t nat -S | grep -c 8090</code> 回 0，<code>nft list ruleset</code> 列得出 <code>tcp dport 8090 dnat ip to 10.88.0.3:80</code>，同時 <code>ss -ltnp</code> 顯示 <code>conmon</code> 聽著 8090。查到改寫規則時，就是轉送路徑，結果取決於轉送鏈上每一張表的預設政策，聽著那個連接埠的程式（<code>conmon</code>、docker-proxy 的 IPv4 那一份）只是佔住連接埠，不算數；IPv6 的改寫規則（<code>ip6 nat</code> 或 <code>inet</code> 表裡帶 <code>ip6</code> 的那幾條）另外判一次。查不到改寫規則、由一個轉送程式聽著時（rootlesskit、rootlessport、rootful Docker 的 IPv6 那一份聽著的 docker-proxy），才是 input 路徑，入站規則管得到。查表的結果是預測，最終以範圍外的來源實際連一次為準（<code>probe &lt;連接埠&gt;</code>）；Kubernetes 的 NodePort 這類多跳改寫沒有測。</p>
]]></content:encoded></item><item><title>Userland Proxy（使用者空間的連接埠轉送程式）</title><link>https://tarrragon.github.io/blog/devops/infra/knowledge-cards/userland-proxy/</link><pubDate>Mon, 26 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/devops/infra/knowledge-cards/userland-proxy/</guid><description>&lt;p>把連線送進容器有兩種做法。&lt;a href="https://tarrragon.github.io/blog/devops/infra/knowledge-cards/dnat/" data-link-title="DNAT（目的位址轉換）" data-link-desc="容器發佈的連接埠沒被主機防火牆擋下、port forwarding 的限制規則要寫在位址轉換之前還是之後、或要比對位址轉換之前的連接埠時，查 DNAT 改寫封包目的地的時間點與它對防火牆規則的影響">DNAT（目的位址轉換）&lt;/a> 是核心在封包剛進主機時改寫目的地，封包本身被轉送給容器；userland proxy 是一個在主機上執行的使用者空間程式聽著那個連接埠，收下連線之後自己再開一條連線到容器，把兩條連線之間的資料來回搬。常見的三個是 Docker 的 docker-proxy、rootless Docker 的 rootlesskit，以及 rootless Podman 的 rootlessport。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>連線的終點是主機上的一個程式，所以封包走的是 input 路徑（目的地是主機本身程式的那一條），主機防火牆的入站規則（ufw 的入站規則、firewalld 的 zone）管得到它，跟 DNAT 那一類正好相反。代價在容器看到的來源：容器收到的連線是轉送程式開的，來源位址是轉送程式那一端的位址，不是外面的真實來源。這跟 &lt;a href="https://tarrragon.github.io/blog/devops/infra/knowledge-cards/reverse-proxy/" data-link-title="Reverse Proxy" data-link-desc="代替後端服務接收外部請求、再轉發到內部服務的中介層，承擔 TLS 終結、負載平衡與路由分流">Reverse Proxy（反向代理）&lt;/a> 換掉來源位址的道理相同，差別在反向代理常用 HTTP 標頭（&lt;code>X-Forwarded-For&lt;/code>）把原本的來源帶過去，這幾個轉送程式在 TCP 這一層搬資料，什麼都不帶。&lt;/p>
&lt;p>三個程式各在什麼時候出現：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>docker-proxy&lt;/strong>：以 root 執行的 Docker 用 &lt;code>-p&lt;/code> 發佈時，主機上 IPv4 與 IPv6 各有一份；IPv4 那一份從外面來的封包在 prerouting 就被 DNAT 改寫、到不了它，IPv6 那一份在容器網路沒開 IPv6 時由它接收。Docker 的 &lt;code>userland-proxy&lt;/code> 設成 &lt;code>false&lt;/code> 時它不啟動，IPv6 那一份也就沒有發佈（2026-10-26 在 Docker 29.9.2 上實測）。&lt;/li>
&lt;li>&lt;strong>rootlesskit&lt;/strong>：rootless Docker 的 dockerd 以一般使用者執行、不能改主機的 iptables，發佈的連接埠全部由它接聽。&lt;/li>
&lt;li>&lt;strong>rootlessport&lt;/strong>：rootless Podman 的對應程式，網路由 slirp4netns（Podman 4）或 pasta（Podman 5 的預設）提供。&lt;/li>
&lt;/ul>
&lt;h2 id="可觀察訊號與例子">可觀察訊號與例子&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>&lt;code>ss -ltnp&lt;/code> 列出這幾個程式聽著發佈的連接埠，nat 表裡沒有對應的 DNAT&lt;/strong>：&lt;code>users:((&amp;quot;rootlesskit&amp;quot;,pid=4085,fd=13))&lt;/code> 聽著 &lt;code>0.0.0.0:8083&lt;/code>，&lt;code>iptables -t nat -S | grep -c 8083&lt;/code> 回 0。&lt;/li>
&lt;li>&lt;strong>容器的存取紀錄只有閘道位址&lt;/strong>：2026-10-26 的實測裡，經由 docker-proxy 的 IPv6 連線與 rootless Docker 的連線，容器記到的來源都是 &lt;code>172.17.0.1&lt;/code>；rootless Podman 4.9 記到的是 slirp4netns 那張 tap 介面的 &lt;code>10.0.2.100&lt;/code>。&lt;/li>
&lt;li>&lt;strong>主機防火牆的入站規則對容器的連接埠有效&lt;/strong>：ufw 預設拒絕入站時兩個測試來源都連不到 rootless 容器的連接埠，加一條入站放行之後只有被放行的那個連得到。&lt;/li>
&lt;/ul>
&lt;h2 id="判讀方式">判讀方式&lt;/h2>
&lt;p>判斷一個發佈的連接埠屬於哪一類，先查有沒有規則把它的目的地改寫掉，再看誰在聽：&lt;code>nft list ruleset | grep 'dport &amp;lt;連接埠&amp;gt;'&lt;/code> 同時列得出經由 iptables-nft 寫的與原生 nftables 寫的改寫規則。查到改寫規則時是 DNAT 那一類，聽著連接埠的程式只是佔住它（Podman 的 &lt;code>conmon&lt;/code>、docker-proxy 的 IPv4 那一份），不算數；查不到時，聽著的轉送程式才是接收者。查表是預測，最終以從範圍外的來源實際連一次為準。屬於這一類時，限制寫在主機防火牆的入站規則上就有效，而容器裡依來源位址做的判斷（存取紀錄、速率限制、只信任來自 CDN 位址段的 &lt;code>CF-Connecting-IP&lt;/code>）都拿不到真正的來源；Docker 文件列的保留來源位址做法（RootlessKit v3 的 &lt;code>builtin&lt;/code> 加 &lt;code>userland-proxy: false&lt;/code>、slirp4netns 轉送方式、pasta 加 &lt;code>implicit&lt;/code>）這個站上沒有實跑。實測見 &lt;a href="https://tarrragon.github.io/blog/devops/infra/03-network-foundation/hands-on/podman-rootless-ports-walkthrough/" data-link-title="Podman 與 rootless Docker 發佈連接埠的實測紀錄：rootful Podman 在 ufw 與 firewalld 上的轉送路徑、rootless Docker 與 rootless Podman 由主機程式接收的連接埠與容器看到的來源位址" data-link-desc="用 Podman 或 rootless 模式的 Docker 發佈連接埠、想知道 ufw 或 firewalld 的規則管不管得到、或想在容器裡拿到連線的真實來源位址時，查 rootful Podman 在 ufw 與 firewalld 上的不同結果、rootless 模式接收連線的路徑，以及容器看到的來源位址">Podman 與 rootless Docker 發佈連接埠的實測紀錄：rootful Podman 在 ufw 與 firewalld 上的轉送路徑、rootless Docker 與 rootless Podman 由主機程式接收的連接埠與容器看到的來源位址&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/devops/infra/03-network-foundation/hands-on/ufw-docker-walkthrough/" data-link-title="ufw 主機上 Docker 發佈連接埠的實測紀錄：ufw 規則的效果、DOCKER-USER 規則的存續與 after.rules、IPv6 的 docker-proxy、compose 內建 DNS 的出站路徑" data-link-desc="在 Ubuntu 的 ufw 主機上用 Docker 發佈連接埠、想只放行 Cloudflare 的位址、或想限制容器送出的 DNS 查詢時，查 ufw 規則對發佈連接埠的效果、DOCKER-USER 規則在 reload 與重開機之後還在不在、IPv6 那一份發佈由誰接收，以及 compose 容器的 DNS 封包從哪裡送出">ufw 主機上 Docker 發佈連接埠的實測紀錄：ufw 規則的效果、DOCKER-USER 規則的存續與 after.rules、IPv6 的 docker-proxy、compose 內建 DNS 的出站路徑&lt;/a> 的〈IPv6 那一份發佈：docker-proxy 的接收路徑與 ufw 入站規則的效果〉。&lt;/p></description><content:encoded><![CDATA[<p>把連線送進容器有兩種做法。<a href="/blog/devops/infra/knowledge-cards/dnat/" data-link-title="DNAT（目的位址轉換）" data-link-desc="容器發佈的連接埠沒被主機防火牆擋下、port forwarding 的限制規則要寫在位址轉換之前還是之後、或要比對位址轉換之前的連接埠時，查 DNAT 改寫封包目的地的時間點與它對防火牆規則的影響">DNAT（目的位址轉換）</a> 是核心在封包剛進主機時改寫目的地，封包本身被轉送給容器；userland proxy 是一個在主機上執行的使用者空間程式聽著那個連接埠，收下連線之後自己再開一條連線到容器，把兩條連線之間的資料來回搬。常見的三個是 Docker 的 docker-proxy、rootless Docker 的 rootlesskit，以及 rootless Podman 的 rootlessport。</p>
<h2 id="概念位置">概念位置</h2>
<p>連線的終點是主機上的一個程式，所以封包走的是 input 路徑（目的地是主機本身程式的那一條），主機防火牆的入站規則（ufw 的入站規則、firewalld 的 zone）管得到它，跟 DNAT 那一類正好相反。代價在容器看到的來源：容器收到的連線是轉送程式開的，來源位址是轉送程式那一端的位址，不是外面的真實來源。這跟 <a href="/blog/devops/infra/knowledge-cards/reverse-proxy/" data-link-title="Reverse Proxy" data-link-desc="代替後端服務接收外部請求、再轉發到內部服務的中介層，承擔 TLS 終結、負載平衡與路由分流">Reverse Proxy（反向代理）</a> 換掉來源位址的道理相同，差別在反向代理常用 HTTP 標頭（<code>X-Forwarded-For</code>）把原本的來源帶過去，這幾個轉送程式在 TCP 這一層搬資料，什麼都不帶。</p>
<p>三個程式各在什麼時候出現：</p>
<ul>
<li><strong>docker-proxy</strong>：以 root 執行的 Docker 用 <code>-p</code> 發佈時，主機上 IPv4 與 IPv6 各有一份；IPv4 那一份從外面來的封包在 prerouting 就被 DNAT 改寫、到不了它，IPv6 那一份在容器網路沒開 IPv6 時由它接收。Docker 的 <code>userland-proxy</code> 設成 <code>false</code> 時它不啟動，IPv6 那一份也就沒有發佈（2026-10-26 在 Docker 29.9.2 上實測）。</li>
<li><strong>rootlesskit</strong>：rootless Docker 的 dockerd 以一般使用者執行、不能改主機的 iptables，發佈的連接埠全部由它接聽。</li>
<li><strong>rootlessport</strong>：rootless Podman 的對應程式，網路由 slirp4netns（Podman 4）或 pasta（Podman 5 的預設）提供。</li>
</ul>
<h2 id="可觀察訊號與例子">可觀察訊號與例子</h2>
<ul>
<li><strong><code>ss -ltnp</code> 列出這幾個程式聽著發佈的連接埠，nat 表裡沒有對應的 DNAT</strong>：<code>users:((&quot;rootlesskit&quot;,pid=4085,fd=13))</code> 聽著 <code>0.0.0.0:8083</code>，<code>iptables -t nat -S | grep -c 8083</code> 回 0。</li>
<li><strong>容器的存取紀錄只有閘道位址</strong>：2026-10-26 的實測裡，經由 docker-proxy 的 IPv6 連線與 rootless Docker 的連線，容器記到的來源都是 <code>172.17.0.1</code>；rootless Podman 4.9 記到的是 slirp4netns 那張 tap 介面的 <code>10.0.2.100</code>。</li>
<li><strong>主機防火牆的入站規則對容器的連接埠有效</strong>：ufw 預設拒絕入站時兩個測試來源都連不到 rootless 容器的連接埠，加一條入站放行之後只有被放行的那個連得到。</li>
</ul>
<h2 id="判讀方式">判讀方式</h2>
<p>判斷一個發佈的連接埠屬於哪一類，先查有沒有規則把它的目的地改寫掉，再看誰在聽：<code>nft list ruleset | grep 'dport &lt;連接埠&gt;'</code> 同時列得出經由 iptables-nft 寫的與原生 nftables 寫的改寫規則。查到改寫規則時是 DNAT 那一類，聽著連接埠的程式只是佔住它（Podman 的 <code>conmon</code>、docker-proxy 的 IPv4 那一份），不算數；查不到時，聽著的轉送程式才是接收者。查表是預測，最終以從範圍外的來源實際連一次為準。屬於這一類時，限制寫在主機防火牆的入站規則上就有效，而容器裡依來源位址做的判斷（存取紀錄、速率限制、只信任來自 CDN 位址段的 <code>CF-Connecting-IP</code>）都拿不到真正的來源；Docker 文件列的保留來源位址做法（RootlessKit v3 的 <code>builtin</code> 加 <code>userland-proxy: false</code>、slirp4netns 轉送方式、pasta 加 <code>implicit</code>）這個站上沒有實跑。實測見 <a href="/blog/devops/infra/03-network-foundation/hands-on/podman-rootless-ports-walkthrough/" data-link-title="Podman 與 rootless Docker 發佈連接埠的實測紀錄：rootful Podman 在 ufw 與 firewalld 上的轉送路徑、rootless Docker 與 rootless Podman 由主機程式接收的連接埠與容器看到的來源位址" data-link-desc="用 Podman 或 rootless 模式的 Docker 發佈連接埠、想知道 ufw 或 firewalld 的規則管不管得到、或想在容器裡拿到連線的真實來源位址時，查 rootful Podman 在 ufw 與 firewalld 上的不同結果、rootless 模式接收連線的路徑，以及容器看到的來源位址">Podman 與 rootless Docker 發佈連接埠的實測紀錄：rootful Podman 在 ufw 與 firewalld 上的轉送路徑、rootless Docker 與 rootless Podman 由主機程式接收的連接埠與容器看到的來源位址</a> 與 <a href="/blog/devops/infra/03-network-foundation/hands-on/ufw-docker-walkthrough/" data-link-title="ufw 主機上 Docker 發佈連接埠的實測紀錄：ufw 規則的效果、DOCKER-USER 規則的存續與 after.rules、IPv6 的 docker-proxy、compose 內建 DNS 的出站路徑" data-link-desc="在 Ubuntu 的 ufw 主機上用 Docker 發佈連接埠、想只放行 Cloudflare 的位址、或想限制容器送出的 DNS 查詢時，查 ufw 規則對發佈連接埠的效果、DOCKER-USER 規則在 reload 與重開機之後還在不在、IPv6 那一份發佈由誰接收，以及 compose 容器的 DNS 封包從哪裡送出">ufw 主機上 Docker 發佈連接埠的實測紀錄：ufw 規則的效果、DOCKER-USER 規則的存續與 after.rules、IPv6 的 docker-proxy、compose 內建 DNS 的出站路徑</a> 的〈IPv6 那一份發佈：docker-proxy 的接收路徑與 ufw 入站規則的效果〉。</p>
]]></content:encoded></item></channel></rss>