<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Linux on 亂筆</title>
        <link>https://blog.l3zc.com/zh-hant-tw/tags/linux/</link>
        <description>Recent content in Linux on 亂筆</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh-hant-tw</language>
        <lastBuildDate>Wed, 08 Jul 2026 17:02:34 +0800</lastBuildDate><atom:link href="https://blog.l3zc.com/zh-hant-tw/tags/linux/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>Terraform IaC 初體驗</title>
            <link>https://blog.l3zc.com/zh-hant-tw/2026/04/iac-with-terraform/</link>
            <pubDate>Thu, 09 Apr 2026 07:13:35 +0000</pubDate>
            <guid>https://blog.l3zc.com/zh-hant-tw/2026/04/iac-with-terraform/</guid>
            <description>&lt;img src=&#34;https://blog.l3zc.com/2026/04/iac-with-terraform/cover_hu_a8e83f97ed61569c.webp&#34; alt=&#34;Featured image of post Terraform IaC 初體驗&#34; /&gt;&lt;p&gt;Terraform 是一款 IaC 工具，所謂 IaC 就是「基礎設施即代碼」，將我們的基礎設施以聲明式的代碼寫出來，隨後使用&lt;code&gt;terraform apply&lt;/code&gt;即可完成基礎設施的部署，同一份配置，部署出來的一定是一模一樣的基礎設施（Nix OS 用戶狂喜）。&lt;/p&gt;&#xA;&lt;h2 id=&#34;為什麼要用-terraform&#34;&gt;為什麼要用 Terraform&#xA;&lt;/h2&gt;&lt;p&gt;傳統的基礎設施管理絕大部分基於人力和各種雲服務商的 Dashboard，這就帶來了下面這些痛點：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th style=&#34;text-align: left&#34;&gt;痛點&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: left&#34;&gt;說明&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;難以重現&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;通過 Dashboard 點點點進行配置，容易改錯或者改漏，而且難以復現&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;環境漂移&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;手動操作導致生產和測試環境配置逐漸偏離，導致極端情況下測試沒問題生產直接崩&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;難以擴展&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;新增一套環境需要重複大量手工操作，耗時且容易出錯&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;難以審計&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;沒有變更記錄，&lt;del&gt;出了事不好甩鍋&lt;/del&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;難以協作&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;基礎設施由少數「懂的人」掌控，誰想要改都得去找這些人，效率低&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;為了解決這些問題，「基礎設施即代碼」&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;的概念被提了出來，Terraform 就是其中一個著名的解決方案。&lt;/p&gt;&#xA;&lt;p&gt;假設一個現實的使用場景，例如你每年都會購買新的 GCP 的 $300 試用金賬號，雖然便宜，但是每年都要重新跑到 GCP 的面板裡開機器。而有了 Terraform，想要部署同樣配置的機器，只要在每次更換賬號之後，將 API Token 替換成新的，隨後&lt;code&gt;terraform apply&lt;/code&gt;，只需要幾分鐘就能開出和你上個賬號一模一樣的機器、VPC、S3、防火牆配置等等。&lt;/p&gt;&#xA;&lt;p&gt;再比如 Cloudflare DNS 和 Tunnel 的管理和遷移，也只需要將原 Tunnel 的 Ingress Rule 複製到新 Tunnel 的 Ingress Rule。即使將來要遷移到 AliDNS、Route 53 等其他服務商，我們也可以原封不動的將數據複製過去&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a href=&#34;#fn:2&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;。&lt;/p&gt;&#xA;&lt;p&gt;尤其是到了多人協作的時候，配合 Git，每次更改都有跡可循，更不需要擔心合併衝突，PR 可以自動生成更改預覽，出現問題可以立刻回滾到上一個版本，這些都是傳統依賴人力去直接操作服務提供商 Dashboard 的做法完全無法做到的。&lt;/p&gt;&#xA;&lt;h2 id=&#34;terraform-的安裝&#34;&gt;Terraform 的安裝&#xA;&lt;/h2&gt;&lt;p&gt;Terraform 是用 Go 寫的，編譯出來的產物自然也是單個可執行文件，所以安裝起來也非常容易，Windows 下可以直接使用 Winget 安裝：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-pwsh&#34;&gt;winget install Hashicorp.Terraform&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果不想使用 Winget，也可以使用 Scoop 等其他包管理器，或者將預編譯的二進制文件放入&lt;code&gt;$PATH&lt;/code&gt;，即可完成安裝。&lt;/p&gt;&#xA;&lt;p&gt;如果你在 Linux 環境下，則使用對應的包管理器安裝即可，如果使用將二進制文件放入&lt;code&gt;$PATH&lt;/code&gt;的安裝方法，則要記得&lt;code&gt;sudo chmod +x terraform&lt;/code&gt;賦予文件的執行權限。&lt;/p&gt;&#xA;&lt;h2 id=&#34;terraform-的兩個基本概念&#34;&gt;Terraform 的兩個基本概念&#xA;&lt;/h2&gt;&lt;h3 id=&#34;providers&#34;&gt;Provider(s)&#xA;&lt;/h3&gt;&lt;p&gt;我們前面已經提到，Terraform 是一個款礎設施即代碼工具，作為一個工具本身，他並不綁定某個平臺，而是通過 Provider(s) 與各個平臺對接，要查看有哪些 Provider(s)，可以瀏覽 &lt;a class=&#34;link&#34; href=&#34;https://registry.terraform.io/browse/providers&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;Terraform 的 Registry&lt;/a&gt;。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2026/04/iac-with-terraform/image_hu_463ebc576c22c691.webp&#34; alt=&#34;Terraform 擁有豐富的 Provider(s) 生態&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;h3 id=&#34;狀態管理&#34;&gt;狀態管理&#xA;&lt;/h3&gt;&lt;p&gt;Terraform 將每次執行基礎設施變更操作時的狀態信息保存在一個狀態文件中，默認情況下會保存在當前工作目錄下的&lt;code&gt;terraform.tfstate&lt;/code&gt;文件裡&lt;sup id=&#34;fnref:3&#34;&gt;&lt;a href=&#34;#fn:3&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;3&lt;/a&gt;&lt;/sup&gt;，我們也可以修改配置文件自行指定其他的存儲後端，例如 S3、Postgres。每次我們執行&lt;code&gt;terraform apply&lt;/code&gt;時，Terraform 都會將目前配置文件聲明的狀態同現有的狀態文件進行比對，從而計算變動的部分，自動確定調整順序之後操作 Provider 落實狀態變動。&lt;/p&gt;&#xA;&lt;h2 id=&#34;terraform-的資源導入難題&#34;&gt;Terraform 的資源導入難題&#xA;&lt;/h2&gt;&lt;p&gt;現有資源的導入一直是 Terraform 為人詬病的難題，Hashi Corp. 似乎一直在堅持著一個固執且愚蠢的觀點：你所有的基礎設施從一開始就應當是用我們的 Terraform 創建的，所以也不存在什麼資源導入的問題。&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;時間&lt;/th&gt;&#xA;          &lt;th&gt;版本&lt;/th&gt;&#xA;          &lt;th&gt;進展&lt;/th&gt;&#xA;          &lt;th&gt;問題&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;2014–2022&lt;/td&gt;&#xA;          &lt;td&gt;v0.x–v1.4&lt;/td&gt;&#xA;          &lt;td&gt;只有 &lt;code&gt;terraform import&lt;/code&gt;，一次一條，而且完全不生成配置&lt;/td&gt;&#xA;          &lt;td&gt;導入完還得自己手寫 HCL&lt;sup id=&#34;fnref:4&#34;&gt;&lt;a href=&#34;#fn:4&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;2023.06&lt;/td&gt;&#xA;          &lt;td&gt;v1.5&lt;/td&gt;&#xA;          &lt;td&gt;引入 &lt;code&gt;import&lt;/code&gt; 塊和 &lt;code&gt;-generate-config-out&lt;/code&gt;參數，可以生成配置了&lt;/td&gt;&#xA;          &lt;td&gt;然而還是得一條一條寫，而且還得自己提供現有資源的 ID&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;2024.01&lt;/td&gt;&#xA;          &lt;td&gt;v1.7&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;import&lt;/code&gt; 塊支持 &lt;code&gt;for_each&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;批量了，但 ID 還得自己搞&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;2024 下半年&lt;/td&gt;&#xA;          &lt;td&gt;v1.12&lt;/td&gt;&#xA;          &lt;td&gt;引入了 &lt;code&gt;terraform query&lt;/code&gt; 和 &lt;code&gt;list&lt;/code&gt; 塊，終於實現了自動發現資源的功能&lt;/td&gt;&#xA;          &lt;td&gt;然而這個功能要 Provider 要自己實現，大量 Provider 根本沒跟上&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;這麼多年的時間，社區一直在罵，然而，「我有一堆現有資源，怎麼導入 Terraform」這個問題一直沒有被認真對待。Terraform 作為「基礎設施即代碼」工具，設計哲學就是聲明式和冪等性&lt;sup id=&#34;fnref:5&#34;&gt;&lt;a href=&#34;#fn:5&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;5&lt;/a&gt;&lt;/sup&gt;，Hashi Corp. 他們篤信「一切以代碼聲明的狀態為準，就應該用 Terraform 從零開始創建資源」。但現實是絕大多數公司都有大量歷史存量資源，先有資源、後有代碼才是常態。這個矛盾 Hashi Corp. 承認得很晚，&lt;code&gt;v1.12&lt;/code&gt; 的 &lt;code&gt;terraform query&lt;/code&gt; 才算是官方第一次認真面對這個問題——但 Provider 生態的跟進嘛……真的是一言難盡。&lt;/p&gt;&#xA;&lt;h2 id=&#34;terraform-的文件結構&#34;&gt;Terraform 的文件結構&#xA;&lt;/h2&gt;&lt;p&gt;Terraform 的文件結構很簡單，其主程序運行時會無腦讀取工作目錄下所有的&lt;code&gt;.tf&lt;/code&gt;文件，只要信息齊全，想給文件取什麼名字都可以，比如我的文件結構就長這樣：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;❯ tree -a -I .git&#xA;.&#xA;├── .editorconfig&#xA;├── .github&#xA;│   ├── dependabot.yml&#xA;│   └── workflows&#xA;│       ├── terraform-apply.yml&#xA;│       ├── terraform-plan.yml&#xA;│       └── your-fork.yml&#xA;├── .gitignore&#xA;├── .terraform.lock.hcl&#xA;├── cf_dns_zones.tf&#xA;├── cf_tunnel.tf&#xA;├── dns_example_com.tf&#xA;├── dns_example_net.tf&#xA;├── dns_example_top.tf&#xA;├── dns_example_cn.tf&#xA;├── main.tf                 # 基礎配置（terraform 塊）&#xA;├── moved.tf                # 移動過的資源&#xA;├── provider.tf             # 每個 Provider 的配置&#xA;├── README.md&#xA;├── rename_resources.ps1&#xA;└── variables.tf            # 自定義的變量&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;main.tf&lt;/code&gt;用於存放基礎配置:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-hcl&#34;&gt;terraform {&#xA;  required_providers {&#xA;    cloudflare = {&#xA;      source  = &#34;cloudflare/cloudflare&#34;&#xA;      version = &#34;~&gt; 5&#34;&#xA;    }&#xA;&#xA;    tencentcloud = {&#xA;      source  = &#34;tencentcloudstack/tencentcloud&#34;&#xA;      version = &#34;&gt;= 1.81.43&#34;&#xA;    }&#xA;  }&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;provider.tf&lt;/code&gt;用於存放每個 Provider 的配置：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-hcl&#34;&gt;provider &#34;cloudflare&#34; {}&#xA;provider &#34;tencentcloud&#34; {}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;這裡留空是因為我們可以通過環境變量來傳遞 Credentials，而不是直接將 Credentials 寫在配置文件中，例如 Cloudflare 的 Provider 就接受&lt;code&gt;CLOUDFLARE_API_TOKEN&lt;/code&gt;作為&lt;code&gt;api_token&lt;/code&gt;這個變量的替代。&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;variables.tf&lt;/code&gt;用於聲明自定義的變量：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-hcl&#34;&gt;variable &#34;cloudflare_zone_example_com&#34; {&#xA;  description = &#34;Cloudflare zone ID for example.com&#34;&#xA;  type        = string&#xA;}&#xA;&#xA;variable &#34;cloudflare_zone_example_top&#34; {&#xA;  description = &#34;Cloudflare zone ID for example.top&#34;&#xA;  type        = string&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;這些變量可以通過前綴為&lt;code&gt;TF_VAR_&lt;/code&gt;的環境變量傳入，Terraform 也會自動讀取&lt;code&gt;terraform.tfvars&lt;/code&gt;文件中的變量，變量的主要用途是在別的配置文件中調用，例如：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-hcl&#34;&gt;resource &#34;cloudflare_dns_record&#34; &#34;example_cname&#34; {&#xA;  content = &#34;${cloudflare_zero_trust_tunnel_cloudflared.Production_Tunnel.id}.cfargotunnel.com&#34;&#xA;  name    = &#34;example.example.com&#34;&#xA;  proxied = true&#xA;  tags    = []&#xA;  ttl     = 1&#xA;  type    = &#34;CNAME&#34;&#xA;  zone_id = var.cloudflare_zone_id_example_com  #這裡調用了cloudflare_zone_example_com這個變量&#xA;  settings = {&#xA;    flatten_cname = false&#xA;  }&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;有了這些基礎配置，我們就可以運行&lt;code&gt;terraform init&lt;/code&gt;初始化 Terraform 環境並鎖定依賴版本，剩下的就都是我們資源的聲明瞭。&lt;/p&gt;&#xA;&lt;h2 id=&#34;將現有資源導入-terraform&#34;&gt;將現有資源導入 Terraform&#xA;&lt;/h2&gt;&lt;p&gt;前文提到過 Terraform 的狀態管理這個概念，狀態管理雖好，但在我們初始化時也帶來了一個問題——在默認狀態下，Terraform 的狀態文件顯然是空的。&lt;/p&gt;&#xA;&lt;p&gt;此時我們需要做的是將雲服務提供商上現有資源的狀態導入 Terraform，這樣 Terraform 才能無縫接手並管理我們的基礎設施。相信大家也都看到了前文對 Terraform 資源導入問題的吐槽。以導入 Cloudflare 上託管的 DNS 記錄為例，前文提到過，如果 Provider 支持，可以使用 Terraform 在 1.12 版本之後引入的&lt;code&gt;terraform query&lt;/code&gt;，然而大多數情況下，Providers 都是沒有跟進這個新功能的，Cloudflare 就屬於不支持的那一類。&lt;/p&gt;&#xA;&lt;p&gt;好在雖然 Hashi Corp. 不認真解決問題，各路高強度使用 Terraform 管理基礎設施的公司就各顯神通。Cloudflare 就維護了名為 &lt;a class=&#34;link&#34; href=&#34;https://github.com/cloudflare/cf-terraforming&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;cf-terraforming&lt;/a&gt; 的導入工具，免去了很多手動導入的麻煩。首先安裝&lt;code&gt;cf-terraforming&lt;/code&gt;，這個工具是用 Go 語言編寫的，需要在有 Go 語言環境的情況下安裝，或者將官方編譯好的二進制文件其放入&lt;code&gt;$PATH&lt;/code&gt;並賦予其執行權限。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;go install github.com/cloudflare/cf-terraforming/cmd/cf-terraforming@latest&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;這個工具的用法還是比較簡單的，首先你需要以下環境變量：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;# 如果你使用 API Token&#xA;export CLOUDFLARE_API_TOKEN=&#39;Hzsq3Vub-7Y-hSTlAaLH3Jq_YfTUOCcgf22_Fs-j&#39;&#xA;&#xA;# 如果你使用 API Key&#xA;export CLOUDFLARE_EMAIL=&#39;user@example.com&#39;&#xA;export CLOUDFLARE_API_KEY=&#39;1150bed3f45247b99f7db9696fffa17cbx9&#39;&#xA;&#xA;# 指定需要導入的域名的區域 ID，如果導入的是賬戶資源（例如 Cloudflare Tunnel）則不需要&#xA;export CLOUDFLARE_ZONE_ID=&#39;81b06ss3228f488fh84e5e993c2dc17&#39;&lt;/code&gt;&lt;/pre&gt;&lt;blockquote class=&#34;alert alert-tip&#34;&gt;&#xA;        &lt;div class=&#34;alert-header&#34;&gt;&#xA;            &lt;span class=&#34;alert-icon&#34;&gt;💡&lt;/span&gt;&#xA;            &lt;span class=&#34;alert-title&#34;&gt;提示&lt;/span&gt;&#xA;        &lt;/div&gt;&#xA;        &lt;div class=&#34;alert-body&#34;&gt;&#xA;            &lt;p&gt;此處的命令假設你正在使用 Bash，如果使用的是與 Bash 語法不兼容的 Shell，則需要做出調整，例如對於 Windows 上的 PowerShell，導入環境變量的語法如下：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-pwsh&#34;&gt;$env:CLOUDFLARE_API_TOKEN=&#39;Hzsq3Vub-7Y-hSTlAaLH3Jq_YfTUOCcgf22_Fs-j&#39;&lt;/code&gt;&lt;/pre&gt;&#xA;        &lt;/div&gt;&#xA;    &lt;/blockquote&gt;&#xA;&lt;p&gt;通常我們只需要設置&lt;code&gt;CLOUDFLARE_API_TOKEN&lt;/code&gt;和&lt;code&gt;CLOUDFLARE_ZONE_ID&lt;/code&gt;就可以了，在控制檯創建 API Token 時，記得賦予這個 Token 必要的權限，本次我們只是導入 DNS 記錄，所以只賦予編輯區域 DNS 的權限即可。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2026/04/iac-with-terraform/image-1_hu_15298d4bd366a377.webp&#34; alt=&#34;賦予操作資源所需要的權限&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;好了，準備工作全部完成，現在可以開始生成配置文件：&lt;/p&gt;&#xA;&lt;p&gt;首先導入賬戶中域名的配置，也就是&lt;code&gt;cloudflare_zone&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;cf-terraforming generate \&#xA;  --key $CLOUDFLARE_API_KEY \&#xA;  --resource-type &#34;cloudflare_zone&#34; &gt; zone.tf&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;這一步會在當前目錄下生成一個名為&lt;code&gt;zone.tf&lt;/code&gt;的文件，裡面會有如下格式的內容：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-hcl&#34;&gt;resource &#34;cloudflare_zone&#34; &#34;REDACTED&#34; {&#xA;  name                = &#34;REDACTED&#34;&#xA;  paused              = false&#xA;  type                = &#34;full&#34;&#xA;  vanity_name_servers = []&#xA;  account = {&#xA;    id   = &#34;REDACTED&#34;&#xA;    name = &#34;REDACTED&#34;&#xA;  }&#xA;}&#xA;&#xA;resource &#34;cloudflare_zone&#34; &#34;REDACTED&#34; {&#xA;  name                = &#34;REDACTED&#34;&#xA;  paused              = false&#xA;  type                = &#34;full&#34;&#xA;  vanity_name_servers = []&#xA;  account = {&#xA;    id   = &#34;REDACTED&#34;&#xA;    name = &#34;REDACTED&#34;&#xA;  }&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;現在域名資源已經導入了，但是裡面的配置並沒有。接下來，導入域名下的 DNS 記錄：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;cf-terraforming generate \&#xA;  --zone $CLOUDFLARE_ZONE_ID \&#xA;  --key $CLOUDFLARE_API_KEY \&#xA;  --resource-type &#34;cloudflare_dns_record&#34; &gt;&gt; dns.tf&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;這一步會在當前目錄下生成一個名為&lt;code&gt;dns.tf&lt;/code&gt;的配置文件，裡面會有如下格式的內容：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-hcl&#34;&gt;resource &#34;cloudflare_dns_record&#34; &#34;terraform_managed_resource_5deb14xxxxxb629bf123xxxxxxxc8f_0&#34; {&#xA;  content  = &#34;67.24.33.108&#34;&#xA;  name     = &#34;example.example.com&#34;&#xA;  proxied  = true&#xA;  tags     = []&#xA;  ttl      = 1&#xA;  type     = &#34;A&#34;&#xA;  zone_id  = &#34;81c7f2de8dfxxxxxx52629xxxxxxfc&#34;&#xA;  settings = {}&#xA;}&#xA;&#xA;resource &#34;cloudflare_dns_record&#34; &#34;terraform_managed_resource_89xxxxx0bf9cxxxxxx9a_1&#34; {&#xA;  content  = &#34;35.27.108.33&#34;&#xA;  name     = &#34;terraform.example.com&#34;&#xA;  proxied  = true&#xA;  tags     = []&#xA;  ttl      = 1&#xA;  type     = &#34;A&#34;&#xA;  zone_id  = &#34;8xxxxxx7644e428526xxxxxx&#34;&#xA;  settings = {}&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果有多個域名需要導入，則多次分別設置環境變量&lt;code&gt;CLOUDFLARE_ZONE_ID&lt;/code&gt;再重複運行命令即可。&lt;/p&gt;&#xA;&lt;p&gt;這裡生成的配置文件可以直接使用，也就是我們之後需要的 Terraform 配置文件。然而，此時我們僅僅只是生成了配置文件，但是目前 Terraform 的狀態依然是空的，這時要是直接&lt;code&gt;terraform apply&lt;/code&gt;，Terraform 會不管三七二十一將我們剛剛導入的聲明一律視為新增資源，然後甩出一大堆「Alredy Exists」報錯。所以接下來，我們需要將生成的配置文件導入 Terraform 的&lt;code&gt;terraform.tfstate&lt;/code&gt;狀態。&lt;/p&gt;&#xA;&lt;p&gt;Terraform 在 1.5 版本引入了&lt;code&gt;import&lt;/code&gt;塊，相比以往一行一行輸入命令的方式更加現代。其導入流程是先生成一個包含&lt;code&gt;import&lt;/code&gt;塊的&lt;code&gt;.tf&lt;/code&gt;文件，下次進行&lt;code&gt;terraform apply&lt;/code&gt;時，Terraform 就會自動為我們執行導入操作。&lt;/p&gt;&#xA;&lt;p&gt;生成&lt;code&gt;cloudflare_zone&lt;/code&gt;的&lt;code&gt;import&lt;/code&gt;塊：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;cf-terraforming import \&#xA;  --resource-type &#34;cloudflare_zone&#34; \&#xA;  --modern-import-block \&#xA;  --key $CLOUDFLARE_API_KEY \&#xA;  --zone $CLOUDFLARE_ZONE_ID &gt;&gt; import.tf&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;生成&lt;code&gt;cloudflare_dns_record&lt;/code&gt;的&lt;code&gt;import&lt;/code&gt;塊：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;cf-terraforming import \&#xA;  --resource-type &#34;cloudflare_dns_record&#34; \&#xA;  --modern-import-block \&#xA;  --key $CLOUDFLARE_API_KEY \&#xA;  --zone $CLOUDFLARE_ZONE_ID &gt;&gt; import.tf&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;這一步會在當前目錄下生成&lt;code&gt;import.tf&lt;/code&gt;，它包含了所需要的導入信息，作用就是告訴 Terraform 上一步生成的每個&lt;code&gt;resource&lt;/code&gt;塊到底對應的是哪一個雲服務提供商的資源 ID。這個 ID 是雲服務提供商內部標記資源的代碼，平常是不會在控制面板上顯示的，只有在用 API 特別請求時才會知道。Terraform 在導入過程中需要用到這個 ID 以確認本地的定義對應的雲端資源，以實現嚴格的冪等性。&lt;/p&gt;&#xA;&lt;p&gt;好了，現在我們運行&lt;code&gt;terraform plan&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;$ terraform plan&#xA;cloudflare_dns_record.minio_a: Refreshing state... [id=xxxxxxxxxxx53]&#xA;cloudflare_zero_trust_tunnel_cloudflared_config.raspberrypi: Refreshing state...&#xA;......&#xA;&#xA;Terraform will perform the following actions:&#xA;&#xA;  # cloudflare_dns_record.terraform_managed_resource_0 will be imported&#xA;    resource &#34;cloudflare_dns_record&#34; &#34;terraform_managed_resource_REDACTED_0&#34; {&#xA;        content     = &#34;67.24.33.108&#34;&#xA;        created_on  = &#34;2026-04-08T10:18:12Z&#34;&#xA;        id          = &#34;5deb14c21xxxxxxx20f1c8f&#34;&#xA;        meta        = jsonencode({})&#xA;        modified_on = &#34;2026-04-08T10:18:12Z&#34;&#xA;        name        = &#34;example.example.com&#34;&#xA;        proxiable   = true&#xA;        proxied     = true&#xA;        settings    = {}&#xA;        tags        = []&#xA;        ttl         = 1&#xA;        type        = &#34;A&#34;&#xA;        zone_id     = &#34;REDACTED&#34;&#xA;    }&#xA;&#xA;  # cloudflare_dns_record.terraform_managed_resource_1 will be imported&#xA;    resource &#34;cloudflare_dns_record&#34; &#34;terraform_managed_resource_89c149exxxxxxxxxxxba13xxxxxa_1&#34; {&#xA;        content     = &#34;35.27.108.33&#34;&#xA;        created_on  = &#34;2026-04-08T10:17:54Z&#34;&#xA;        id          = &#34;89cxxxxxxxxxxxxxxxxxx09a&#34;&#xA;        meta        = jsonencode({})&#xA;        modified_on = &#34;2026-04-08T10:17:54Z&#34;&#xA;        name        = &#34;terraform.example.com&#34;&#xA;        proxiable   = true&#xA;        proxied     = true&#xA;        settings    = {}&#xA;        tags        = []&#xA;        ttl         = 1&#xA;        type        = &#34;A&#34;&#xA;        zone_id     = &#34;81xxxxxxxxxxxxxxxxxxxxxfc&#34;&#xA;    }&#xA;&#xA;Plan: 2 to import, 0 to add, 0 to change, 0 to destroy.&#xA;&#xA;────────────────────────────────────────────────────────────────────────────────────────────────────────&#xA;&#xA;Note: You didn&#39;t use the -out option to save this plan, so Terraform can&#39;t guarantee to take exactly&#xA;these actions if you run &#34;terraform apply&#34; now.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果需要 Add、Change 和 Destroy 的資源數量都是 0，說明我們的導入操作沒有問題，直接&lt;code&gt;terraform apply --auto-approve&lt;/code&gt;，資源就導入完成了。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;$ terraform apply --auto-approve&#xA;cloudflare_dns_record.push_a: Refreshing state... [id=REDACTED]&#xA;&#xA;Terraform will perform the following actions:&#xA;&#xA;  # cloudflare_dns_record.terraform_managed_resource_REDACTED_0 will be imported&#xA;    resource &#34;cloudflare_dns_record&#34; &#34;terraform_managed_resource_REDACTED_0&#34; {&#xA;        content     = &#34;67.24.33.108&#34;&#xA;        created_on  = &#34;2026-04-08T10:18:12Z&#34;&#xA;        id          = &#34;REDACTED&#34;&#xA;        meta        = jsonencode({})&#xA;        modified_on = &#34;2026-04-08T10:18:12Z&#34;&#xA;        name        = &#34;example.example.com&#34;&#xA;        proxiable   = true&#xA;        proxied     = true&#xA;        settings    = {}&#xA;        tags        = []&#xA;        ttl         = 1&#xA;        type        = &#34;A&#34;&#xA;        zone_id     = &#34;REDACTED&#34;&#xA;    }&#xA;&#xA;  # cloudflare_dns_record.terraform_managed_resource_REDACTED_1 will be imported&#xA;    resource &#34;cloudflare_dns_record&#34; &#34;terraform_managed_resource_REDACTED_1&#34; {&#xA;        content     = &#34;35.27.108.33&#34;&#xA;        created_on  = &#34;2026-04-08T10:17:54Z&#34;&#xA;        id          = &#34;REDACTED&#34;&#xA;        meta        = jsonencode({})&#xA;        modified_on = &#34;2026-04-08T10:17:54Z&#34;&#xA;        name        = &#34;terraform.example.com&#34;&#xA;        proxiable   = true&#xA;        proxied     = true&#xA;        settings    = {}&#xA;        tags        = []&#xA;        ttl         = 1&#xA;        type        = &#34;A&#34;&#xA;        zone_id     = &#34;REDACTED&#34;&#xA;    }&#xA;&#xA;Plan: 2 to import, 0 to add, 0 to change, 0 to destroy.&#xA;cloudflare_dns_record.terraform_managed_resource_REDACTED_1: Importing... [id=REDACTED/REDACTED]&#xA;cloudflare_dns_record.terraform_managed_resource_REDACTED_1: Import complete [id=REDACTED/REDACTED]&#xA;cloudflare_dns_record.terraform_managed_resource_REDACTED_0: Importing... [id=REDACTED/REDACTED]&#xA;cloudflare_dns_record.terraform_managed_resource_REDACTED_0: Import complete [id=REDACTED/REDACTED]&#xA;&#xA;Apply complete! Resources: 2 imported, 0 added, 0 changed, 0 destroyed.&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;狀態存儲和持續集成&#34;&gt;狀態存儲和持續集成&#xA;&lt;/h2&gt;&lt;p&gt;IaC 的核心價值之一就在於可以輕易實現基於 Git 的多人協作，以及 CI 的持續集成，但是在此之前，又有一個新的問題需要解決——&lt;code&gt;terraform.tfstate&lt;/code&gt;到底放哪裡：沒人想要換一次環境辛辛苦苦導入的狀態就丟一次。&lt;/p&gt;&#xA;&lt;p&gt;Terraform 目前支持以下幾種保存狀態的後端：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;local&lt;/li&gt;&#xA;&lt;li&gt;remote&lt;/li&gt;&#xA;&lt;li&gt;azurerm&lt;/li&gt;&#xA;&lt;li&gt;consul&lt;/li&gt;&#xA;&lt;li&gt;cos&lt;/li&gt;&#xA;&lt;li&gt;gcs&lt;/li&gt;&#xA;&lt;li&gt;http&lt;/li&gt;&#xA;&lt;li&gt;Kubernetes&lt;/li&gt;&#xA;&lt;li&gt;oci&lt;/li&gt;&#xA;&lt;li&gt;oss&lt;/li&gt;&#xA;&lt;li&gt;pg&lt;/li&gt;&#xA;&lt;li&gt;s3&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;沒有特殊需求可以像我一樣選擇&lt;code&gt;s3&lt;/code&gt;，畢竟 Cloudflare R2 有免費額度，不用白不用。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-hcl&#34;&gt;terraform {&#xA;  backend &#34;s3&#34; {&#xA;    bucket = &#34;terraform&#34;&#xA;    key    = &#34;terraform.tfstate&#34;&#xA;    region = &#34;auto&#34;&#xA;    endpoints = {&#xA;      s3 = &#34;https://REDACTED.r2.cloudflarestorage.com&#34;&#xA;    }&#xA;&#xA;    # R2 不需要這些 AWS 的驗證&#xA;    skip_credentials_validation = true&#xA;    skip_metadata_api_check     = true&#xA;    skip_region_validation      = true&#xA;    skip_requesting_account_id  = true&#xA;    use_path_style              = true&#xA;  }&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;對於 S3 的後端，建議用&lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt;和&lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt;兩個環境變量來存儲 Credentials：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;export AWS_ACCESS_KEY_ID=&#39;REDACTED&#39;&#xA;export AWS_SECRET_ACCESS_KEY=&#39;REDACTED&#39;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;配置完成，運行&lt;code&gt;terraform init -migrate-state&lt;/code&gt;，配置就成功存儲到雲上了，以後不論在何處修改配置、運行&lt;code&gt;terraform apply&lt;/code&gt;都無須擔心 Terraform 的 State 不同步的問題。&lt;/p&gt;&#xA;&lt;p&gt;接下來就是 Github CI 的配置，其實很簡單，無非就是每次&lt;code&gt;git push&lt;/code&gt;時觸發一次&lt;code&gt;terraform init&lt;/code&gt;、&lt;code&gt;terraform fmt&lt;/code&gt;和&lt;code&gt;terraform apply&lt;/code&gt;，以下是我的&lt;code&gt;.github/workflows/apply.yml&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;name: &#34;Terraform Apply&#34;&#xA;&#xA;on:&#xA;  push:&#xA;    branches:&#xA;      - main&#xA;&#xA;env:&#xA;  TF_IN_AUTOMATION: &#34;true&#34;&#xA;  CLOUDFLARE_API_TOKEN: &#34;${{ secrets.CLOUDFLARE_API_TOKEN }}&#34;&#xA;  AWS_ACCESS_KEY_ID: &#34;${{ secrets.AWS_ACCESS_KEY_ID }}&#34;&#xA;  AWS_SECRET_ACCESS_KEY: &#34;${{ secrets.AWS_SECRET_ACCESS_KEY }}&#34;&#xA;  TF_VAR_cloudflare_zone_id_example_com: &#34;${{ vars.TF_VAR_CLOUDFLARE_ZONE_ID_EXAMPLE_COM }}&#34;&#xA;  TF_VAR_cloudflare_zone_id_example_top: ${{ vars.TF_VAR_CLOUDFLARE_ZONE_ID_EXAMPLE_TOP }}&#xA;&#xA;jobs:&#xA;  terraform:&#xA;    name: &#34;Terraform Apply&#34;&#xA;    runs-on: ubuntu-latest&#xA;    permissions:&#xA;      contents: read&#xA;    concurrency:&#xA;      group: terraform-apply&#xA;      cancel-in-progress: false&#xA;    steps:&#xA;      - name: Checkout&#xA;        uses: actions/checkout@v6&#xA;&#xA;      - name: Setup Terraform&#xA;        uses: hashicorp/setup-terraform@v4&#xA;&#xA;      - name: Terraform Init&#xA;        run: terraform init -input=false&#xA;&#xA;      - name: Terraform Apply&#xA;        run: terraform apply -input=false -auto-approve&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;對於 PR 則應當讓 CI 自動為每次 PR 附上&lt;code&gt;terraform plan&lt;/code&gt;的輸出：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;name: Terraform Plan&#xA;&#xA;on:&#xA;  pull_request:&#xA;    paths:&#xA;      - &#34;**/*.tf&#34;&#xA;      - &#34;.github/workflows/terraform-plan.yml&#34;&#xA;&#xA;permissions:&#xA;  contents: read&#xA;  pull-requests: write&#xA;&#xA;env:&#xA;  TF_IN_AUTOMATION: &#34;true&#34;&#xA;  CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}&#xA;  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}&#xA;  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}&#xA;  TF_VAR_cloudflare_zone_id_example_com: &#34;${{ vars.TF_VAR_CLOUDFLARE_ZONE_ID_EXAMPLE_COM }}&#34;&#xA;  TF_VAR_cloudflare_zone_id_example_top: ${{ vars.TF_VAR_CLOUDFLARE_ZONE_ID_EXAMPLE_TOP }}&#xA;&#xA;jobs:&#xA;  plan:&#xA;    name: Terraform Plan&#xA;    runs-on: ubuntu-latest&#xA;    steps:&#xA;      - uses: actions/checkout@v6&#xA;&#xA;      - uses: hashicorp/setup-terraform@v4&#xA;&#xA;      - name: Terraform fmt&#xA;        id: fmt&#xA;        run: terraform fmt -check -recursive&#xA;        continue-on-error: true&#xA;&#xA;      - name: Terraform Init&#xA;        id: init&#xA;        run: terraform init -input=false&#xA;&#xA;      - name: Terraform Validate&#xA;        id: validate&#xA;        run: terraform validate -no-color&#xA;&#xA;      - name: Terraform Plan&#xA;        id: plan&#xA;        run: terraform plan -input=false -no-color&#xA;        continue-on-error: true&#xA;&#xA;      - name: Post Plan to PR&#xA;        uses: actions/github-script@v8&#xA;        with:&#xA;          github-token: ${{ secrets.GITHUB_TOKEN }}&#xA;          script: |&#xA;            const { data: comments } = await github.rest.issues.listComments({&#xA;              owner: context.repo.owner,&#xA;              repo: context.repo.repo,&#xA;              issue_number: context.issue.number,&#xA;            });&#xA;            const botComment = comments.find(c =&gt;&#xA;              c.user.type === &#39;Bot&#39; &amp;&amp; c.body.includes(&#39;&lt;!-- terraform-plan --&gt;&#39;)&#xA;            );&#xA;&#xA;            const planOutput = `${{ steps.plan.outputs.stdout }}`.substring(0, 65000);&#xA;&#xA;            const body = `&lt;!-- terraform-plan --&gt;&#xA;            #### Terraform Plan&#xA;&#xA;            | Step     | Result                            |&#xA;            | -------- | --------------------------------- |&#xA;            | fmt      | \`${{ steps.fmt.outcome }}\`      |&#xA;            | init     | \`${{ steps.init.outcome }}\`     |&#xA;            | validate | \`${{ steps.validate.outcome }}\` |&#xA;            | plan     | \`${{ steps.plan.outcome }}\`     |&#xA;&#xA;            &lt;details&gt;&lt;summary&gt;展開 Plan 詳情&lt;/summary&gt;&#xA;&#xA;            \`\`\`terraform&#xA;            ${planOutput}&#xA;            \`\`\`&#xA;            &lt;/details&gt;`;&#xA;&#xA;            if (botComment) {&#xA;              await github.rest.issues.updateComment({&#xA;                owner: context.repo.owner,&#xA;                repo: context.repo.repo,&#xA;                comment_id: botComment.id,&#xA;                body&#xA;              });&#xA;            } else {&#xA;              await github.rest.issues.createComment({&#xA;                issue_number: context.issue.number,&#xA;                owner: context.repo.owner,&#xA;                repo: context.repo.repo,&#xA;                body&#xA;              });&#xA;            }&#xA;&#xA;      - name: Fail if plan failed&#xA;        if: steps.plan.outcome == &#39;failure&#39;&#xA;        run: exit 1&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2026/04/iac-with-terraform/image-2_hu_c922b7d4de19a009.webp&#34; alt=&#34;每次 PR 都會有 Plan 的輸出&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;h2 id=&#34;後續的工作流程&#34;&gt;後續的工作流程&#xA;&lt;/h2&gt;&lt;p&gt;到這一步，Terraform 的「接管初始化」已經結束了，後面就進入了日常維護階段。這個階段其實就三件事：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;創建新資源&lt;/li&gt;&#xA;&lt;li&gt;修改現有的資源&lt;/li&gt;&#xA;&lt;li&gt;刪除不再需要的資源&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;常用的操作就兩個：&lt;code&gt;terraform plan&lt;/code&gt;和&lt;code&gt;terraform apply&lt;/code&gt;，如果是個人使用，小的改動直接提交就算了；如果是團隊協作，每次修改則應當遵循能 PR 就不直接 Commit 的原則。&lt;/p&gt;&#xA;&lt;h3 id=&#34;創建基礎設施&#34;&gt;創建基礎設施&#xA;&lt;/h3&gt;&lt;p&gt;假設你現在要新增一條 DNS 記錄，或者新建一個 Tunnel、一個對象存儲桶，流程如下：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-mermaid&#34;&gt;flowchart TD&#xA;  C1[新建分支&#xA;如 feat/add-minio-record] --&gt; C2[新增 resource 塊]&#xA;  C2 --&gt; C3[terraform fmt + validate]&#xA;  C3 --&gt; C4[terraform plan]&#xA;  C4 --&gt; C5{僅新增預期資源?}&#xA;  C5 -- 否 --&gt; C6[修正配置後重跑 plan]&#xA;  C6 --&gt; C4&#xA;  C5 -- 是 --&gt; C7[提交 PR]&#xA;  C7 --&gt; C8[合併後 CI apply]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;最理想的&lt;code&gt;plan&lt;/code&gt;輸出是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;X to add&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;0 to change&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;0 to destroy&lt;/code&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;如果你只是想加東西，結果出現了&lt;code&gt;to destroy&lt;/code&gt;，那就先別衝動，通常是引用寫錯、變量搞錯，或者不小心改了資源地址，仔細檢查是什麼地方出了問題。&lt;/p&gt;&#xA;&lt;h3 id=&#34;修改與刪除基礎設施&#34;&gt;修改與刪除基礎設施&#xA;&lt;/h3&gt;&lt;p&gt;修改流程和創建類似，但要多一步——評估變更是否會觸發重建。&lt;/p&gt;&#xA;&lt;p&gt;因為很多 Provider 字段是&lt;code&gt;ForceNew&lt;/code&gt;，你以為只是改個字段，Terraform 看完說：「好的，刪了重建。」這在我們修改 DNS 的這個場景並不是什麼很大的問題，但是到了雲實例這種資源，如果刪除重建勢必會造成損失。&lt;/p&gt;&#xA;&lt;p&gt;建議按下面這個順序來：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-mermaid&#34;&gt;flowchart TD&#xA;  M1[修改 .tf] --&gt; M2[terraform plan]&#xA;  M2 --&gt; M3{出現 replace/destroy?}&#xA;  M3 -- 否 --&gt; M7[確認影響範圍]&#xA;  M7 --&gt; M8[terraform apply]&#xA;  M3 -- 是 --&gt; M4[暫停並複核變更]&#xA;  M4 --&gt; M5[必要時加 lifecycle 保護]&#xA;  M5 --&gt; M6[安排變更窗口]&#xA;  M6 --&gt; M8&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;對生產環境來說，創建、修改得慢一點並不是什麼很大的問題，最應當看重的是操作的正確性，慢一點，不要出錯。IaC 不是比手速，IaC 比的是可預期性。&lt;/p&gt;&#xA;&lt;p&gt;如果是刪除資源（比如下線某個 DNS 記錄、清理廢棄 Tunnel），走下面這個流程&lt;sup id=&#34;fnref:6&#34;&gt;&lt;a href=&#34;#fn:6&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;6&lt;/a&gt;&lt;/sup&gt;：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-mermaid&#34;&gt;flowchart TD&#xA;  D1[確認資源已廢棄&#xA;  檢查業務/監控/腳本依賴] --&gt; D2[刪除 resource 塊或調整 count/for_each]&#xA;  D2 --&gt; D3[terraform plan]&#xA;  D3 --&gt; D4{to destroy 是否符合預期?}&#xA;  D4 -- 否 --&gt; D5[回滾修改並繼續排查依賴]&#xA;  D5 --&gt; D1&#xA;  D4 -- 是 --&gt; D6[準備回滾方案並選低峰窗口]&#xA;  D6 --&gt; D7[PR 審核通過]&#xA;  D7 --&gt; D8[terraform apply]&#xA;  D8 --&gt; D9[刪除資源後的可用性檢查]&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;日常協作建議&#34;&gt;日常協作建議&#xA;&lt;/h3&gt;&lt;ul&gt;&#xA;&lt;li&gt;把 Credentials 放環境變量或 CI Secret，別寫進&lt;code&gt;.tf&lt;/code&gt;和倉庫&lt;/li&gt;&#xA;&lt;li&gt;對關鍵資源開啟保護策略，防止誤刪&lt;/li&gt;&#xA;&lt;li&gt;將目錄按資源類型拆分&lt;/li&gt;&#xA;&lt;li&gt;定期執行&lt;code&gt;terraform plan&lt;/code&gt;做基礎設施漂移檢查&lt;sup id=&#34;fnref:7&#34;&gt;&lt;a href=&#34;#fn:7&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;7&lt;/a&gt;&lt;/sup&gt;和校正，避免在面板誤操作手動修改&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;雖然這套流程看起來很麻煩，但每一次變更都有記錄、可審計、可回滾，最重要的是可復現，這才是 IaC 最有價值的地方。&lt;/p&gt;&#xA;&lt;h2 id=&#34;參考&#34;&gt;參考&#xA;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://candinya.com/posts/manage-cloudflare-dns-with-terraform/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;使用 Terraform 管理 CloudFlare 上的 DNS 解析記錄 - Candinya&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://developer.hashicorp.com/terraform/tutorials/automation/github-actions&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;Automate Terraform with GitHub Actions&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://developer.hashicorp.com/terraform/language/files/tfquery&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;Query configuration files&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://developer.hashicorp.com/terraform/language/block/tfquery/list&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;list block reference&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://github.com/cloudflare/cf-terraforming&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;cloudflare/cf-terraforming&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://developers.cloudflare.com/terraform/advanced-topics/import-cloudflare-resources/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;Import Cloudflare resources&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://registry.terraform.io/providers/cloudflare/cloudflare/latest/docs&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;Cloudflare Provider - Terraform Registry&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;&lt;a class=&#34;link&#34; href=&#34;https://en.wikipedia.org/wiki/Infrastructure_as_code&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;Infrastructure as Code&lt;/a&gt;，是指採用機器可讀的配置文件定義所需要的基礎設施的部署方法。&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:2&#34;&gt;&#xA;&lt;p&gt;每個供應商的配置文件字段命名和格式都有區別，但這可以很容易的編寫腳本進行轉換。&amp;#160;&lt;a href=&#34;#fnref:2&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:3&#34;&gt;&#xA;&lt;p&gt;需要特別注意的是，狀態文件中可能包含數據庫密碼、API 密鑰等明文存儲的敏感信息，因此絕對不要將 &lt;code&gt;.tfstate&lt;/code&gt; 文件提交到公開的代碼倉庫中。&amp;#160;&lt;a href=&#34;#fnref:3&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:4&#34;&gt;&#xA;&lt;p&gt;HashiCorp Configuration Language，HashiCorp 自家開發的一種聲明式配置語言，旨在兼顧機器可讀性與人類可讀性。&amp;#160;&lt;a href=&#34;#fnref:4&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:5&#34;&gt;&#xA;&lt;p&gt;冪等性（Idempotence）指計算機系統或接口在接收到同一請求的多次操作時，產生的影響與一次執行的結果相同，不論執行多少次，系統的最終狀態始終保持一致。&amp;#160;&lt;a href=&#34;#fnref:5&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:6&#34;&gt;&#xA;&lt;p&gt;如果你僅僅是想讓 Terraform 不再管理某個資源，而不是真正在雲端銷燬它，應該使用 &lt;code&gt;terraform state rm&lt;/code&gt; 命令，而不是在代碼中刪掉資源塊然後 &lt;code&gt;apply&lt;/code&gt;，否則雲環境上的真實資源也會被一併銷燬。&amp;#160;&lt;a href=&#34;#fnref:6&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:7&#34;&gt;&#xA;&lt;p&gt;基礎設施漂移（Infrastructure Drift）是指現實中通過控制檯點按等非 IaC 途徑修改了基礎設施，導致其實際狀態與代碼中聲明的狀態不一致的情況。&amp;#160;&lt;a href=&#34;#fnref:7&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;</description>
        </item><item>
            <title>從入門到進階：Tailscale &#43; ShellCrash 異地組網和科學上網</title>
            <link>https://blog.l3zc.com/zh-hant-tw/2025/04/tailscale-setup-recap/</link>
            <pubDate>Mon, 14 Apr 2025 10:10:25 +0000</pubDate>
            <guid>https://blog.l3zc.com/zh-hant-tw/2025/04/tailscale-setup-recap/</guid>
            <description>&lt;img src=&#34;https://blog.l3zc.com/2025/04/tailscale-setup-recap/tailscale_hu_62168b00e213c4a1.webp&#34; alt=&#34;Featured image of post 從入門到進階：Tailscale + ShellCrash 異地組網和科學上網&#34; /&gt;&lt;p&gt;在幾次旅途中，我試圖將新拍的照片上傳到長沙家中的 Immich 服務器。過去我用 Cloudflare Tunnel 做反代，但由於國內特殊的網絡環境，連接慢速、頻繁中斷、上傳失敗幾乎成了常態。後來我開始嘗試 Tailscale —— 一個基於 WireGuard 的內網穿透工具，它終於讓我在全國各地都能穩定、高速地訪問家中的 NAS 和照片庫。&lt;/p&gt;&#xA;&lt;p&gt;但新的問題也隨之而來：Tailscale 在 Android 手機上運行時，需要作為 VPN 服務接管系統流量，而我平時使用的 Clash 同樣依賴 VPN 接口進行分流。由於 Android 系統限制（只能啟用一個 VPN 服務），兩者無法共存，導致我必須在「訪問家中服務」和「科學上網」之間二選一。&lt;/p&gt;&#xA;&lt;p&gt;這篇文章從 Tailscale 的原理講起，到自建 DERP 服務器優化連接質量，再到如何用 iptables 劫持 Tailscale Exit Node 的流量並轉發給 ShellCrash，實現流量的靈活轉發與安全穿透。無論你是想訪問家庭局域網上的 NAS、照片庫，還是希望在陌生網絡中保護自己的數據安全，這篇文章都能為你提供一套實用、穩定的解決方案。&lt;/p&gt;&#xA;&lt;h2 id=&#34;什麼是-tailscale&#34;&gt;什麼是 Tailscale&#xA;&lt;/h2&gt;&lt;p&gt;Tailscale 是一款基於 WireGuard 協議的零配置虛擬局域網工具，它能夠讓分佈在不同網絡環境中的設備像處於同一個安全內網中一樣互聯互通。通過自動穿透 NAT、防火牆等網絡障礙，Tailscale 讓你無需公網 IP、無需端口轉發，也能輕鬆訪問家中的 NAS、個人服務器、開發環境等內網資源。它的核心優勢在於簡單、安全、穩定，啟動即用，數據傳輸全程加密，適合個人開發者、遠程辦公者、家庭用戶等多種場景使用。&lt;/p&gt;&#xA;&lt;p&gt;Tailscale 的技術實現非常巧妙：其構建在 WireGuard 加密協議之上，卻顛覆了傳統 VPN 的 IP 分配邏輯。每個設備通過 SSO/OAuth2 完成身份認證後，會獲得一個終身綁定的節點密鑰。這種基於身份的組網模式，讓「長沙的 NAS」和「香港的手機」在虛擬網絡中如同辦公室同事般直接對話。&lt;/p&gt;&#xA;&lt;h2 id=&#34;tailscale-的連接過程原理&#34;&gt;Tailscale 的連接過程原理&#xA;&lt;/h2&gt;&lt;h3 id=&#34;中心控制服務器control-server&#34;&gt;中心控制服務器（Control Server）&#xA;&lt;/h3&gt;&lt;p&gt;每個 Tailscale 客戶端在啟動後，首先會連接控制服務器（controlplane），進行身份驗證，並拉取整個網絡中其他節點的信息，包括每臺設備的公網 IP、端口、NAT 類型等。這一步相當於是「認識朋友」。&lt;/p&gt;&#xA;&lt;p&gt;Tailscale 的控制服務器不會轉發任何數據，只負責協調連接 —— 類似一個調度中心。&lt;/p&gt;&#xA;&lt;h3 id=&#34;derp-服務器&#34;&gt;DERP 服務器&#xA;&lt;/h3&gt;&lt;p&gt;說到 Tailscale 能保持高連接成功率的關鍵，就不得不提到 Tailscale 自研的中轉協議 DERP，在 Tailscale 的網絡架構裡，DERP（Designated Encrypted Relay for Packets）是一個很重要但通常只在必要時介入的組件。簡單來說，它就是一個基於 HTTP 的加密中繼服務器，用來在兩臺設備無法直接通信時，作為它們之間的「中轉站」。&lt;/p&gt;&#xA;&lt;p&gt;所有客戶端之間的連接都是先選擇 DERP 模式（中繼模式），這意味著連接立即就能建立，用戶無需等待。然後連接雙方開始並行地進行路徑發現，通常幾秒鐘之後，Tailscale 就能發現一條更優路徑，然後將現有連接透明升級（upgrade）過去，變成點對點連接（直連）。&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&#xA;&lt;p&gt;需要注意的是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;所有通過 DERP 的數據都是端到端加密的，DERP 服務器無法查看內容；&lt;/li&gt;&#xA;&lt;li&gt;Tailscale 會盡可能少地使用 DERP，一旦直連建立成功，就會自動切換過去；&lt;/li&gt;&#xA;&lt;li&gt;官方部署了多個分佈式 DERP 節點，客戶端會自動選擇延遲最低的那個；&lt;/li&gt;&#xA;&lt;li&gt;你也可以自建 DERP 節點（比如在國內），來解決延遲高或連接不穩定的問題。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;可以把 DERP 理解為一個兜底機制，雖然性能不如直連，但確保了即便不能打洞成功，設備之間也始終能保持連接。&lt;/p&gt;&#xA;&lt;h3 id=&#34;nat-穿透nat-traversal&#34;&gt;NAT 穿透（NAT Traversal）&#xA;&lt;/h3&gt;&lt;p&gt;拿到對端的地址信息後，Tailscale 會嘗試通過 NAT 穿透來建立點對點（P2P）連接。這個過程使用了 STUN 協議，雙方互相發送探測包，嘗試在 NAT 路由器上打出一條直連的通道。如果雙方的網絡條件允許，就可以成功建立起一個 UDP 的直連隧道，數據走直連，速度快、延遲低。&lt;/p&gt;&#xA;&lt;p&gt;受制於篇幅，我無法完整細緻的講述 NAT 穿透的原理，若對這部分感興趣，可以閱讀 Tailscale 官方的「&lt;a class=&#34;link&#34; href=&#34;https://tailscale.com/blog/how-nat-traversal-works&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;How NAT traversal works&lt;/a&gt;」一文。&lt;/p&gt;&#xA;&lt;h3 id=&#34;完整連接流程圖示&#34;&gt;完整連接流程圖示&#xA;&lt;/h3&gt;&lt;pre&gt;&lt;code class=&#34;language-mermaid&#34;&gt;flowchart TD&#xA;    A[設備 A 啟動 Tailscale] --&gt; B[通過 DERP 服務器建立初始連接]&#xA;    B --&gt; C[交換網絡信息和 WireGuard 密鑰]&#xA;    C --&gt; D[雙方並行進行 NAT 類型探測]&#xA;    D --&gt; E{能否直連？}&#xA;    E -- 是 --&gt; F[建立 P2P 直連隧道]&#xA;    F --&gt; G[定期檢測連接質量]&#xA;    G --&gt; H{直連優於 DERP？}&#xA;    H -- 是 --&gt; I[切換大部分流量至直連通道]&#xA;    H -- 否 --&gt; J[繼續通過 DERP 轉發部分或全部流量]&#xA;    E -- 否 --&gt; J&#xA;&#xA;    style B fill:#e3f2fd,stroke:#2196f3,color:#000&#xA;    style F fill:#e8f5e9,stroke:#4caf50,color:#000&#xA;    style J fill:#fff3e0,stroke:#ff9800,color:#000&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;自建-derp&#34;&gt;自建 DERP&#xA;&lt;/h2&gt;&lt;p&gt;Tailscale 的安裝在各個平臺上都相對簡單，官方文檔已經提供了詳細的操作指南。本文將不再贅述安裝過程，以下內容默認你已經在相關設備上成功安裝並登錄了 Tailscale。&lt;/p&gt;&#xA;&lt;h3 id=&#34;為什麼要自建-derp&#34;&gt;為什麼要自建 DERP？&#xA;&lt;/h3&gt;&lt;p&gt;Tailscale 在全球部署了眾多 DERP &lt;sup id=&#34;fnref:2&#34;&gt;&lt;a href=&#34;#fn:2&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;中繼服務器，用於在打洞失敗時接管流量中轉。但由於眾所周知的原因，中國大陸並沒有官方部署的 DERP 節點。這意味著：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;一旦 NAT 打洞失敗，所有流量都必須繞行海外的 DERP 節點，延遲高、速度慢，體驗極差；&lt;/li&gt;&#xA;&lt;li&gt;某些官方 DERP 節點容易被 GFW 干擾，可能出現連接中斷、握手失敗等問題；&lt;/li&gt;&#xA;&lt;li&gt;即使打洞成功，Tailscale 仍需通過 DERP 交換路由信息和 WireGuard 密鑰，如果 DERP 不可達，連接質量也會受到影響。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;因此，在國內網絡環境下，自建一個本地 DERP 節點，不僅可以顯著提高連接穩定性和傳輸性能，還能規避部分網絡封鎖所帶來的不可預期問題，是一個非常值得做的優化。&lt;/p&gt;&#xA;&lt;h3 id=&#34;準備工作&#34;&gt;準備工作&#xA;&lt;/h3&gt;&lt;p&gt;前面提到，DERP 是基於 HTTP 的，所以你需要準備好一個 HTTP 反代服務，並自行解決 SSL 證書的簽發等基礎問題。本文使用 Docker 部署，在部署開始之前，你需要在你的服務器上裝好 Docker 以及 Docker Compose 等附加組件。為了編輯配置文件，你當然也得知道如何使用 nano 之類的編輯器。為了最好的效果，你的服務器最好擁有靜態公網 IPv4+IPv6 雙棧地址。&lt;/p&gt;&#xA;&lt;p&gt;如果以上條件都具備，就可以開始部署了。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;mkdir tailscale-derp &amp;&amp; cd tailscale-derp&#xA;nano docker-compose.yml&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;docker-composeyml&#34;&gt;docker-compose.yml&#xA;&lt;/h3&gt;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;services:&#xA;  derper:&#xA;    name: tailscale-derp&#xA;    image: fredliang/derper&#xA;    environment:&#xA;      - DERP_DOMAIN=derp.nightcity.pub&#xA;      - DERP_VERIFY_CLIENTS=true&#xA;      - DERP_ADDR=:4433&#xA;    network_mode: host&#xA;    restart: unless-stopped&#xA;    volumes:&#xA;      - &#34;/var/run/tailscale/tailscaled.sock:/var/run/tailscale/tailscaled.sock&#34;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;network_mode: host&lt;/code&gt;是一個關鍵配置，表示容器將共享宿主機的網絡棧。如果使用 Docker 默認的 bridge 網絡模式，容器的網絡會經過 Docker 內網轉發，會造成 DERP 的 STUN 服務識別到 Docker &lt;code&gt;172.17.0.0/16&lt;/code&gt;網段下的地址，而&#xA;無法識別到客戶端正確的外網地址，導致 Tailscale 客戶端無法正確連接。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;volumes:&#xA;  - &#34;/var/run/tailscale/tailscaled.sock:/var/run/tailscale/tailscaled.sock&#34;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;前面提到所有通過 DERP 的數據都是端到端加密的，DERP 並不知道是誰在使用，這意味著如果不採取措施，任何知道你 DERP 服務器地址和端口號的人都可以使用它。這條配置的作用是掛載宿主機的 Tailscale 套接字文件到容器內，目的是允許 derper 服務通過 Tailscale 的 tailscaled 服務進行身份驗證。配合&lt;code&gt;DERP_VERIFY_CLIENTS=true&lt;/code&gt;，可以防止你的 DERP 節點被他人白嫖。&lt;/p&gt;&#xA;&lt;p&gt;需要注意的是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;宿主機必須已經安裝並登陸 Tailscale 客戶端（tailscaled），否則這個文件不存在，容器會報錯；&lt;/li&gt;&#xA;&lt;li&gt;tailscaled 必須以 root 權限運行，才能創建這個 sock 文件。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 id=&#34;反向代理&#34;&gt;反向代理&#xA;&lt;/h3&gt;&lt;p&gt;以 Caddy 為例，需要反向代理 4433 端口，併為其部署 SSL 證書。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-caddyfile&#34;&gt;{&#xA;        email webmaster@l3zc.com&#xA;}&#xA;&#xA;*.l3zc.com {&#xA;        encode gzip&#xA;&#xA;        tls {&#xA;                dns dnspod APP_ID,APP_KEY&#xA;                resolvers 119.29.29.29 223.5.5.5&#xA;        }&#xA;&#xA;        @derp host derp.l3zc.com&#xA;        reverse_proxy @derp :4433&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;需要注意的是，如果你和我一樣使用 Caddy 配合 dnsproviders 申請泛域名證書，Tailscale 的 MagicDNS 可能會導致 Caddy 本地證書驗證失敗而報錯，需要手動指定&lt;code&gt;resolvers&lt;/code&gt;參數解決。&lt;/p&gt;&#xA;&lt;p&gt;訪問剛剛反代的節點，如果出現以下頁面，說明配置正確。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/04/tailscale-setup-recap/image_hu_8846b1a6f5d79837.webp&#34; alt=&#34;DERP 搭建成功&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;h3 id=&#34;配置-acl-策略&#34;&gt;配置 ACL 策略&#xA;&lt;/h3&gt;&lt;p&gt;打開 Tailscale 控制檯的「&lt;a class=&#34;link&#34; href=&#34;https://login.tailscale.com/admin/acls/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;Access Controls&lt;/a&gt;」頁面配置 ACL 策略，將配置好的 DERP 加上。&lt;/p&gt;&#xA;&lt;p&gt;Tailscale 的 ACL 策略是用 HuJSON&lt;sup id=&#34;fnref:3&#34;&gt;&lt;a href=&#34;#fn:3&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;3&lt;/a&gt;&lt;/sup&gt; 寫的，想要在 VSCode 中編輯，選擇語言為「JSON with Comments（jsonc）」即可。以下是一個配置示例：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-jsonc&#34;&gt;{&#xA;  &#34;acls&#34;: [{ &#34;action&#34;: &#34;accept&#34;, &#34;src&#34;: [&#34;*&#34;], &#34;dst&#34;: [&#34;*:*&#34;] }],&#xA;  &#34;ssh&#34;: [&#xA;    {&#xA;      &#34;action&#34;: &#34;check&#34;,&#xA;      &#34;src&#34;: [&#34;autogroup:member&#34;],&#xA;      &#34;dst&#34;: [&#34;autogroup:self&#34;],&#xA;      &#34;users&#34;: [&#34;autogroup:nonroot&#34;, &#34;root&#34;]&#xA;    }&#xA;  ],&#xA;&#xA;  // 自建 DERP 配置&#xA;  &#34;derpMap&#34;: {&#xA;    &#34;OmitDefaultRegions&#34;: false, // 改為 true 以排除官方 DERP&#xA;    &#34;Regions&#34;: {&#xA;      &#34;900&#34;: {&#xA;        &#34;RegionID&#34;: 900,&#xA;        &#34;RegionCode&#34;: &#34;sha&#34;,&#xA;        &#34;RegionName&#34;: &#34;Shanghai&#34;,&#xA;        &#34;Nodes&#34;: [&#xA;          {&#xA;            &#34;Name&#34;: &#34;myderp&#34;,&#xA;            &#34;RegionID&#34;: 900,&#xA;            &#34;HostName&#34;: &#34;derp.l3zc.com&#34;&#xA;          }&#xA;        ]&#xA;      }&#xA;    }&#xA;  }&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Tailscale 保留&lt;code&gt;RegionID&lt;/code&gt;中的 1-899 作為官方節點，自建節點的 RegionID 必須大於等於 900。&lt;/p&gt;&#xA;&lt;h3 id=&#34;測試連接&#34;&gt;測試連接&#xA;&lt;/h3&gt;&lt;p&gt;配置好 ACL 並保存，Tailscale 會自動為所有客戶端同步配置，稍等片刻在客戶端用&lt;code&gt;tailscale netcheck&lt;/code&gt;測試連接。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/04/tailscale-setup-recap/image-1_hu_25ccc2304c1a66fd.webp&#34; alt=&#34;用 tailscale netcheck 測試連接&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;需要注意返回的 IP 是否是自己真實的公網 IP，若返回了&lt;code&gt;172.17.0.0/16&lt;/code&gt;網段的地址，說明你 Docker 部分配置錯了。&lt;/p&gt;&#xA;&lt;h2 id=&#34;與科學上網並存在-exit-node-劫持流量到-clash-內核&#34;&gt;與科學上網並存：在 Exit Node 劫持流量到 Clash 內核&#xA;&lt;/h2&gt;&lt;h3 id=&#34;聲明-exit-node&#34;&gt;聲明 Exit Node&#xA;&lt;/h3&gt;&lt;p&gt;在家中準備一個 24 小時開機的設備，可以是樹莓派，可以是 MacMini。在上面安裝 Tailscale 並將其聲明為 Exit Node，並根據需要在 Tailscale 內網聲明家庭內網網段，隨後在控制檯啟用這個設備作為 Exit Node，你就獲得了一個免費的 VPN，可以讓你在陌生的網絡環境中保持安全。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;sudo tailscale up --advertise-exit-node --advertise-routes 192.168.1.0/24&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/04/tailscale-setup-recap/image-2_hu_bac8de7f1a5258d0.webp&#34; alt=&#34;找到 Route Settings&#34; /&gt;&#xA; &#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/04/tailscale-setup-recap/Snipaste_2025-04-13_21-20-09_hu_d8313f59e1940d09.webp&#34; alt=&#34;啟用相關設置&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;廣播完成後，無論身處何地，只要能連上 Tailscale 網絡，就能訪問家中所有的內網設備。&lt;/p&gt;&#xA;&lt;h3 id=&#34;啟用-ip-轉發和禁用-udp-gro&#34;&gt;啟用 IP 轉發和禁用 UDP GRO&lt;sup id=&#34;fnref:4&#34;&gt;&lt;a href=&#34;#fn:4&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;4&lt;/a&gt;&lt;/sup&gt;&#xA;&lt;/h3&gt;&lt;p&gt;啟用 IP 轉發是樹莓派等設備作為 Exit Node 所必須的配置，這裡以樹莓派為例，如果你使用其他設備，請自行查閱 Tailscale 官網教程。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;echo &#39;net.ipv4.ip_forward = 1&#39; | sudo tee -a /etc/sysctl.d/99-tailscale.conf&#xA;echo &#39;net.ipv6.conf.all.forwarding = 1&#39; | sudo tee -a /etc/sysctl.d/99-tailscale.conf&#xA;sudo sysctl -p /etc/sysctl.d/99-tailscale.conf&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;根據 Tailscale 官方的說法&lt;sup id=&#34;fnref:5&#34;&gt;&lt;a href=&#34;#fn:5&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;5&lt;/a&gt;&lt;/sup&gt;，禁用 UDP GRO&lt;sup id=&#34;fnref:6&#34;&gt;&lt;a href=&#34;#fn:6&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;6&lt;/a&gt;&lt;/sup&gt; 可以提升轉發性能，但官方的持久化教程似乎在樹莓派上無效，好在我們可以手動配置。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;# 安裝 ethtool&#xA;sudo apt update &amp;&amp; sudo apt install ethtool -y&#xA;&#xA;# 關閉 UDP GRO&#xA;sudo ethtool -K eth0 gro off&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;針對持久化的問題手動編寫 systemd 配置文件：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;# 創建服務文件&#xA;sudo nano /etc/systemd/system/ethtool.service&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;文件內容如下：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-systemd&#34;&gt;[Unit]&#xA;Description=Configure eth0 GRO&#xA;After=network.target&#xA;&#xA;[Service]&#xA;Type=oneshot&#xA;ExecStart=/sbin/ethtool -K eth0 gro off&#xA;&#xA;[Install]&#xA;WantedBy=multi-user.target&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;隨後啟動服務：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;sudo systemctl daemon-reload&#xA;sudo systemctl enable ethtool&#xA;sudo systemctl start ethtool&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;在-exit-node-劫持流量&#34;&gt;在 Exit Node 劫持流量&#xA;&lt;/h3&gt;&lt;p&gt;我的 Exit Node 是一臺樹莓派，在樹莓派上配置好 Exit Node 之後就來到了最後一步，也就是劫持手機發送到 Exit Node 上的流量，實現科學上網。出於穩定性原因，我不希望在家庭主路由上直接運行代理軟件，為了實現這一點，直接在樹莓派上劫持流量是唯一的選擇。&lt;/p&gt;&#xA;&lt;p&gt;首先安裝 ShellCrash，請自行根據你的需要導入配置文件、配置自動任務等：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;export url=&#39;https://fastly.jsdelivr.net/gh/juewuy/ShellCrash@master&#39; &amp;&amp; wget -q --no-check-certificate -O /tmp/install.sh $url/install.sh  &amp;&amp; bash /tmp/install.sh &amp;&amp; source /etc/profile &amp;&gt; /dev/null&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;隨後啟動服務，修改修改防火牆運行模式為純淨模式，我個人建議將 SNI 嗅探打開，並將 DNS 模式從&lt;code&gt;fake-ip&lt;/code&gt;切換為&lt;code&gt;redir-host&lt;/code&gt;&lt;sup id=&#34;fnref:7&#34;&gt;&lt;a href=&#34;#fn:7&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;7&lt;/a&gt;&lt;/sup&gt;，同時啟用 IPv6 透明代理。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/04/tailscale-setup-recap/image-5_hu_9e9d06f0578b4304.webp&#34; alt=&#34;修改防火牆劫持範圍&#34; /&gt;&#xA; &#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/04/tailscale-setup-recap/image-3_hu_efcbb45f4cd30701.webp&#34; alt=&#34;啟用域名嗅探&#34; /&gt;&#xA; &#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/04/tailscale-setup-recap/image-6_hu_d681ee087b8dd14a.webp&#34; alt=&#34;修改端口設置&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;設置純淨模式的目的是手動配置 iptables 以實現更精準的流量劫持。我們直接用 iptables 劫持所有 tailscale 網卡作為 Exit Node 轉發的流量，首先用&lt;code&gt;ifconfig&lt;/code&gt;查看 tailscale 網卡的名稱，默認情況下一般為 Tailscale 0：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;root@raspberrypi:~# ifconfig&#xA;eth0: ......&#xA;&#xA;tailscale0: flags=4305&lt;UP,POINTOPOINT,RUNNING,NOARP,MULTICAST&gt;  mtu 1280&#xA;        inet 100.111.19.50  netmask 255.255.255.255  destination 100.111.19.50&#xA;        inet6 fd7a:115c:a1e0::3d01:1332  prefixlen 128  scopeid 0x0&lt;global&gt;&#xA;        inet6 fe80::c0d5:1a1b:2005:48eb  prefixlen 64  scopeid 0x20&lt;link&gt;&#xA;        unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00  txqueuelen 500  (UNSPEC)&#xA;        RX packets 6780155  bytes 958732442 (914.3 MiB)&#xA;        RX errors 0  dropped 0  overruns 0  frame 0&#xA;        TX packets 4811113  bytes 10858316472 (10.1 GiB)&#xA;        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;劫持&lt;code&gt;tailscale0&lt;/code&gt;網卡的所有流量到本地 Clash 內核監聽端口&lt;code&gt;7892&lt;/code&gt;，這個設置在 ShellCrash 中叫做「靜態路由端口」。以及，如果你不想和我一樣遇到莫名其妙的網絡問題，就一定不要忘記劫持 IPv6。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;# IPv4 劫持 tailscale0&#xA;iptables -t nat -A PREROUTING -i tailscale0 -p tcp -j REDIRECT --to-ports 7892&#xA;&#xA;# IPv6 劫持 tailscale0&#xA;ip6tables -t nat -A PREROUTING -i tailscale0 -p tcp -j REDIRECT --to-ports 7892&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;進階用-tproxy-劫持-udp-流量&#34;&gt;進階：用 TProxy 劫持 UDP 流量&#xA;&lt;/h3&gt;&lt;p&gt;iptables 的 REDIRECT 只能重定向 TCP 流量，UDP 沒有連接狀態（無連接協議），所以 REDIRECT 無法保留目標地址，導致透明代理無法知道原始目標地址。&lt;/p&gt;&#xA;&lt;p&gt;所以，如果你用如下方式劫持 UDP 流量：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;iptables -t nat -A PREROUTING -i tailscale0 -p udp -j REDIRECT --to-ports 7892&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;則這個規則不會生效或代理行為不正常。&lt;/p&gt;&#xA;&lt;p&gt;那麼，有辦法代理 UDP 流量嗎？有的兄弟，有的。但前提是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;核心支持 UDP 透明代理（Clash Premium 和 Mihomo 都支持）；&lt;/li&gt;&#xA;&lt;li&gt;使用 TProxy 模式，而不是 REDIRECT；&lt;/li&gt;&#xA;&lt;li&gt;正確配置了 iptables mangle 表和 policy routing；&lt;/li&gt;&#xA;&lt;li&gt;代理配置文件中啟用了 UDP 代理（如 mode: rule + udp: true）。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;假設你已經滿足第一條、第二條和最後一條，則以下是一個示例&lt;sup id=&#34;fnref:8&#34;&gt;&lt;a href=&#34;#fn:8&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;8&lt;/a&gt;&lt;/sup&gt;：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;# 創建自定義鏈&#xA;sudo iptables -t mangle -N SHELLCRASH&#xA;&#xA;# 根據自己的需要忽略本地流量&#xA;sudo iptables -t mangle -A SHELLCRASH -d 127.0.0.1/8 -j RETURN&#xA;sudo iptables -t mangle -A SHELLCRASH -d 100.64.0.0/10 -j RETURN&#xA;sudo iptables -t mangle -A SHELLCRASH -d 192.168.1.0/24 -j RETURN&#xA;sudo iptables -t mangle -A SHELLCRASH -d 172.17.0.0/16 -j RETURN&#xA;&#xA;# mark UDP 和 TCP 到代理&#xA;sudo iptables -t mangle -A SHELLCRASH -p tcp -j TPROXY --on-port 7893 --tproxy-mark 233&#xA;sudo iptables -t mangle -A SHELLCRASH -p udp -j TPROXY --on-port 7893 --tproxy-mark 233&#xA;&#xA;# 接口跳轉&#xA;sudo iptables -t mangle -A PREROUTING -i tailscale0 -j SHELLCRASH&#xA;&#xA;# 路由表配置&#xA;echo &#34;233 shellcrash&#34; | sudo tee -a /etc/iproute2/rt_tables&#xA;sudo ip rule add fwmark 233 lookup shellcrash&#xA;sudo ip route add local 0.0.0.0/0 dev lo table shellcrash&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;當然，不要忘記 IPv6：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;# 創建鏈&#xA;sudo ip6tables -t mangle -N SHELLCRASH6&#xA;&#xA;# 跳過本地地址&#xA;sudo ip6tables -t mangle -A SHELLCRASH6 -d ::1/128 -j RETURN&#xA;sudo ip6tables -t mangle -A SHELLCRASH6 -d fd7a:115c:a1e0::/48 -j RETURN&#xA;&#xA;# 標記 TCP/UDP&#xA;ip6tables -t mangle -A SHELLCRASH6 -i tailscale0 -p tcp -j TPROXY --on-port 7893 --tproxy-mark 233&#xA;ip6tables -t mangle -A SHELLCRASH6 -i tailscale0 -p udp -j TPROXY --on-port 7893 --tproxy-mark 233&#xA;&#xA;# 接口跳轉&#xA;sudo ip6tables -t mangle -A PREROUTING -i tailscale0 -j SHELLCRASH6&#xA;&#xA;# 路由表配置&#xA;echo &#34;233 shellcrash&#34; | sudo tee -a /etc/iproute2/rt_tables&#xA;sudo ip -6 rule add fwmark 233 lookup shellcrash&#xA;sudo ip -6 route add local ::/0 dev lo table shellcrash&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;路由規則持久化&#34;&gt;路由規則持久化&#xA;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;ip rule&lt;/code&gt;和&lt;code&gt;ip route&lt;/code&gt;創建的規則在重啟後會丟失，所以需要我們手動持久化，最簡單直接的方法就是創建一個腳本，並將其添加至 crontab。&lt;/p&gt;&#xA;&lt;p&gt;創建一個腳本：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;sudo nano /usr/local/bin/policy-route.sh&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;編輯為如下內容：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;#!/bin/bash&#xA;&#xA;# IPv4 策略路由&#xA;ip rule add fwmark 233 lookup 233&#xA;ip route add local 0.0.0.0/0 dev lo table 233&#xA;&#xA;# IPv6 策略路由&#xA;ip -6 rule add fwmark 233 lookup 233&#xA;ip -6 route add local ::/0 dev lo table 233&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;授予執行權限後編輯 Crontab：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;sudo chmod +x /usr/local/bin/policy-route.sh&#xA;sudo crontab -e&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在 crontab 文件底部加上如下內容：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-crontab&#34;&gt;@reboot /usr/local/bin/policy-route.sh&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;原理解釋tproxy-到底是怎麼轉發-udp-流量的&#34;&gt;原理解釋：TProxy 到底是怎麼轉發 UDP 流量的？&#xA;&lt;/h3&gt;&lt;p&gt;如果你看到這裡，也許會產生疑惑：為什麼整個流程中，我們沒有在 iptables 裡寫&lt;code&gt;--to-ports&lt;/code&gt;，也沒看到目標地址被改寫，UDP 流量就莫名其妙地被代理了？這是怎麼做到的？&lt;/p&gt;&#xA;&lt;p&gt;要解釋這個問題，我們先來看 TProxy 和 REDIRECT 的根本區別：&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;REDIRECT 模式：&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;使用 iptables nat 表；&lt;/li&gt;&#xA;&lt;li&gt;將目標地址改寫為本地地址（比如 127.0.0.1:7892）；&lt;/li&gt;&#xA;&lt;li&gt;通常用於 TCP 流量；&lt;/li&gt;&#xA;&lt;li&gt;不能保留真實目標地址；&lt;/li&gt;&#xA;&lt;li&gt;需要指定&lt;code&gt;--to-ports&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-mermaid&#34;&gt;flowchart TB&#xA;    A[客戶端設備&lt;br&gt;通過 Tailscale 發起 TCP 請求]&#xA;    B[tailscale0 接口接收流量]&#xA;    C[iptables NAT PREROUTING&lt;br&gt;REDIRECT --to-ports 7892]&#xA;    D[ShellCrash 本地監聽&lt;br&gt;127.0.0.1:7892]&#xA;    E[ShellCrash 發起新 TCP 請求&lt;br&gt;→ 目標服務器]&#xA;    F[響應從網絡返回&lt;br&gt;ShellCrash 轉發響應]&#xA;    G[響應回到客戶端設備]&#xA;&#xA;    A --&gt; B --&gt; C --&gt; D --&gt; E --&gt; F --&gt; G&#xA;&#xA;    style C fill:#f9f,stroke:#aaa,stroke-width:1px,color:#000&#xA;    style D fill:#bbf,stroke:#aaa,stroke-width:1px,color:#000&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;TPROXY 模式：&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;使用 iptables mangle 表；&lt;/li&gt;&#xA;&lt;li&gt;不修改目標 IP，而是保留原始目標地址；&lt;/li&gt;&#xA;&lt;li&gt;通過 fwmark 和 policy routing 將報文路由到 &lt;code&gt;lo&lt;/code&gt;；&lt;/li&gt;&#xA;&lt;li&gt;代理程序監聽一個特殊端口（例如 7893），並啟用 &lt;code&gt;IP_TRANSPARENT&lt;/code&gt;；&lt;/li&gt;&#xA;&lt;li&gt;支持 UDP 和 TCP；&lt;/li&gt;&#xA;&lt;li&gt;不需要 iptables 內指定 &lt;code&gt;--to-ports&lt;/code&gt;，因為不是 NAT，而是標記 + 路由。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-mermaid&#34;&gt;flowchart TB&#xA;    A[客戶端設備&lt;br&gt;通過 Tailscale 發起 TCP/UDP 請求]&#xA;    B[tailscale0 接口接收流量]&#xA;    C[iptables MANGLE PREROUTING&lt;br&gt;打上 fwmark 233]&#xA;    D[ip rule: fwmark 233&lt;br&gt;使用 routing table shellcrash]&#xA;    E[ip route: local 0.0.0.0/0&lt;br&gt;dev lo table shellcrash]&#xA;    F[ShellCrash 在 lo:7893 監聽&lt;br&gt;IP_TRANSPARENT 模式]&#xA;    G[ShellCrash 獲取原始目標地址&lt;br&gt;發起代理連接]&#xA;    H[響應從網絡返回&lt;br&gt;ShellCrash 轉發響應]&#xA;    I[響應回到客戶端設備]&#xA;&#xA;    A --&gt; B --&gt; C --&gt; D --&gt; E --&gt; F --&gt; G --&gt; H --&gt; I&#xA;&#xA;    style C fill:#f9f,stroke:#aaa,stroke-width:1px,color:#000&#xA;    style F fill:#bbf,stroke:#aaa,stroke-width:1px,color:#000&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;TProxy 不使用 DNAT/REDIRECT，而是通過 mangle 表給數據包打上 mark，然後通過 policy routing（&lt;code&gt;ip rule&lt;/code&gt; + &lt;code&gt;ip route&lt;/code&gt;）將這些數據包送到 &lt;code&gt;lo&lt;/code&gt; 接口。代理程序（如 Clash / ShellCrash）監聽在 &lt;code&gt;lo&lt;/code&gt;&lt;sup id=&#34;fnref:9&#34;&gt;&lt;a href=&#34;#fn:9&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;9&lt;/a&gt;&lt;/sup&gt; 上的端口（例如 7893），通過啟用 &lt;code&gt;IP_TRANSPARENT&lt;/code&gt; 選項，可以讀取數據包的原始目標 IP 和端口並進行代理轉發。&lt;/p&gt;&#xA;&lt;p&gt;簡而言之，TProxy 模式只需要：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;iptables&lt;/code&gt; 給數據包打上 mark；&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;ip rule&lt;/code&gt; + &lt;code&gt;ip route&lt;/code&gt; 將這些包送到 &lt;code&gt;lo&lt;/code&gt;；&lt;/li&gt;&#xA;&lt;li&gt;程序監聽 &lt;code&gt;lo&lt;/code&gt; 上的端口並開啟 &lt;code&gt;IP_TRANSPARENT&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;所以不需要在 iptables 中指定 &lt;code&gt;--to-ports&lt;/code&gt;，因為目標 IP 和端口保持不變，代理程序自己可以感知並處理。&lt;/p&gt;&#xA;&lt;h3 id=&#34;iptables-規則持久化&#34;&gt;iptables 規則持久化&#xA;&lt;/h3&gt;&lt;p&gt;安裝&lt;code&gt;iptables-persistent&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;sudo apt update&#xA;sudo apt install iptables-persistent&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;安裝過程中會提示你是否保存當前的 IPv4 和 IPv6 配置，選擇「是」即可。之後如果你添加了新的規則，記得執行保存命令：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;# 保存當前 IPv4/IPv6 規則&#xA;sudo netfilter-persistent save&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;保存後的規則文件路徑：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;IPv4：&lt;code&gt;/etc/iptables/rules.v4&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;IPv6：&lt;code&gt;/etc/iptables/rules.v6&lt;/code&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;你也可以直接編輯上面的&lt;code&gt;rules.v4&lt;/code&gt;/&lt;code&gt;rules.v6&lt;/code&gt;文件，按需修改。&lt;/p&gt;&#xA;&lt;h2 id=&#34;最終效果&#34;&gt;最終效果&#xA;&lt;/h2&gt;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/04/tailscale-setup-recap/Screenshot_2025-04-14-18-05-18-259_com.tailscale._hu_783d6158c36336e1.webp&#34; alt=&#34;基本都能打洞成功&#34; /&gt;&#xA; &#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/04/tailscale-setup-recap/Screenshot_2025-04-14-18-04-45-053_com.cnspeedtes_hu_a47b6440dc775101.webp&#34; alt=&#34;速度尚可接受&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;這套方案的使用體驗取決於你家的上行帶寬，我家的網絡是下行 500M 上行 60M，目前沒有遇到一次打洞失敗的情況，所以基本都能跑滿，延遲也尚可接受，並且可以在外地隨時隨地端到端加密訪問家中的 Immich 和 OpenWRT 路由器等設備以及實現科學上網，總體來看，我還算比較滿意。&lt;/p&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;參考: &lt;a class=&#34;link&#34; href=&#34;https://icloudnative.io/posts/custom-derp-servers/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;https://icloudnative.io/posts/custom-derp-servers/&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:2&#34;&gt;&#xA;&lt;p&gt;參考: &lt;a class=&#34;link&#34; href=&#34;https://tailscale.com/kb/1232/derp-servers&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;https://tailscale.com/kb/1232/derp-servers&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:2&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:3&#34;&gt;&#xA;&lt;p&gt;有關 HuJSON: &lt;a class=&#34;link&#34; href=&#34;https://github.com/tailscale/hujson&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;https://github.com/tailscale/hujson&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:3&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:4&#34;&gt;&#xA;&lt;p&gt;參考： &lt;a class=&#34;link&#34; href=&#34;https://tailscale.com/kb/1408/quick-guide-exit-nodes&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;https://tailscale.com/kb/1408/quick-guide-exit-nodes&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:4&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:5&#34;&gt;&#xA;&lt;p&gt;參考: &lt;a class=&#34;link&#34; href=&#34;https://tailscale.com/kb/1320/performance-best-practices#linux-optimizations-for-subnet-routers-and-exit-nodes&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;https://tailscale.com/kb/1320/performance-best-practices#linux-optimizations-for-subnet-routers-and-exit-nodes&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:5&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:6&#34;&gt;&#xA;&lt;p&gt;UDP GRO（Generic Receive Offload）是 Linux 內核中的一種網絡優化技術，主要用於合併多個小數據包以提高處理效率。但在設備作為網絡轉發節點的使用場景下，這可能會導致轉發延遲增加和高丟包率環境下的吞吐量下降。&amp;#160;&lt;a href=&#34;#fnref:6&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:7&#34;&gt;&#xA;&lt;p&gt;相較於&lt;code&gt;fake-ip&lt;/code&gt;，&lt;code&gt;redir-host&lt;/code&gt;兼容性更好，出現問題的概率更低，也不會出現開關代理之後短時間內因為&lt;code&gt;fake-ip&lt;/code&gt;殘留而斷網的情況，所以一般情況下我建議使用&lt;code&gt;redir-host&lt;/code&gt;搭配 GeoSite 分流規則使用。我這臺樹莓派的 DNS 上游是&lt;a class=&#34;link&#34; href=&#34;https://blog.l3zc.com/2025/02/what-i-have-done-on-my-dns/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;已經配置好的 SmartDNS&lt;/a&gt;，不存在 DNS 汙染的問題，體驗良好。&amp;#160;&lt;a href=&#34;#fnref:7&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:8&#34;&gt;&#xA;&lt;p&gt;參考: &lt;a class=&#34;link&#34; href=&#34;https://blog.zonowry.com/posts/clash_iptables_tproxy/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;https://blog.zonowry.com/posts/clash_iptables_tproxy/&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:8&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:9&#34;&gt;&#xA;&lt;p&gt;&lt;code&gt;lo&lt;/code&gt; 是 Linux 系統中默認的「迴環接口（Loopback Interface）」，在透明代理中，它不僅處理 localhost 流量，還被用來接收原本屬於外部世界的網絡連接，實現對外部流量的本地劫持和轉發。&amp;#160;&lt;a href=&#34;#fnref:9&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;</description>
        </item><item>
            <title>OBS 多路推流折騰記：那些本不必要的麻煩</title>
            <link>https://blog.l3zc.com/zh-hant-tw/2024/09/my-multi-streaming-setup/</link>
            <pubDate>Wed, 18 Sep 2024 06:16:51 +0000</pubDate>
            <guid>https://blog.l3zc.com/zh-hant-tw/2024/09/my-multi-streaming-setup/</guid>
            <description>&lt;img src=&#34;https://blog.l3zc.com/2024/09/my-multi-streaming-setup/cover_hu_891493d684eb454d.webp&#34; alt=&#34;Featured image of post OBS 多路推流折騰記：那些本不必要的麻煩&#34; /&gt;&lt;p&gt;長話短說，最近我開始直播我的遊戲實況，我並不指望有多少人看，Just, Why not? 再者每個直播平臺都會有直播回放。相當於免費保存我每一次遊戲的錄像，何樂而不為？&lt;/p&gt;&#xA;&lt;p&gt;扯遠了，如你所見，近期我有了多平臺同時直播的需求，然而 OBS 本身顯然不支持同時推流，同時也因為不支持設置代理的特點和眾所周知的原因，無法推流到 twitch.tv，於是我開始尋找解決方案。&lt;/p&gt;&#xA;&lt;h2 id=&#34;明確需求&#34;&gt;明確需求&#xA;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;多路平臺同時直播&lt;/li&gt;&#xA;&lt;li&gt;最好對性能影響小&lt;/li&gt;&#xA;&lt;li&gt;其中一路需要推流需要代理&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;多路推流&#34;&gt;多路推流&#xA;&lt;/h2&gt;&lt;p&gt;多路推流有現成的插件可用：&lt;a class=&#34;link&#34; href=&#34;https://github.com/sorayuki/obs-multi-rtmp&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;Github&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;在 Release 頁面下載&lt;code&gt;.exe&lt;/code&gt;安裝包，安裝完成後記得在「停靠窗口」菜單裡勾選「多路推流」。這時候默認會彈出一個獨立窗口，可以趁這個機會順便調整一下 OBS 的佈局。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2024/09/my-multi-streaming-setup/image_hu_b8b674a93b9f86bc.webp&#34; alt=&#34;啟用多路推流面板&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;插件的設置也沒什麼好說的，用在原版 OBS 的填法在這個插件裡如法炮製即可。&lt;/p&gt;&#xA;&lt;h3 id=&#34;bilibili-直播姬&#34;&gt;Bilibili 直播姬&#xA;&lt;/h3&gt;&lt;p&gt;莫名其妙，B 站小於 50 粉絲的 Up 主只能使用直播姬開播，其他國內平臺做出類似規定者大概也不在少數，啟用第三方推流模式之後，直播面板會顯示推流的配置。此時的不需要按照直播姬給出的推流地址進行配置，因為直播姬莫名其妙的給了一個每次都會變的內網 IP，直接以&lt;code&gt;127.0.0.1&lt;/code&gt;或者&lt;code&gt;localhost&lt;/code&gt;替換即可。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2024/09/my-multi-streaming-setup/image-2_hu_71c45766ee560a9.webp&#34; alt=&#34;Bilibili 直播姬的第三方推流模式&#34; /&gt;&#xA; &#xA;&lt;img src=&#34;https://blog.l3zc.com/2024/09/my-multi-streaming-setup/image-1_hu_17dc4c09eb0ae0af.webp&#34; alt=&#34;插件的設置&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;h2 id=&#34;推流到-twitch&#34;&gt;推流到 Twitch&#xA;&lt;/h2&gt;&lt;p&gt;推流到 Twitch 則會因為 GFW 多出很多不必要的麻煩。&lt;/p&gt;&#xA;&lt;p&gt;OBS 本身並不支持設置代理，起初我打算用 Clash 的 TAP 模式強制代理 OBS 的流量，而 Clash 的 TAP 並不監聽 rtmp 流量，宣告失敗。我決定直接架設一個轉發服務器，這個服務器需要既可以與 Twitch 通信，又可以不受 GFW 的干擾。&lt;/p&gt;&#xA;&lt;p&gt;在網上查到一些信息，Caddy 似乎不支持 RTMP 協議，而 &lt;code&gt;nginx-rtmp&lt;/code&gt; 插件則專門為 RTMP 協議設計。這幾乎是唯一的選擇。&lt;/p&gt;&#xA;&lt;p&gt;部署的前提：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;熟悉基本的 Linux 命令行操作。&lt;/li&gt;&#xA;&lt;li&gt;服務器已經安裝好&lt;code&gt;docker&lt;/code&gt;和&lt;code&gt;docker compose&lt;/code&gt;插件。&lt;/li&gt;&#xA;&lt;li&gt;有合適的命令行文本編輯器，比如&lt;code&gt;nvim&lt;/code&gt;或者絕大部分 distro 自帶的&lt;code&gt;nano&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;新建目錄：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;mkdir nginx-rtmp &amp;&amp; cd nginx-rtmp&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;創建&lt;code&gt;docker-compose.yml&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;services:&#xA;  nginx-rtmp:&#xA;    image: tiangolo/nginx-rtmp:latest&#xA;    container_name: nginx_rtmp&#xA;    ports:&#xA;      - &#34;1935:1935&#34; # RTMP port&#xA;    volumes:&#xA;      - ./nginx.conf:/etc/nginx/nginx.conf&#xA;    restart: unless-stopped&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;創建&lt;code&gt;nginx.conf&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-nginx&#34;&gt;worker_processes  auto;&#xA;&#xA;events {&#xA;    worker_connections  1024;&#xA;}&#xA;&#xA;rtmp {&#xA;    server {&#xA;        listen 1935;&#xA;        chunk_size 4096;&#xA;&#xA;        application live {&#xA;            live on;&#xA;            push rtmp://live.twitch.tv/app/{Twitch 主直播密鑰};&#xA;        }&#xA;    }&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;啟動編排：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;docker compose up -d&#xA;# 老版本 compose 插件則是 docker-compose up&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;當然，別忘了開放防火牆端口。我的防火牆參考了 &lt;a class=&#34;link&#34; href=&#34;https://github.com/chaifeng/ufw-docker#solving-ufw-and-docker-issues&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;To Fix The Docker and UFW Security Flaw Without Disabling Iptables&lt;/a&gt; 一文中提到的配置，相應地，開放端口的命令如下：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;ufw route allow proto tcp from any to any port 1935&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;配置完成後，OBS 內的推流目標地址填入&lt;code&gt;rtmp://服務器ip/live&lt;/code&gt;，推流碼填寫任意值均可。設置完成後即可開始推流。&lt;/p&gt;&#xA;&lt;h3 id=&#34;netch&#34;&gt;Netch&#xA;&lt;/h3&gt;&lt;p&gt;如果沒有服務器，亦可以使用 Netch 代理 OBS 進程進行推流。&lt;/p&gt;&#xA;&lt;p&gt;首先下載 Netch 1.9.2 版本，注意一定要是 1.9.2 版本。添加一個進程模式，將 OBS 文件夾加入，隨後啟用該進程模式的代理即可。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2024/09/my-multi-streaming-setup/image-3_hu_fab77139607785da.webp&#34; alt=&#34;創建一個進程模式&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;h2 id=&#34;尾聲&#34;&gt;尾聲&#xA;&lt;/h2&gt;&lt;p&gt;這次為了在同時在 Twitch 和 B 站上推流，遇到了很多原本不必要的麻煩。我可以怪 OBS 沒有內置代理功能，也可以怪 Clash Verge 等代理工具不支持 RTMP，轉來轉去，最終要怪的，還得是 GFW。雖然最後的結果還算令人滿意，僅僅為了推流而折騰這些東西的體驗實在是稱不上良好。也許我今後我應該去研究修改 Mihomo 內核，讓其支持 RTMP 協議，亦或是自己寫一套獨立的實現單獨轉發 OBS 的流量，不論如何，在這片土地上，想要暢快的直播，道阻且長。&lt;/p&gt;&#xA;&lt;p&gt;最後，歡迎關注我的 Twitch 頻道: &lt;a class=&#34;link&#34; href=&#34;https://www.twitch.tv/powerfullz233&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;https://www.twitch.tv/powerfullz233&lt;/a&gt;&lt;/p&gt;&#xA;</description>
        </item><item>
            <title>docker-mailserver 郵件服務器搭建記錄</title>
            <link>https://blog.l3zc.com/zh-hant-tw/2024/09/docker-mailserver-deployment-recap/</link>
            <pubDate>Fri, 06 Sep 2024 03:57:18 +0000</pubDate>
            <guid>https://blog.l3zc.com/zh-hant-tw/2024/09/docker-mailserver-deployment-recap/</guid>
            <description>&lt;img src=&#34;https://blog.l3zc.com/2024/09/docker-mailserver-deployment-recap/docker-mailserver_hu_a649605529f354c4.webp&#34; alt=&#34;Featured image of post docker-mailserver 郵件服務器搭建記錄&#34; /&gt;&lt;p&gt;某天早上醒來，發現收件箱裡多了封郵件，通知我的郵箱在韓國登錄，當時認為只是偶爾的 IP 誤判，也就沒有多想。可接下來幾天，時不時又有新的郵件進來，而且每次 IP 都不一樣，這下可就有些問題了，再不採取行動，怕不是郵箱真的要被拿去群發郵件。於是趕緊刪掉了域名的 MX、SPF、DMARC 記錄，尋找新的解決方案。&lt;/p&gt;&#xA;&lt;p&gt;我原本只是想改改密碼，結果呢，騰訊企業郵箱改密碼的入口真的巨能藏，到處都找不到。算了，遷移，何必受這氣。&lt;/p&gt;&#xA;&lt;p&gt;於是就有了這篇文章，從最開始的開放防火牆端口開始，到最後的優化送達率，我將分享我搭建 docker-mailserver 郵件服務器的經驗，希望能幫你少踩些坑。&lt;/p&gt;&#xA;&lt;h2 id=&#34;搭建-docker-mailserver-郵件服務器前的準備&#34;&gt;搭建 docker-mailserver 郵件服務器前的準備&#xA;&lt;/h2&gt;&lt;h3 id=&#34;防火牆的處理&#34;&gt;防火牆的處理&#xA;&lt;/h3&gt;&lt;p&gt;首先先開放防火牆的郵件服務常用端口，分別是&lt;code&gt;25&lt;/code&gt;、&lt;code&gt;143&lt;/code&gt;、&lt;code&gt;465&lt;/code&gt;、&lt;code&gt;587&lt;/code&gt;、&lt;code&gt;993&lt;/code&gt;，各分支開放端口的操作不同，請自行谷歌或詢問 AI，以 Ubuntu 使用的&lt;code&gt;ufw&lt;/code&gt;為例：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;ufw allow 25&#xA;ufw allow 143&#xA;ufw allow 465&#xA;ufw allow 587&#xA;ufw allow 993&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;我的防火牆比較特殊，用到了&lt;code&gt;ufw&lt;/code&gt;和 &lt;a class=&#34;link&#34; href=&#34;https://github.com/chaifeng/ufw-docker#solving-ufw-and-docker-issues&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;To Fix The Docker and UFW Security Flaw Without Disabling Iptables&lt;/a&gt; 一文中提到的配置。所以當我想要把 Docker 容器的端口暴露在外時需要做出額外的配置。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;# 開放郵件服務器所需的端口&#xA;ufw route allow proto tcp from any to any port 25&#xA;ufw route allow proto tcp from any to any port 143&#xA;ufw route allow proto tcp from any to any port 465&#xA;ufw route allow proto tcp from any to any port 587&#xA;ufw route allow proto tcp from any to any port 993&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;驗證你的服務器是否具備搭建條件&#34;&gt;驗證你的服務器是否具備搭建條件&#xA;&lt;/h3&gt;&lt;p&gt;很多服務器，尤其是各種廉價的 VPS，並不適合搭建 docker-mailserver，各大郵件服務商為了過濾垃圾郵件，索性將所有曾經有過 Spam 行為的 IP 全部列入黑名單。如果你的 IP 被某個 Spammer 使用過，而恰好又被你的服務器提供商分配給了你，而你折騰了幾個小時的郵件服務器卻愣是收不到郵件，最後發現竟是 IP 被列入黑名單的原因，我不知道你會作何感想。&lt;/p&gt;&#xA;&lt;p&gt;有的服務器提供商的 IP 乍一看確實不在郵件黑名單中，但實際上搭建好之後還是無法發送郵件，為什麼？因為服務器提供商擔心 IP 被濫用，索性將郵件服務的端口全都封鎖了。&lt;/p&gt;&#xA;&lt;p&gt;使用如下腳本檢測你的服務器是否具備搭建郵件服務器的條件：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;bash &lt;(curl -sL IP.Check.Place)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2024/09/docker-mailserver-deployment-recap/image-3_hu_6dacc5e9abb4b1ca.webp&#34; alt=&#34;不能搭建郵件服務器&#34; /&gt;&#xA; &#xA;&lt;img src=&#34;https://blog.l3zc.com/2024/09/docker-mailserver-deployment-recap/image-4_hu_fb1b1c9dd78cde41.webp&#34; alt=&#34;可以搭建郵件服務器&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;如果在這一步發現你的服務器不適合搭建甚至不能搭建&lt;code&gt;docker-mailserver&lt;/code&gt;，那就趁早打住吧，想辦法換一個更乾淨、限制更少的 IP 再說。&lt;/p&gt;&#xA;&lt;h2 id=&#34;搭建過程&#34;&gt;搭建過程&#xA;&lt;/h2&gt;&lt;h3 id=&#34;拉取配置文件&#34;&gt;拉取配置文件&#xA;&lt;/h3&gt;&lt;p&gt;docker-mailserver 需要配置的配置文件有兩個，&lt;code&gt;compose.yaml&lt;/code&gt;和&lt;code&gt;mailserver.env&lt;/code&gt;，分別拉取配置文件：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;mkdir mailserver &amp;&amp; cd mailserver&#xA;sudo apt install wget&#xA;wget -O compose.yaml &#34;https://raw.githubusercontent.com/docker-mailserver/docker-mailserver/master/compose.yaml&#34;&#xA;wget -O mailserver.env &#34;https://github.com/docker-mailserver/docker-mailserver/blob/master/mailserver.env&#34;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;根據需求，可以修改不同的配置，下面是我修改的部分：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;POSTMASTER_ADDRESS=webmaster@l3zc.com&#xA;TZ=Asia/Shanghai&#xA;SSL_TYPE=manual&#xA;SSL_CERT_PATH=/certs/cert.crt&#xA;SSL_KEY_PATH=/certs/cert.key&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;用-caddy-準備和自動維護-tls-證書&#34;&gt;用 Caddy 準備和自動維護 TLS 證書&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;&#xA;&lt;/h3&gt;&lt;p&gt;因為我已經在使用 Caddy，所以我這次就打算直接使用 Caddy 來自動維護證書。根據項目的官方文檔，我只需要在 Caddyfile 裡新增一個子域名，Caddy 就可以自動幫我維護證書。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-caddyfile&#34;&gt;mail.example.com {&#xA;  tls internal {&#xA;    # ?? 這不是直接生成的內部證書麼&#xA;    key_type rsa2048&#xA;  }&#xA;&#xA;  # Optional, can be useful for troubleshooting&#xA;  # connection to Caddy with correct certificate:&#xA;  respond &#34;Hello DMS&#34;&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;當我更新完 DNS 記錄，實際操作起來發現，我的情況比較特殊：我這臺服務器上並沒有用到 Caddy 的自動申請證書功能，而是使用了15年有效期的泛域名 Cloudflare Origin Certificate。即使我不為這個子域名指定證書文件，Caddy 也會在加載這張泛域名證書後認為無需再申請新的證書。這就很尷尬了，用 Caddy 的時候 Cloudflare Origin Certificate 和郵件服務器只能二選一。只好安裝了&lt;code&gt;dns.providers.cloudflare&lt;/code&gt;，並放棄使用 Cloudflare Origin Certificate，轉而讓 Caddy 自動為每個子域名申請和維護 TLS/SSL 證書，反正都是自動的，眼不見心不煩。&lt;/p&gt;&#xA;&lt;p&gt;安裝&lt;code&gt;dns.providers.cloudflare&lt;/code&gt;後的全局配置：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-caddyfile&#34;&gt;{&#xA;        acme_dns cloudflare {API_KEY}&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;最後把 Caddy 的證書存儲位置映射到 docker-mailserver 容器內。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;volumes:&#xA;  - /home/user/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/yourdomain.tld/:/certs/&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果 Caddy 以 root 用戶運行，則證書的位置應當為&lt;code&gt;/root/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/yourdomain.tld/&lt;/code&gt;，做出相應的修改即可。&lt;/p&gt;&#xA;&lt;p&gt;然後就是之前提到過的配置，我將證書存儲位置映射到了容器內的&lt;code&gt;/certs/&lt;/code&gt;目錄，所以配置為：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;SSL_CERT_PATH=/certs/cert.crt&#xA;SSL_KEY_PATH=/certs/cert.key&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;首次啟動容器&#34;&gt;首次啟動容器&#xA;&lt;/h3&gt;&lt;p&gt;首次啟動容器需要立即（120 秒內）創建新的 Postmaster 賬號。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;docker compose up -d&#xA;docker exec -it &lt;CONTAINER NAME&gt; setup email add &lt;postmaster@yourdomain.tld&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;優化送達率&#34;&gt;優化送達率&#xA;&lt;/h2&gt;&lt;h3 id=&#34;dns-記錄&#34;&gt;DNS 記錄&#xA;&lt;/h3&gt;&lt;ul&gt;&#xA;&lt;li&gt;一條 A 記錄，名稱隨意（假設為&lt;code&gt;mail&lt;/code&gt;），指向服務器 IP（Cloudflare 需使用「僅 DNS」）&lt;/li&gt;&#xA;&lt;li&gt;一條 MX 記錄，Apex 域（@），指向&lt;code&gt;mail.yourdomain.tld&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;一條 TXT 記錄（SPF），Apex 域（@），可以在&lt;a class=&#34;link&#34; href=&#34;https://mxtoolbox.com/SPFRecordGenerator.aspx&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;這裡&lt;/a&gt;生成(可選)。&lt;/li&gt;&#xA;&lt;li&gt;一條 TXT 記錄（DMARC），名稱&lt;code&gt;_dmarc&lt;/code&gt;，可以在&lt;a class=&#34;link&#34; href=&#34;https://mxtoolbox.com/DMARCRecordGenerator.aspx&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;這裡&lt;/a&gt;生成（可選）。&lt;/li&gt;&#xA;&lt;li&gt;一條 TXT 記錄（DKIM），名稱&lt;code&gt;mail._domainkey&lt;/code&gt;，具體配置在後文（可選）。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 id=&#34;dkim-spf-和-dmarc&#34;&gt;DKIM, SPF 和 DMARC&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a href=&#34;#fn:2&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;&#xA;&lt;/h3&gt;&lt;p&gt;什麼是 DKIM，SPF 和 DMARC？簡單來說，DKIM 是一個數字簽名機制；SPF 類似於「員工名單」，記錄所有合法的發件服務器；DMARC 則用於指示收件方的服務器如何處理未通過 DKIM 和 SPF 驗證的郵件。&lt;sup id=&#34;fnref:3&#34;&gt;&lt;a href=&#34;#fn:3&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&#xA;&lt;p&gt;首先設置 DKIM，在服務器上生成公私鑰對。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;docker exec -it &lt;CONTAINER NAME&gt; setup config dkim&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;生成後的公私鑰對可以在映射的容器目錄&lt;code&gt;./docker-data/dms/config/opendkim/keys/&lt;/code&gt;下找到。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2024/09/docker-mailserver-deployment-recap/image_hu_1f64fffc01f8db13.webp&#34; alt=&#34;mail.txt 便是公鑰的 DNS Zone file&#34; /&gt;&#xA; &#xA;&lt;img src=&#34;https://blog.l3zc.com/2024/09/docker-mailserver-deployment-recap/image-1_hu_d6c86630e3947c78.webp&#34; alt=&#34;Cloudflare 的導入入口&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;下載 mail.txt（複製其內所有內容粘貼到本地文件也可以），將其導入到 Cloudflare Dashboard 即可完成 DKIM 設置。需要注意配置完成 DKIM 後需要重啟容器才能生效。&lt;sup id=&#34;fnref:4&#34;&gt;&lt;a href=&#34;#fn:4&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;docker compose up -d --force-recreate&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;前面提到 SPF 相當於一個「員工名單」，既然我合法的發件服務器只有一臺，那麼 DNS 填寫如下設置即可。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;&#34;v=spf1 mx ~all&#34;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;再就是 DMARC，可以使用前面提到的模板生成，這裡不再贅述。&lt;/p&gt;&#xA;&lt;p&gt;如果你的域名服務商擁有自己的設置嚮導（例如 Cloudflare），亦可以使用它們簡化設置流程。&lt;/p&gt;&#xA;&lt;h2 id=&#34;客戶端設置&#34;&gt;客戶端設置&#xA;&lt;/h2&gt;&lt;p&gt;客戶端設置相對就很簡單了，IMAP 和 SMTP 服務器都是&lt;code&gt;mail.yourdomain.ltd&lt;/code&gt;，開啟 SSL 後端口分別是 465 和 993，登錄即可。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2024/09/docker-mailserver-deployment-recap/image-2_hu_51756253bf099359.webp&#34; alt=&#34;設置示例&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;h2 id=&#34;善用-mailtester&#34;&gt;善用 MailTester&#xA;&lt;/h2&gt;&lt;p&gt;至此我們已經完成了搭建 docker-mailserver 郵件服務器的整個過程，在開始發送郵件之前，我們可以使用 &lt;a class=&#34;link&#34; href=&#34;https://www.mail-tester.com/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;MailTester&lt;/a&gt; 測試我們的配置是否正確。如果測試結果是 10/10，那麼我們的 docker-mailserver 郵件服務器應該會擁有漂亮的送達率，好好享受吧。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2024/09/docker-mailserver-deployment-recap/image-5_hu_6de856f3e3b52ca0.webp&#34; alt=&#34;完美的評分&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;如果不是 10/10，也不要著急，認真查看扣分的原因，對症下藥，一項一項解決即可。&lt;/p&gt;&#xA;&lt;h2 id=&#34;todo-list&#34;&gt;TODO-List&#xA;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 配套 WebUI&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;參考: &lt;a class=&#34;link&#34; href=&#34;https://docker-mailserver.github.io/docker-mailserver/latest/config/security/ssl/#caddy&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;https://docker-mailserver.github.io/docker-mailserver/latest/config/security/ssl/#caddy&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:2&#34;&gt;&#xA;&lt;p&gt;參考: &lt;a class=&#34;link&#34; href=&#34;https://docker-mailserver.github.io/docker-mailserver/latest/config/best-practices/dkim_dmarc_spf/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;https://docker-mailserver.github.io/docker-mailserver/latest/config/best-practices/dkim_dmarc_spf/&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:2&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:3&#34;&gt;&#xA;&lt;p&gt;更加具體的介紹: &lt;a class=&#34;link&#34; href=&#34;https://www.cloudflare.com/learning/email-security/dmarc-dkim-spf/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;https://www.cloudflare.com/learning/email-security/dmarc-dkim-spf/&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:3&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:4&#34;&gt;&#xA;&lt;p&gt;在&lt;a class=&#34;link&#34; href=&#34;https://docker-mailserver.github.io/docker-mailserver/latest/config/best-practices/dkim_dmarc_spf/#generating-keys&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;官方文檔&lt;/a&gt;中有提及。&amp;#160;&lt;a href=&#34;#fnref:4&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;</description>
        </item><item>
            <title>Firefish 數據庫升級記錄</title>
            <link>https://blog.l3zc.com/zh-hant-tw/2024/08/upgrading-firefish-database-docker/</link>
            <pubDate>Fri, 30 Aug 2024 04:45:12 +0000</pubDate>
            <guid>https://blog.l3zc.com/zh-hant-tw/2024/08/upgrading-firefish-database-docker/</guid>
            <description>&lt;img src=&#34;https://blog.l3zc.com/2024/08/upgrading-firefish-database-docker/cover_hu_c0ecfda1e4670195.webp&#34; alt=&#34;Featured image of post Firefish 數據庫升級記錄&#34; /&gt;&lt;p&gt;&lt;a class=&#34;link&#34; href=&#34;https://social.l3zc.com&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;我的 Firefish 實例&lt;/a&gt;搭建於去年的 11 月，使用的數據庫版本自然不是最新的，確切的說，是 PGroonga 12，一個基於 5 年前的 Postgres 的遠古版本。為了避免今後的兼容性問題和安全問題，同時為了更好的性能，今天這數據庫是非升級不可。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2024/08/upgrading-firefish-database-docker/image_hu_49fb33e91bcaa665.webp&#34; alt=&#34;PostgreSQL 12 是在 2019 年發佈的&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;升級一個用 Docker 跑起來的數據庫和升級其他的 Docker 容器有很大不同，並不是拉取一個新的鏡像就可以宣告結束。因為 Postgres 幾乎每個大版本都有 Breaking Changes，新老版本產生的持久化文件並不兼容，如果直接拉取一個新的鏡像運行，數據庫是無法啟動的，必須要採用一定的手段進行數據的遷移。&lt;/p&gt;&#xA;&lt;p&gt;Postgres 有官方的升級工具&lt;code&gt;pg_upgrade&lt;/code&gt;&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;，這一次我不採用，因為我的數據庫跑了一年多，而且用的還是 5 年前的老版本，用這個工具我不確定會不會出問題。這次遷移，大致思路是先導出 SQL，再將導出的 SQL 導入新版本。&lt;/p&gt;&#xA;&lt;h2 id=&#34;升級前的部署概況&#34;&gt;升級前的部署概況&#xA;&lt;/h2&gt;&lt;p&gt;以下是一些升級前需要了解的配置信息：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;項目&lt;/th&gt;&#xA;          &lt;th&gt;配置&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;實例部署方式&lt;/td&gt;&#xA;          &lt;td&gt;Docker Compose&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;數據庫容器名稱&lt;/td&gt;&#xA;          &lt;td&gt;firefish_db&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;升級前的數據庫容器鏡像&lt;/td&gt;&#xA;          &lt;td&gt;groonga/pgroonga:3.1.9-alpine-12-slim&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;數據庫用戶&lt;/td&gt;&#xA;          &lt;td&gt;example-firefish-user&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;數據庫名稱&lt;/td&gt;&#xA;          &lt;td&gt;firefish&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;Docker Compose 的數據庫部分如下：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;db:&#xA;  restart: unless-stopped&#xA;  image: groonga/pgroonga:3.1.9-alpine-12-slim&#xA;  container_name: firefish_db&#xA;  networks:&#xA;    - calcnet&#xA;  env_file:&#xA;    - .config/docker.env&#xA;  volumes:&#xA;    - ./db:/var/lib/postgresql/data&#xA;  healthcheck:&#xA;    test: pg_isready --user=&#34;$${POSTGRES_USER}&#34; --dbname=&#34;$${POSTGRES_DB}&#34;&#xA;    interval: 5s&#xA;    timeout: 5s&#xA;    retries: 5&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;導出當前數據庫&#34;&gt;導出當前數據庫&#xA;&lt;/h2&gt;&lt;p&gt;對數據庫進行升級之前，首先下線 Firefish 的編排：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;docker compose down&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;單獨啟動數據庫容器，導出當前數據庫為 SQL：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;docker compose up -d db&#xA;docker exec -it firefish_db pg_dumpall -U example-firefish-user &gt; backup.sql&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;由於 Firefish 的數據庫比較大，導出過程可能需要很長一段時間，比如我的個人實例就用了十幾分鍾。&lt;/p&gt;&#xA;&lt;h2 id=&#34;導出文件的處理&#34;&gt;導出文件的處理&#xA;&lt;/h2&gt;&lt;p&gt;可能是由於新版的 Postgres 認證方式有所變更，如果直接向新數據庫導入之前導出的&lt;code&gt;backup.sql&lt;/code&gt;文件，就會改變新數據庫的 Authentication Scheme，導致之後 Firefish 連接數據庫時認證失敗。要避免這個問題需要對目前的&lt;code&gt;backup.sql&lt;/code&gt;進行一些處理，只抽取出其中的&lt;code&gt;firefish&lt;/code&gt;數據庫的部分，而不是直接導入所有數據。&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a href=&#34;#fn:2&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;#!/bin/bash&#xA;[ $# -lt 2 ] &amp;&amp; { echo &#34;Usage: $0 &lt;postgresql dump&gt; &lt;dbname&gt;&#34;; exit 1; }&#xA;sed  &#34;/connect.*$2/,\$!d&#34; $1 | sed &#34;/PostgreSQL database dump complete/,\$d&#34;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;編輯一個 Shell 腳本為以上內容，保存至&lt;code&gt;script.sh&lt;/code&gt;，用以處理目前的&lt;code&gt;backup.sql&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;nvim script.sh  # 或者任何你喜歡的編輯器&#xA;chmod +x script.sh&#xA;./script.sh backup.sql firefish &gt;&gt; upgrade.sql&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果一切順利，當前目錄下就會出現一個名為&lt;code&gt;upgrade.sql&lt;/code&gt;的文件，可以打開它看一下，確保其被正確導出。&lt;/p&gt;&#xA;&lt;h2 id=&#34;向新數據庫導入現有數據&#34;&gt;向新數據庫導入現有數據&#xA;&lt;/h2&gt;&lt;p&gt;修改&lt;code&gt;docker-compose.yml&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;db:&#xA;  restart: unless-stopped&#xA;  image: groonga/pgroonga:3.2.2-alpine-16-slim # 最新的容器&#xA;  container_name: firefish_db&#xA;  networks:&#xA;    - calcnet&#xA;  env_file:&#xA;    - .config/docker.env&#xA;  volumes:&#xA;    - ./database:/var/lib/postgresql/data # 注意這一行的變動&#xA;  healthcheck:&#xA;    test: pg_isready --user=&#34;$${POSTGRES_USER}&#34; --dbname=&#34;$${POSTGRES_DB}&#34;&#xA;    interval: 5s&#xA;    timeout: 5s&#xA;    retries: 5&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;注意數據庫目錄映射配置由&lt;code&gt;./db:/var/lib/postgresql/data&lt;/code&gt;變為&lt;code&gt;./database:/var/lib/postgresql/data&lt;/code&gt;，這麼做是為了給新的數據庫一個 Fresh Start，同時也保存老的持久化數據，即使出現問題，也可以隨時用回老的數據庫。&lt;/p&gt;&#xA;&lt;p&gt;拉取並啟動新容器：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;docker compose pull&#xA;docker compose up db -d&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;向新數據庫導入&lt;code&gt;upgrade.sql&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;cat upgrade.sql | docker exec -i firefish_db psql -U example-firefish-user -d firefish&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;取決於數據庫的大小，導入過程也會持續較長的時間。&lt;/p&gt;&#xA;&lt;h2 id=&#34;收尾工作&#34;&gt;收尾工作&#xA;&lt;/h2&gt;&lt;p&gt;導入完成，啟動整個編排：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;docker compose stop db&#xA;docker compose up&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;導入後的首次啟動不建議帶&lt;code&gt;-d&lt;/code&gt;參數，建議不帶參數啟動，確保啟動過程沒有問題後再以&lt;code&gt;-d&lt;/code&gt;參數啟動。&lt;/p&gt;&#xA;&lt;p&gt;最後，登入剛剛啟動的 Firefish 實例，檢查是否有任何數據損失，沒有問題的話就大功告成啦🎉&lt;/p&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;官方文檔：https://www.postgresql.org/docs/current/pgupgrade.html&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:2&#34;&gt;&#xA;&lt;p&gt;參考：https://thomasbandt.com/postgres-docker-major-version-upgrade&amp;#160;&lt;a href=&#34;#fnref:2&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;</description>
        </item><item>
            <title>科學上網：用 Caddy 反向代理 VLESS</title>
            <link>https://blog.l3zc.com/zh-hant-tw/2024/08/caddy-vless-proxy/</link>
            <pubDate>Tue, 13 Aug 2024 08:24:26 +0000</pubDate>
            <guid>https://blog.l3zc.com/zh-hant-tw/2024/08/caddy-vless-proxy/</guid>
            <description>&lt;img src=&#34;https://blog.l3zc.com/2024/08/caddy-vless-proxy/cover_hu_1cd20ddda32e23a.webp&#34; alt=&#34;Featured image of post 科學上網：用 Caddy 反向代理 VLESS&#34; /&gt;&lt;p&gt;&lt;a class=&#34;link&#34; href=&#34;https://blog.l3zc.com/2024/08/aria2-downloading-server/&#34; &gt;上一篇文章&lt;/a&gt;提到，我搭建了一個基於 Aria2 的 BT 離線下載服務器。為了用 Caddy 反向代理部署好的容器，我把原來偷懶用一鍵腳本部署的 Trojan 給卸載了，問就是因為這腳本把 80 和 443 端口都佔用了，不卸載掉根本沒法繼續。&lt;/p&gt;&#xA;&lt;p&gt;於是離線下載服務器是搭建好了，可時不時需要的代理卻不得不下線。仔細一想，之前看到過有人 Cloudflare + V2Ray + WS 復活被牆的 VPS，既然是 WebSocket，還能用 Cloudflare 反代，那用 Caddy 反代又有何不可呢？況且 Caddy 還有自動 TLS，比起 Nginx + Certbot 的組合配置起來肯定更簡單。只要配置成功，我就可以在為 Caddy 留住 443 端口的同時跑代理了。&lt;/p&gt;&#xA;&lt;h2 id=&#34;安裝-vless-和-caddy&#34;&gt;安裝 VLESS 和 Caddy&#xA;&lt;/h2&gt;&lt;p&gt;安裝VLESS 可以直接使用 &lt;a class=&#34;link&#34; href=&#34;https://github.com/v2fly/fhs-install-v2ray&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;v2fly/fhs-install-v2ray&lt;/a&gt; 一鍵腳本：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;# 安裝執行檔和 .dat 資料檔&#xA;bash &lt;(curl -L https://raw.githubusercontent.com/v2fly/fhs-install-v2ray/master/install-release.sh)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Caddy 的安裝請參考&lt;a class=&#34;link&#34; href=&#34;https://caddyserver.com/docs/install&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;官方教程&lt;/a&gt;，例如，我使用 Debian 12，則使用以下命令：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl&#xA;curl -1sLf &#39;https://dl.cloudsmith.io/public/caddy/stable/gpg.key&#39; | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg&#xA;curl -1sLf &#39;https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt&#39; | sudo tee /etc/apt/sources.list.d/caddy-stable.list&#xA;sudo apt update&#xA;sudo apt install caddy&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;配置-vless&#34;&gt;配置 VLESS&#xA;&lt;/h2&gt;&lt;p&gt;編輯&lt;code&gt;/usr/local/etc/v2ray/config.json&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{&#xA;  &#34;log&#34;: {&#xA;    &#34;access&#34;: &#34;/var/log/v2ray/access.log&#34;,&#xA;    &#34;error&#34;: &#34;/var/log/v2ray/error.log&#34;,&#xA;    &#34;loglevel&#34;: &#34;warning&#34;&#xA;  },&#xA;  &#34;inbounds&#34;: [&#xA;    {&#xA;      &#34;port&#34;: 1234, //任意端口&#xA;      &#34;listen&#34;: &#34;127.0.0.1&#34;,&#xA;      &#34;protocol&#34;: &#34;vless&#34;,&#xA;      &#34;settings&#34;: {&#xA;        &#34;clients&#34;: [&#xA;          {&#xA;            &#34;id&#34;: &#34;super-random-uuid&#34;, //隨機生成一個 UUID 替換即可&#xA;            &#34;level&#34;: 0,&#xA;            &#34;email&#34;: &#34;example@example.com&#34;&#xA;          }&#xA;        ],&#xA;        &#34;decryption&#34;: &#34;none&#34;&#xA;      },&#xA;      &#34;streamSettings&#34;: {&#xA;        &#34;network&#34;: &#34;ws&#34;,&#xA;        &#34;security&#34;: &#34;none&#34;,&#xA;        &#34;wsSettings&#34;: {&#xA;          &#34;path&#34;: &#34;/&#34; // 需要與之後的 Caddy 配置保持相同&#xA;        }&#xA;      }&#xA;    }&#xA;  ],&#xA;  &#34;outbounds&#34;: [&#xA;    {&#xA;      &#34;protocol&#34;: &#34;freedom&#34;&#xA;    }&#xA;  ]&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;編輯完成後保存退出即可。&lt;/p&gt;&#xA;&lt;h2 id=&#34;配置-caddy&#34;&gt;配置 Caddy&#xA;&lt;/h2&gt;&lt;p&gt;在你喜歡的位置創建一個 Caddyfile，或者編輯已有的 Caddyfile，加入如下內容，反向代理剛才在 VLESS 配置文件中設置的端口：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-caddyfile&#34;&gt;yourdomain.com {&#xA;        # 用一個 Matcher 匹配所有指定路徑的 Websocket 請求&#xA;        @websockets {&#xA;                path /      # 與前面 VLESS 的 path 保持一致&#xA;                header Connection Upgrade&#xA;                header Upgrade websocket&#xA;        }&#xA;        reverse_proxy @websockets :1234&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;至此，服務端所有配置文件都編輯完畢。&lt;/p&gt;&#xA;&lt;h2 id=&#34;啟動服務&#34;&gt;啟動服務&#xA;&lt;/h2&gt;&lt;p&gt;啟動 VLESS，並設置開機自啟動：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;systemtcl enable v2ray &amp;&amp; systemctl start v2ray&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;啟動 Caddy/更新 Caddy 配置：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;caddy start&#xA;caddy reload  # 更新配置用&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;以上操作全部完成後，Caddy 反向代理 VLESS 就完成了。&lt;/p&gt;&#xA;&lt;h2 id=&#34;客戶端設置&#34;&gt;客戶端設置&#xA;&lt;/h2&gt;&lt;p&gt;客戶端方面需要手動設置：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;地址是綁定給 VLESS 的域名。&lt;/li&gt;&#xA;&lt;li&gt;端口是 443&lt;/li&gt;&#xA;&lt;li&gt;TLS 打開&lt;/li&gt;&#xA;&lt;li&gt;傳輸方式選擇 websocket&lt;/li&gt;&#xA;&lt;li&gt;UUID 是之前填入 VLESS 配置文件的 UUID&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2024/08/caddy-vless-proxy/IMG_0003_hu_844bce19772c475.webp&#34; alt=&#34;Shadowrocket 設置&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;所有的客戶端設置大同小異，至於 Clash 的配置文件，可以參考 &lt;a class=&#34;link&#34; href=&#34;https://wiki.metacubex.one/config/proxies/vless/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;Mihomo 文檔&lt;/a&gt;&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;，以下是我的示例配置文件：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;proxies:&#xA;  - name: &#34;A Random Name&#34;&#xA;    type: vless&#xA;    server: yourdomain.com&#xA;    port: 443&#xA;    udp: true&#xA;    uuid: super-random-uuid&#xA;    flow: xtls-rprx-vision&#xA;    packet-encoding: xudp&#xA;&#xA;    tls: true&#xA;    servername: yourdomain.com&#xA;    alpn:&#xA;      - h2&#xA;      - http/1.1&#xA;    skip-cert-verify: false&#xA;&#xA;    network: ws&#xA;&#xA;    smux:&#xA;      enabled: false&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;需要生成 Clash 配置文件可以使用 &lt;a class=&#34;link&#34; href=&#34;https://v2rayse.com/clash-template/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;V2RaySE 提供的工具&lt;/a&gt;。&lt;/p&gt;&#xA;&lt;h2 id=&#34;完成&#34;&gt;完成&#xA;&lt;/h2&gt;&lt;p&gt;好了，現在我們已經擁有了一個 VLESS 服務器，Caddy 會自動為我們申請和維護 TLS 證書，非常好用。&lt;/p&gt;&#xA;&lt;p&gt;只需要這麼小小折騰一下，我彷彿又找到了當年用一鍵腳本搭建 SSR 時的愉悅，好好享受這種能順利打開 google.com 時的興奮吧。&lt;/p&gt;&#xA;&lt;h2 id=&#34;todo-list&#34;&gt;TODO List&#xA;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;input checked=&#34;&#34; disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 基本搭建&lt;/li&gt;&#xA;&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 偽裝&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;畢竟繼承 Clash Meta 衣缽的 Mihomo 也算是 Clash 正統了。&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;</description>
        </item><item>
            <title>搭建一個 BT 離線下載服務器</title>
            <link>https://blog.l3zc.com/zh-hant-tw/2024/08/aria2-downloading-server/</link>
            <pubDate>Sat, 10 Aug 2024 07:46:40 +0000</pubDate>
            <guid>https://blog.l3zc.com/zh-hant-tw/2024/08/aria2-downloading-server/</guid>
            <description>&lt;img src=&#34;https://blog.l3zc.com/2024/08/aria2-downloading-server/cover_hu_935547325c8335d7.webp&#34; alt=&#34;Featured image of post 搭建一個 BT 離線下載服務器&#34; /&gt;&lt;p&gt;自從買了一些廉價 VPS 以後，手上就閒置了一臺 Azure 的學生機，除了用來跑個 Trojan 暫時沒有其他的用途。正好最近想要下一些 BT 資源，而國內的 BT 環境懂的都懂，遂決定用這臺接近閒置的機器離線下載 BT。&lt;/p&gt;&#xA;&lt;p&gt;首先聲明，所有 BT 下載行為需要自行承擔被版權投訴的風險。&lt;/p&gt;&#xA;&lt;h2 id=&#34;安裝-aria2&#34;&gt;安裝 Aria2&#xA;&lt;/h2&gt;&lt;p&gt;對於這些不是「基礎設施」的軟件，我更喜歡直接拉取一個 Docker 容器的方案。我採用了&lt;a class=&#34;link&#34; href=&#34;https://github.com/P3TERX/Aria2-Pro-Docker&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;P3TERX/Aria2-Pro-Docker&lt;/a&gt;&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;，這套已經修改好的 Docker 容器。Docker 容器部署起來如出一轍：首先編輯&lt;code&gt;docker-compose.yml&lt;/code&gt;，&lt;code&gt;docker compose up&lt;/code&gt;把所有容器都跑起來，最後反代暴露的端口即可。&lt;/p&gt;&#xA;&lt;p&gt;我的&lt;code&gt;docker-compose.yml&lt;/code&gt;如下&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a href=&#34;#fn:2&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;services:&#xA;  Aria2-Pro:&#xA;    container_name: aria2-pro&#xA;    image: p3terx/aria2-pro&#xA;    environment:&#xA;      - PUID=65534&#xA;      - PGID=65534&#xA;      - UMASK_SET=022&#xA;      - RPC_SECRET=replace-me # 記得修改這一行&#xA;      - RPC_PORT=6800&#xA;      - LISTEN_PORT=6888&#xA;      - DISK_CACHE=64M&#xA;      - IPV6_MODE=false&#xA;      - UPDATE_TRACKERS=true&#xA;      - CUSTOM_TRACKER_URL=&#xA;      - TZ=Asia/Shanghai&#xA;    volumes:&#xA;      - ./aria2-config:/config&#xA;      - ./aria2-downloads:/downloads&#xA;    network_mode: bridge # 如果你需要用到 IPV6 網絡，也可以使用 host 模式&#xA;    ports:&#xA;      - 6800:6800&#xA;      - 6888:6888&#xA;      - 6888:6888/udp&#xA;    restart: unless-stopped&#xA;    # 防止日誌塞滿硬盤&#xA;    logging:&#xA;      driver: json-file&#xA;      options:&#xA;        max-size: 1m&#xA;&#xA;  # 也可以使用其他面板&#xA;  AriaNg:&#xA;    container_name: ariang&#xA;    image: p3terx/ariang&#xA;    command: --port 6880 --ipv6&#xA;    network_mode: bridge&#xA;    ports:&#xA;      - 6880:6880&#xA;    restart: unless-stopped&#xA;    logging:&#xA;      driver: json-file&#xA;      options:&#xA;        max-size: 1m&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;用-caddy-反向代理&#34;&gt;用 Caddy 反向代理&#xA;&lt;/h2&gt;&lt;p&gt;之前的反向代理，我都是用的基於 Nginx 的方案，而在這段時間裡關於 Caddy 的好評不絕於耳，這次的我便換用了 Caddy，當然，主要也是想嘗試一些新東西、新技術。&lt;/p&gt;&#xA;&lt;p&gt;Caddy 的使用真的非常簡單，即使對新人也極其友好，例如，常用的反向代理本機某個端口，只需三行 Caddyfile 即可搞定。&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-caddyfile&#34;&gt;domain.com {&#xA;  reverse_proxy :1234&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;安裝 Caddy 請參考官方文檔，例如，我的服務器使用 Debian，則使用以下命令安裝：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl&#xA;curl -1sLf &#39;https://dl.cloudsmith.io/public/caddy/stable/gpg.key&#39; | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg&#xA;curl -1sLf &#39;https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt&#39; | sudo tee /etc/apt/sources.list.d/caddy-stable.list&#xA;sudo apt update&#xA;sudo apt install caddy&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;安裝完成後記得開放防火牆和從服務商策略組的 80 和 443 端口。&lt;/p&gt;&#xA;&lt;p&gt;在域名服務商處將要綁定的域名指向服務器，就可以開始寫 Caddyfile 了。我們需要反向代理三個端口，分別用作不同的用途。我的 Caddyfile 如下：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-caddyfile&#34;&gt;# Aria-NG 面板&#xA;download.l3zc.com {&#xA;  reverse_proxy :6880&#xA;}&#xA;&#xA;# Aria2 API&#xA;arapi.l3zc.com {&#xA;  reverse_proxy :6800&#xA;}&#xA;&#xA;# Alist&#xA;downloaded.l3zc.com {&#xA;  reverse_proxy :5244&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;看到如此簡潔的配置文件，不由得感嘆，相比於 Nginx，Caddy 用起來真是令人心情舒暢。寫入配置完成後，重新啟動 Caddy&lt;sup id=&#34;fnref:3&#34;&gt;&lt;a href=&#34;#fn:3&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;3&lt;/a&gt;&lt;/sup&gt;，就可以開始欣賞 Caddy 自動幫你申請 SSL 證書配置好 HTTPS 的絕贊過程。至此，反向代理的部分也就完成了。&lt;/p&gt;&#xA;&lt;h2 id=&#34;配置-aria-ng&#34;&gt;配置 Aria-NG&#xA;&lt;/h2&gt;&lt;p&gt;Aria-NG 只是一個前端面板，可以和任何 Aria2 後端通信，為此，我們需要給 Aria-NG 提供配置信息。進入剛剛反代好的 Aria-NG 面板，轉到 AriaNG 設置，填入配置信息。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2024/08/aria2-downloading-server/image-2_hu_d9233f39114b3449.webp&#34; alt=&#34;在 AriaNG 面板內填入配置信息&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;其中，RPC 地址是剛剛反代好的 RPC 地址，協議選擇 HTTPS，請求方法選擇 POST，RPC 密鑰是在&lt;code&gt;docker-compose.yml&lt;/code&gt;裡&lt;code&gt;replace-me&lt;/code&gt;處你所替換的值。&lt;/p&gt;&#xA;&lt;p&gt;若「Aria2 狀態」旁出現「已連接」，說明配置正確。&lt;/p&gt;&#xA;&lt;h2 id=&#34;用-alist-映射下載目錄&#34;&gt;用 Alist 映射下載目錄&#xA;&lt;/h2&gt;&lt;p&gt;我們通過 Alist 映射下載目錄，以更方便的查看、管理和下載服務器上已經下載好的文件。Alist 同樣也通過 Docker 部署，由於 Alist 本身在容器內運行們，無法訪問容器外的文件，部署時需要將 Aria2 的下載目錄提前 Bind 到 Alist 的容器下。&lt;/p&gt;&#xA;&lt;p&gt;我的&lt;code&gt;docker-compose.yml&lt;/code&gt;如下：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;services:&#xA;  alist:&#xA;    image: &#34;xhofe/alist:latest&#34;&#xA;    container_name: alist&#xA;    volumes:&#xA;      - &#34;/etc/alist:/opt/alist/data&#34;&#xA;      - &#34;/home/azureuser/aria2/aria2-downloads:/aria2-downloads&#34; #這裡需要視情況綁定你自己的目錄&#xA;    ports:&#xA;      - &#34;5244:5244&#34;&#xA;    environment:&#xA;      - PUID=0&#xA;      - PGID=0&#xA;      - UMASK=022&#xA;    restart: unless-stopped&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;第一次啟動 Alist 時，Alist 會隨機生成&lt;code&gt;admin&lt;/code&gt;用戶的密碼，所以，第一次啟動這套編排時，最好不要帶&lt;code&gt;-d&lt;/code&gt;參數，而是直接&lt;code&gt;docker compose up&lt;/code&gt;，並把容器啟動後的輸出妥善保存。如果不小心沒有保存管理員密碼，可以用這個命令重新隨機生成一個：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;docker exec -it alist ./alist admin random&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;成功登錄 Alist 之後就可以開始掛載目錄了。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2024/08/aria2-downloading-server/image_hu_fb55cbee3d7dca68.webp&#34; alt=&#34;掛載 Aria2 的下載目錄&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;驅動選擇本機存儲，掛載路徑需要注意，按照我的編排，我將容器外的&lt;code&gt;/home/azureuser/aria2/aria2-downloads&lt;/code&gt;目錄掛載到了容器內的&lt;code&gt;/aria2-downloads&lt;/code&gt;，所以掛載路徑應該是&lt;code&gt;/aria2-downloads&lt;/code&gt;。&lt;/p&gt;&#xA;&lt;h2 id=&#34;完成&#34;&gt;完成&#xA;&lt;/h2&gt;&lt;p&gt;現在所有的服務都已經部署上線了，打開 AriaNG 可以添加下載任務，下載好的文件可以打開 Alist 看到，下載，以及在線觀看。&lt;/p&gt;&#xA;&lt;p&gt;搭建這套服務之前，請仔細查看 VPS 提供商的服務條款，確保 BT 下載行為不會違規。以及，除了盧森堡等少數國家/地區，下載被 DMCA 保護的資源可能會遭到投訴，這些風險都需要你自行承擔。&lt;/p&gt;&#xA;&lt;p&gt;反正我這 Azure 的學生機也是白嫖的，被投訴回收了我也無所謂，不管怎樣，現在可以心無旁騖的享受找資源的快樂了。&lt;/p&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;當然也可以試一試&lt;a class=&#34;link&#34; href=&#34;https://github.com/SuperNG6/docker-aria2&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;SuperNG6/docker-aria2&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:2&#34;&gt;&#xA;&lt;p&gt;參考原作者的&lt;a class=&#34;link&#34; href=&#34;https://github.com/P3TERX/Aria2-Pro-Docker/blob/master/docker-compose.yml&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;&lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:2&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:3&#34;&gt;&#xA;&lt;p&gt;&lt;code&gt;caddy stop&lt;/code&gt;和&lt;code&gt;caddy start&lt;/code&gt;，參考&lt;a class=&#34;link&#34; href=&#34;https://caddyserver.com/docs/getting-started#start-stop-run&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;Caddy文檔&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:3&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;</description>
        </item><item>
            <title>在 GNU/Linux 和 Windows 間共享 Steam 遊戲庫</title>
            <link>https://blog.l3zc.com/zh-hant-tw/2024/04/mount-ntfs-drive-as-steam-lib-on-linux/</link>
            <pubDate>Fri, 12 Apr 2024 10:53:59 +0000</pubDate>
            <guid>https://blog.l3zc.com/zh-hant-tw/2024/04/mount-ntfs-drive-as-steam-lib-on-linux/</guid>
            <description>&lt;img src=&#34;https://blog.l3zc.com/2024/04/mount-ntfs-drive-as-steam-lib-on-linux/cover_hu_f197ca1e93d3a83b.webp&#34; alt=&#34;Featured image of post 在 GNU/Linux 和 Windows 間共享 Steam 遊戲庫&#34; /&gt;&lt;p&gt;得益於與 Steam Deck 一同而來的 Proton，在常見的 GNU/Linux（以下簡稱 Linux）發行版上暢玩各類原本在 Windows 上才有的 3A 大作成為完全可行的選擇，最近我也進行了這方面的嘗試，效果已經較為理想，但隨之而來的是一個問題，我總不可能把動輒上百G的遊戲全都重新裝一遍吧？硬盤沒空間了不說，這麼麻煩還不如切回 Windows 玩呢。&lt;/p&gt;&#xA;&lt;p&gt;出現&lt;code&gt;$&lt;/code&gt;代表是終端命令，下面跟著的是命令執行後的輸出。&lt;/p&gt;&#xA;&lt;h2 id=&#34;掛載-steam-庫所在的分區&#34;&gt;掛載 Steam 庫所在的分區&#xA;&lt;/h2&gt;&lt;p&gt;起初我試圖直接在 GUI 上掛載分區，但這樣會遇到莫名其妙的問題，有這個想法的朋友請不要跳過這一部分。&lt;/p&gt;&#xA;&lt;h3 id=&#34;找到目標分區的-uuid&#34;&gt;找到目標分區的 UUID&#xA;&lt;/h3&gt;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ sudo blkid&#xA;/dev/nvme1n1p8: UUID=&#34;421a3d00-81a8-4112-a2df-4d234c674a24&#34; BLOCK_SIZE=&#34;4096&#34; TYPE=&#34;ext4&#34; PARTUUID=&#34;84e56276-a585-412c-9bf5-0071b2b4d4da&#34;&#xA;/dev/loop1: TYPE=&#34;squashfs&#34;&#xA;/dev/nvme0n1p1: PARTLABEL=&#34;Microsoft reserved partition&#34; PARTUUID=&#34;bbdb2720-6722-42b3-80f5-385332da77c3&#34;&#xA;/dev/nvme0n1p2: LABEL=&#34;Vault&#34; BLOCK_SIZE=&#34;512&#34; UUID=&#34;DC64DA2064D9FCE8&#34; TYPE=&#34;ntfs&#34; PARTLABEL=&#34;Basic data partition&#34; PARTUUID=&#34;f884fe24-1c4d-4650-9847-d2f83b9a93ba&#34;&#xA;......&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果你能在類似「磁盤管理」的應用裡查到目標分區的 UUID，也可以直接使用。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2024/04/mount-ntfs-drive-as-steam-lib-on-linux/sc1_hu_44f5b8a6d71a6fdb.webp&#34; alt=&#34;能查到 UUID 就行&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;h3 id=&#34;查詢用戶-id-和組-id&#34;&gt;查詢用戶 ID 和組 ID&#xA;&lt;/h3&gt;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ id -u&#xA;1000&#xA;$ id -g&#xA;1000&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;默認狀態下，這兩個值都是&lt;code&gt;1000&lt;/code&gt;。&lt;/p&gt;&#xA;&lt;h3 id=&#34;創建掛載點並修改-fstab&#34;&gt;創建掛載點並修改 &lt;code&gt;fstab&lt;/code&gt;&#xA;&lt;/h3&gt;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ sudo mkdir /media/gamedisk&#xA;$ sudo nano /etc/fstab&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;這裡也可以使用任何你喜歡的編輯器。&lt;/p&gt;&#xA;&lt;p&gt;在文件的末尾加上：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;UUID=DC64DA2064D9FCE8 /media/gamedisk lowntfs-3g uid=1000,gid=1000,rw,user,exec,umask=000 0 0&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果你之前已經掛載過這個硬盤，請刪除之前掛載產生的那一行。&lt;/p&gt;&#xA;&lt;p&gt;大小寫敏感可能會導致問題，這裡使用的是&lt;code&gt;lowntfs-3g&lt;/code&gt;，掛載後的 NTFS 卷將不會大小寫敏感，以免出現因大小寫敏感而找不到遊戲文件的問題。&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;防止出現-ntfs-read-error&#34;&gt;防止出現 NTFS Read Error&#xA;&lt;/h2&gt;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;⚠️注意：有報告稱這樣操作可能會造成數據丟失，請自行斟酌並承擔風險。&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;p&gt;由於 NTFS 的特性，如果創建在 Windows 上無效名稱的文件/文件夾將導致 Read Error，進而導致遊戲無法啟動。最常見的問題是文件名中包含&lt;code&gt;:&lt;/code&gt;字符，而 Proton 在 NTFS 磁盤上創建的文件名中就包含了這個字符。&lt;/p&gt;&#xA;&lt;p&gt;修復這個問題相當簡單：在掛載的 NTFS 磁盤上創建一個&lt;code&gt;/compatdata&lt;/code&gt;文件夾，並將其 symlink 到 Linux 分區上的文件夾。&lt;/p&gt;&#xA;&lt;p&gt;創建 symlink：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ mkdir -p ~/.steam/steam/steamapps/compatdata&#xA;$ ln -s ~/.steam/steam/steamapps/compatdata /media/gamedisk/Steam/steamapps/&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果提示&lt;code&gt;/compatdata&lt;/code&gt;已經存在，刪除它並重新 symlink 即可。&lt;/p&gt;&#xA;&lt;h2 id=&#34;最後添加-steam-庫吧&#34;&gt;最後，添加 Steam 庫吧&#xA;&lt;/h2&gt;&lt;p&gt;操作完成後，重啟電腦，目標磁盤就會在 Linux 上自動掛載，這時進入 Steam 正常添加遊戲庫即可&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2024/04/mount-ntfs-drive-as-steam-lib-on-linux/sc2_hu_7681a8427115b857.webp&#34; alt=&#34;正常添加遊戲庫&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;操作完成後，可以正常遊玩 Steam 庫裡所有的 Windows 遊戲，並且可以在任何一邊向庫內添加遊戲。&lt;/p&gt;&#xA;&lt;p&gt;本文參考：&lt;a class=&#34;link&#34; href=&#34;https://github.com/ValveSoftware/Proton/wiki/Using-a-NTFS-disk-with-Linux-and-Windows&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;Using a NTFS disk with Linux and Windows&lt;/a&gt;&lt;/p&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;來源：&lt;a class=&#34;link&#34; href=&#34;https://serverfault.com/questions/901855/ntfs-3g-ignore-case&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;Server Fault&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;</description>
        </item><item>
            <title>最速 Firefish 部署指南</title>
            <link>https://blog.l3zc.com/zh-hant-tw/2023/12/firefish-deploy/</link>
            <pubDate>Mon, 04 Dec 2023 09:17:56 +0000</pubDate>
            <guid>https://blog.l3zc.com/zh-hant-tw/2023/12/firefish-deploy/</guid>
            <description>&lt;img src=&#34;https://blog-img.l3zc.com/firefishlogo.webp&#34; alt=&#34;Featured image of post 最速 Firefish 部署指南&#34; /&gt;&lt;h2 id=&#34;需要的環境&#34;&gt;需要的環境&#xA;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;Docker 和 Docker Compose&lt;/li&gt;&#xA;&lt;li&gt;任意 Web 服務器&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;最速教程&#34;&gt;最速教程&#xA;&lt;/h2&gt;&lt;p&gt;為 Firefish 準備一個目錄：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;mkdir firefish &amp;&amp; cd firefish&lt;/code&gt;&lt;/pre&gt;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;⚠️注意：近期 Firefish 更新頻繁，本文的&lt;code&gt;docker-compose.yml&lt;/code&gt;已經過時，請參考官方倉庫中的&lt;a class=&#34;link&#34; href=&#34;https://firefish.dev/firefish/firefish/-/blob/develop/docker-compose.example.yml&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;docker-compose.example.yml&lt;/a&gt;和&lt;a class=&#34;link&#34; href=&#34;https://firefish.dev/firefish/firefish/-/blob/develop/docs/notice-for-admins.md&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;Notice for server administrators&lt;/a&gt;進行配置。&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;p&gt;用你喜歡的文本編輯器創建&lt;code&gt;docker-compose.yml&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;version: &#34;3&#34;&#xA;&#xA;services:&#xA;  web:&#xA;    image: registry.joinfirefish.org/firefish/firefish:stable-amd64&#xA;    container_name: firefish_web&#xA;    restart: unless-stopped&#xA;    depends_on:&#xA;      - db&#xA;      - redis&#xA;      ### Uncomment one of the following to use a search engine&#xA;      #     - meilisearch&#xA;      - sonic&#xA;    ports:&#xA;      - &#34;6666:3000&#34; # 6666換成需要的端口&#xA;    networks:&#xA;      - calcnet&#xA;    #     - web&#xA;    environment:&#xA;      NODE_ENV: production&#xA;    volumes:&#xA;      - ./files:/firefish/files&#xA;      - ./.config:/firefish/.config:ro&#xA;&#xA;  redis:&#xA;    restart: unless-stopped&#xA;    image: redis:7.0-alpine&#xA;    container_name: firefish_redis&#xA;    networks:&#xA;      - calcnet&#xA;    volumes:&#xA;      - ./redis:/data&#xA;&#xA;  db:&#xA;    restart: unless-stopped&#xA;    image: postgres:12.2-alpine&#xA;    container_name: firefish_db&#xA;    networks:&#xA;      - calcnet&#xA;    env_file:&#xA;      - .config/docker.env&#xA;    volumes:&#xA;      - ./db:/var/lib/postgresql/data&#xA;&#xA;  ### Only one of the below should be used.&#xA;  ### Meilisearch is better overall, but resource-intensive. Sonic is a very light full text search engine.&#xA;&#xA;  #  meilisearch:&#xA;  #    container_name: meilisearch&#xA;  #    image: getmeili/meilisearch:v1.1.1&#xA;  #    environment:&#xA;  #      - MEILI_ENV=${MEILI_ENV:-development}&#xA;  #    ports:&#xA;  #      - &#34;7700:7700&#34;&#xA;  #    networks:&#xA;  #      - calcnet&#xA;  #    volumes:&#xA;  #      - ./meili_data:/meili_data&#xA;  #    restart: unless-stopped&#xA;&#xA;  sonic:&#xA;    restart: unless-stopped&#xA;    image: valeriansaliou/sonic:v1.4.0&#xA;    logging:&#xA;      driver: none&#xA;    networks:&#xA;      - calcnet&#xA;    volumes:&#xA;      - ./sonic:/var/lib/sonic/store&#xA;      - ./sonic/config.cfg:/etc/sonic.cfg:ro&#xA;&#xA;networks:&#xA;  calcnet:&#xA;  #  web:&#xA;  #    external:&#xA;  #      name: web&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;你也可以用 Melisearch，但這是最速指南，只講 Sonic。&lt;/p&gt;&#xA;&lt;p&gt;創建&lt;code&gt;.config&lt;/code&gt;目錄：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;mkdir .config &amp;&amp; cd .config&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;創建&lt;code&gt;docker.env&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;# db settings&#xA;POSTGRES_PASSWORD=example-firefish-pass&#xA;POSTGRES_USER=example-firefish-user&#xA;POSTGRES_DB=firefish&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;創建&lt;code&gt;default.yml&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━&#xA;# Firefish configuration&#xA;#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━&#xA;&#xA;# After starting your server, please don&#39;t change the URL! Doing so will break federation.&#xA;&#xA;#   ┌─────┐&#xA;#───┘ URL └─────────────────────────────────────────────────────&#xA;&#xA;# Final accessible URL seen by a user.&#xA;url: https://你的域名/&#xA;port: 3000&#xA;&#xA;db:&#xA;  host: db&#xA;  port: 5432&#xA;  #ssl: false&#xA;  # Database name&#xA;  db: firefish&#xA;&#xA;  # Auth&#xA;  user: example-firefish-user&#xA;  pass: example-firefish-pass&#xA;&#xA;#   ┌─────────────────────┐&#xA;#───┘ Redis configuration └─────────────────────────────────────&#xA;&#xA;redis:&#xA;  host: redis&#xA;  port: 6379&#xA;&#xA;#   ┌─────────────────────┐&#xA;#───┘ Sonic configuration └─────────────────────────────────────&#xA;&#xA;sonic:&#xA;  host: sonic&#xA;  port: 1491&#xA;  auth: SecretPassword&#xA;  collection: notes&#xA;  bucket: default&#xA;&#xA;# Reserved usernames that only the administrator can register with&#xA;reservedUsernames: [&#34;root&#34;, &#34;admin&#34;, &#34;administrator&#34;, &#34;me&#34;, &#34;system&#34;]&#xA;&#xA;signToActivityPubGet: true&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;回到上一層目錄，創建&lt;code&gt;./sonic/config.cfg&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-toml&#34;&gt;# Sonic&#xA;# Fast, lightweight and schema-less search backend&#xA;# Configuration file&#xA;# Example: https://github.com/valeriansaliou/sonic/blob/master/config.cfg&#xA;&#xA;[server]&#xA;&#xA;log_level = &#34;debug&#34;&#xA;&#xA;[channel]&#xA;&#xA;inet = &#34;0.0.0.0:1491&#34;&#xA;tcp_timeout = 300&#xA;&#xA;auth_password = &#34;SecretPassword&#34;&#xA;&#xA;[channel.search]&#xA;&#xA;query_limit_default = 10&#xA;query_limit_maximum = 100&#xA;query_alternates_try = 4&#xA;&#xA;suggest_limit_default = 5&#xA;suggest_limit_maximum = 20&#xA;&#xA;list_limit_default = 100&#xA;list_limit_maximum = 500&#xA;&#xA;[store]&#xA;&#xA;[store.kv]&#xA;&#xA;path = &#34;./data/store/kv/&#34;&#xA;&#xA;retain_word_objects = 1000&#xA;&#xA;[store.kv.pool]&#xA;&#xA;inactive_after = 1800&#xA;&#xA;[store.kv.database]&#xA;&#xA;flush_after = 900&#xA;&#xA;compress = true&#xA;parallelism = 2&#xA;max_files = 100&#xA;max_compactions = 1&#xA;max_flushes = 1&#xA;write_buffer = 16384&#xA;write_ahead_log = true&#xA;&#xA;[store.fst]&#xA;&#xA;path = &#34;./data/store/fst/&#34;&#xA;&#xA;[store.fst.pool]&#xA;&#xA;inactive_after = 300&#xA;&#xA;[store.fst.graph]&#xA;&#xA;consolidate_after = 180&#xA;&#xA;max_size = 2048&#xA;max_words = 250000&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;全部創建好以後的目錄結構應該看起來像這樣：&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;.&#xA;├── .config&#xA;│   ├── default.yml&#xA;│   └── docker.env&#xA;├── docker-compose.yml&#xA;└── sonic&#xA;    └── config.cfg&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;最後執行&lt;code&gt;docker compose up&lt;/code&gt;，綁定域名、反代端口，實例就搭建完成了。&lt;/p&gt;&#xA;&lt;p&gt;Firefish 已經自動 Minify 了所有的前端文件，如果你使用 Cloudflare 或其他類似的服務，記得關掉 Auto Minify，否則會出現一些奇怪的問題。&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&#xA;&lt;p&gt;以上就是最速教程。&lt;/p&gt;&#xA;&lt;h2 id=&#34;一些問題&#34;&gt;一些問題&#xA;&lt;/h2&gt;&lt;p&gt;Q：&lt;code&gt;docker.env&lt;/code&gt;和&lt;code&gt;./sonic/config.cfg&lt;/code&gt;密碼和用戶名不改沒問題嗎？&lt;/p&gt;&#xA;&lt;p&gt;A：沒很大問題，因為兩個服務沒有暴露到公網上，僅做個人使用不改也沒什麼大事。&lt;/p&gt;&#xA;&lt;p&gt;Q：為什麼 network 要叫&lt;code&gt;calcnet&lt;/code&gt;？&lt;/p&gt;&#xA;&lt;p&gt;A：因為這個項目本來叫做 Calckey。&lt;/p&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;&lt;a class=&#34;link&#34; href=&#34;https://github.com/misskey-dev/misskey/issues/10328&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;https://github.com/misskey-dev/misskey/issues/10328&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;</description>
        </item><item>
            <title>受夠了 MacOS，我給 Macbook 裝上了 Linux</title>
            <link>https://blog.l3zc.com/zh-hant-tw/2023/11/installing-ubuntu-on-macbook/</link>
            <pubDate>Tue, 07 Nov 2023 09:38:50 +0000</pubDate>
            <guid>https://blog.l3zc.com/zh-hant-tw/2023/11/installing-ubuntu-on-macbook/</guid>
            <description>&lt;img src=&#34;https://blog.l3zc.com/2023/11/installing-ubuntu-on-macbook/IMG_7558_hu_8b6cb548722da663.webp&#34; alt=&#34;Featured image of post 受夠了 MacOS，我給 Macbook 裝上了 Linux&#34; /&gt;&lt;p&gt;&lt;a class=&#34;link&#34; href=&#34;https://social.l3zc.com/notes/9xinhefrr14n3pty&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;現在已經換成了 Arch&lt;/a&gt;，btw。&lt;/p&gt;&#xA;&lt;p&gt;現役一臺 MacBook (Retina, 12-inch, Early 2016)，官方最高支持到 Monterey，當時收來的時候我認為，我會 Stick with MacOS。&lt;/p&gt;&#xA;&lt;p&gt;這麼說是因為 MacOS 在我賣掉的老 MBP 上給我的印象是穩定好用，當時的想法是出掉 MBP 買入一臺廉價而性能夠用的 Windows 遊戲本和一臺天天帶出去的輕薄本——事實上我也不後悔這個決定。&lt;/p&gt;&#xA;&lt;p&gt;好吧我承認我高估了這顆兩核 m5 的性能，目前的狀況是：如果使用 MacOS，必須要開啟「減弱動畫效果」，否則會卡得生活不能自理。這就是 MacOS 的另一個特點：「易卡難崩」。&lt;/p&gt;&#xA;&lt;p&gt;於是我給我的 MacBook 裝上了 Linux，就有了現在這篇文章，written on Ubuntu。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2023/11/installing-ubuntu-on-macbook/IMG_7558_hu_8b6cb548722da663.webp&#34; alt=&#34;&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2023/11/installing-ubuntu-on-macbook/image-20231103205715707_hu_5da0a101629b8b98.webp&#34; alt=&#34;Typora On Ubuntu&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;h2 id=&#34;why-ubuntu&#34;&gt;Why Ubuntu?&#xA;&lt;/h2&gt;&lt;p&gt;我其實並不是很喜歡 Ubuntu，選用 Ubuntu 更多是一個無奈的權衡。&lt;/p&gt;&#xA;&lt;p&gt;本次安裝 Linux 的目的是獲得更好的性能和更流暢的體驗，讓我的日常工作更加輕鬆。這臺電腦是我的日常裝備之一，我不太希望選擇一個不夠穩定的分支。之前滾壞過一次系統的經歷讓我直接把 Arch 排除在外。我有太多事情要做，時不時滾壞的系統？ Don&amp;rsquo;t have time for that. I just want a system that works out of the box 。&lt;/p&gt;&#xA;&lt;p&gt;本來想裝 Pop!OS——System76 基於 Ubuntu 打造的系統，但安裝器不知為何老是閃退，遂放棄。&lt;/p&gt;&#xA;&lt;p&gt;用&lt;a class=&#34;link&#34; href=&#34;https://distrochooser.de&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;distrochooser.de&lt;/a&gt;選出了 OpenSUSE，並且在虛擬機上裝好 OpenSUSE + Xfce 試用。我非常喜歡這套環境，但 Xfce 似乎不支持 HiDPI，也不優先考慮。YaST 真的很好用，將來我會在別的電腦上用這套系統，但這臺電腦，no。&lt;/p&gt;&#xA;&lt;p&gt;在這臺 MacBook 上，Ubuntu 的驅動問題算是最少的。除了上面提到的兩個分支，我也嘗試了一些別的 Debian 系 Distro 比如 Linux Mint、Deuvan，甚至國產 Deepin，沒有一個擁有良好的硬件兼容性。&lt;/p&gt;&#xA;&lt;p&gt;在作出這些嘗試以後我認為已經沒有必要再挑三揀四，先用 Ubuntu 權衡才是最優解。&lt;/p&gt;&#xA;&lt;h2 id=&#34;softwarehow-ubuntu-fits-into-my-workflow&#34;&gt;Software：How Ubuntu fits into my workflow&#xA;&lt;/h2&gt;&lt;p&gt;我在安裝時就選擇安裝了 ThunderBird、Libre Office 等軟件。在進系統後也很快就用 apt 安裝好了所有我需要的 CLI 工具以及大部分應用。&lt;/p&gt;&#xA;&lt;p&gt;我在 MacOS 下的包管理器是 Homebrew，換到 Ubuntu 就改用了自帶的 apt，也接受了 snap，即使這會影響到我安裝 GTK 主題後的一致性。&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;與 Homebrew 相比，apt 提供更多的軟件包&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a href=&#34;#fn:2&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;，且 Homebrew 終究是作為一個第三方包管理器存在。&lt;/p&gt;&#xA;&lt;p&gt;為了安裝一些別的應用，我也安裝了 flatpak 作為額外的包管理器。雖然比較難用，但也確實擴充了軟件的選擇範圍。&lt;/p&gt;&#xA;&lt;h3 id=&#34;browsing&#34;&gt;Browsing&#xA;&lt;/h3&gt;&lt;p&gt;Chrome 依舊是我的主力瀏覽器，我難以放棄它的同步功能，Which means I have no choice.&lt;/p&gt;&#xA;&lt;p&gt;所有網頁都可以正常工作，就像在 Windows 和 MacOS 上那樣，as expected.&lt;/p&gt;&#xA;&lt;h3 id=&#34;office-files&#34;&gt;Office files&#xA;&lt;/h3&gt;&lt;p&gt;學生要看的 Word、PPT、PDF 等，這些在 Libre Office 上顯示效果超出我的預期——基本都能正確顯示。字體什麼的都是小問題，&lt;code&gt;.doc&lt;/code&gt;、&lt;code&gt;.ppt&lt;/code&gt;文檔的佈局都正常，渲染也沒有錯誤。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2023/11/installing-ubuntu-on-macbook/officefiles_hu_a2835286f0fcb1a0.webp&#34; alt=&#34;即使是公式渲染也基本正確&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2023/11/installing-ubuntu-on-macbook/officefiles2_hu_3d140cb5db55e490.webp&#34; alt=&#34;大部分時候佈局都是正常的&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;我從不在這臺電腦上做編輯 Office 的工作，即使在 MacOS 下也是如此。有這些工作我都交給了我的 Windows PC。&lt;/p&gt;&#xA;&lt;p&gt;即便如此，在 MacOS 上我卻依舊全量安裝了 Office 365。Office 365 在後臺更新頻繁，我打開 Office 時，經常彈出「正在驗證xxx」的提示，每次都要等很久，非常煩人。相比之下，Libre Office 只在我&lt;code&gt;apt upgrade&lt;/code&gt;時才會更新，用起來不知道比 MacOS 舒服多少。我已經在考慮將這臺 MacBook 上的 Office 365 換成 LibreOffice。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2023/11/installing-ubuntu-on-macbook/image-20231109173015015_hu_1066c20e0dfae621.webp&#34; alt=&#34;時不時彈出的「正在驗證」&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;h3 id=&#34;blogging&#34;&gt;Blogging&#xA;&lt;/h3&gt;&lt;p&gt;我以 VSCode 作為代碼兼文本編輯器，用 Typora 來寫 Markdown 文章，用 PicGo 作為圖床上傳工具，用 Git 把寫好的內容和配置推到 Github 上併發布。我還在本地安裝了 Hugo 用以預覽我的修改，這些工具配置好以後使用起來和在別的平臺上無異。&lt;/p&gt;&#xA;&lt;h3 id=&#34;music&#34;&gt;Music&#xA;&lt;/h3&gt;&lt;p&gt;我主要使用 Spotify 和網易雲音樂來串流音樂。其中 Spotify 有 Native 版本，網易雲的 Linux 客戶端早已停止維護，可能網頁版才是更好的選擇。&lt;/p&gt;&#xA;&lt;p&gt;Youtube 其實也是個聽音樂的好地方。&lt;/p&gt;&#xA;&lt;h3 id=&#34;social&#34;&gt;Social&#xA;&lt;/h3&gt;&lt;p&gt;QQ 的全功能 Electron 重構版本 QQNT 原生支持所有主流的分支，還有 Telegram。至於微信，我暫時沒有什麼硬性需求，用手機就可以了，有文件要傳到電腦上可以用 QQ 中轉。&lt;/p&gt;&#xA;&lt;p&gt;主流的 Twitter Like 社交平臺和 Fediverse 實例都有網頁版，部分還有 PWA&lt;sup id=&#34;fnref:3&#34;&gt;&lt;a href=&#34;#fn:3&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;3&lt;/a&gt;&lt;/sup&gt; 可用，這些用 Chrome 就可以解決。&lt;/p&gt;&#xA;&lt;p&gt;ThunderBird 是非常好的郵件客戶端，我在 Windows 上已經在使用，換到 Ubuntu 的預裝更無需重新適應。&lt;/p&gt;&#xA;&lt;h3 id=&#34;password-management--rss&#34;&gt;Password Management &amp;amp; RSS&#xA;&lt;/h3&gt;&lt;p&gt;我的密碼管理器是 BitWarden，一樣原生支持所有的主流分支。&lt;/p&gt;&#xA;&lt;p&gt;RSS 閱讀器有老朋友 Fluent Reader 和 Thunder Bird，Fluent Reader 把 Fluent Design 直接原封不動搬了過來，一樣支持 Fever API 讓我和 FreshRSS 同步。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2023/11/installing-ubuntu-on-macbook/image-20231104215346302_hu_a8b5d47418c27dab.webp&#34; alt=&#34;FreshRSS 在 Ubuntu 上的樣子&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;NewsFlash 也是很好的閱讀器，原生支持 FreshRSS，稍微有點麻煩的就是必須用 flatpak 安裝，同時滾動時我能感覺到明顯的卡頓和發熱。不知道是軟件自身的優化問題還是 flatpak 的問題。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2023/11/installing-ubuntu-on-macbook/image-20231107130507607_hu_65cba9f2338a5965.webp&#34; alt=&#34;NewsFlash 也是很現代化的 RSS 閱讀器&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;h3 id=&#34;proxy&#34;&gt;Proxy&#xA;&lt;/h3&gt;&lt;p&gt;網絡代理我採用 Clash Verge 和 proxychains 的組合，解決了終端和系統代理的問題。除了在打開管理面板的時候 Tauri 很佔資源以外，本身依然是穩定好用。至於 Proxychains 是用來解決部分工具即使設置了環境變量也不遵循的問題而安裝的。我只需要配置好然後&lt;code&gt;proxychains&lt;/code&gt;任何一個命令就可以讓這個命令走代理。&lt;/p&gt;&#xA;&lt;h3 id=&#34;terminal&#34;&gt;Terminal&#xA;&lt;/h3&gt;&lt;p&gt;終端應用直接採用默認的就好，我並不是 Bash 死忠粉，所以後來我也換到了 Zsh，和 MacOS 上一樣安裝了 Oh My Zsh。最近看到了 Terminology，準備一試。&lt;/p&gt;&#xA;&lt;h3 id=&#34;airdrop-alternative&#34;&gt;Airdrop alternative&#xA;&lt;/h3&gt;&lt;p&gt;傳文件的需求我用到了 QQ 和 Snapdrop。&lt;/p&gt;&#xA;&lt;h2 id=&#34;my-experience-with-ubuntu&#34;&gt;My experience with Ubuntu&#xA;&lt;/h2&gt;&lt;p&gt;在瀏覽器越來越強大，網頁版應用和 Electron App 越來越多的今天，Ubuntu 的軟件生態雖然無法和 MacOS 和 Windows 相比，但也足以讓我有底氣 to make the switch。&lt;/p&gt;&#xA;&lt;h3 id=&#34;its-impressive&#34;&gt;It&amp;rsquo;s impressive&#xA;&lt;/h3&gt;&lt;p&gt;Ubuntu 的性能相比於我在 MacOS 簡直就是一個天上一個地下。在安裝時我直接分了 4G Swap，所有的應用都啟動的非常快，與 MacOS 上彈個半天形成的對比非常強烈，甚至讓我懷疑我到底是不是在用同一臺電腦。對於這個性能表現我非常滿意。&lt;/p&gt;&#xA;&lt;p&gt;即使是 CLI 應用，Ubuntu 上跑起來也比 MacOS 上要更快，更利落。以我最常用的 Hugo 來說，&lt;code&gt;hugo server&lt;/code&gt;甚至比我的 Windows 電腦要更快，以至於我甚至願意用它接上電腦來繼續寫文章。&lt;/p&gt;&#xA;&lt;p&gt;續航是我最滿意的部分：得益於更加乾淨的系統和無風扇的設計，目前這臺 MacBook 一次充電可以連續使用一個下午加一個晚上，相當於超過 8 小時的亮屏時間。但需要注意 Tauri 應用的效率問題，體現在實際使用上就是儘可能少用 Clash Verge 的控制面板。只要使用得當，就可以獲得超過 MacOS 的續航體驗。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2023/11/installing-ubuntu-on-macbook/image-20231107114452388_hu_3b3a20f57757769.webp&#34; alt=&#34;Ubuntu的續航讓我非常滿意&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;高分辨率支持還不錯，只要整數倍縮放就不會出現應用模糊的問題，但 200% 的縮放未免太大了一點，也許我該直接調調 DPI？&lt;/p&gt;&#xA;&lt;h3 id=&#34;its-not-perfect&#34;&gt;It&amp;rsquo;s not perfect&#xA;&lt;/h3&gt;&lt;p&gt;藍牙和揚聲器無法被驅動，我嘗試過打內核補丁，但毫無用處，這意味著外放根本不能用，藍牙耳機、鍵盤、鼠標也想都別想。鍵盤背光幾乎沒法用，想要黑燈瞎火的打字幾乎也是不可能的。前置 Facetime 攝像頭也無緣被驅動，雖然我在 MacOS 上也幾乎不用它。&lt;/p&gt;&#xA;&lt;p&gt;觸控板的防誤觸約等於沒有，寫東西時非常容易被觸控板干擾，目前我的解決方案是在寫東西時用&lt;a class=&#34;link&#34; href=&#34;https://github.com/atareao/Touchpad-Indicator&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;TouchPad Indicator&lt;/a&gt;&lt;sup id=&#34;fnref:4&#34;&gt;&lt;a href=&#34;#fn:4&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;4&lt;/a&gt;&lt;/sup&gt;的快捷鍵直接禁用觸控板。但這樣的體驗自然就不如打磨過防誤觸的 MacOS。&lt;/p&gt;&#xA;&lt;p&gt;雙指滾動的速度太快，導致我看網頁時不得不使用方向鍵滾動網頁。為了調整滾動速度，我試著從 Wayland 換到 Xorg，並用 Xinput 調整設置，但暫時還沒有看到成效。&lt;/p&gt;&#xA;&lt;p&gt;各種驅動上的問題以及體驗上的小問題非常影響體驗。我知道，這些問題中的某一些可能只需要花些時間和精力去做些 tweaks 就可以解決。但要在折騰和生產之間取得一個平衡的話，我想我大概會保持現狀。&lt;/p&gt;&#xA;&lt;h2 id=&#34;wrap-up&#34;&gt;Wrap up&#xA;&lt;/h2&gt;&lt;p&gt;我第一次安裝 Linux 時用的是 Ubuntu 16.04 LTS，當時的我苦於 Linux 軟件生態的匱乏而試著去折騰 Wine，許久終於用上了一些必要的 Windows 軟件，最後卻因心累而換回了 Windows。今天，我同樣在安裝 Ubuntu，除了驅動問題外，我幾乎從沒有遇到任何麻煩，並且幾乎全程樂在其中。這次日常使用 Linux 的嘗試無疑是成功的。&lt;/p&gt;&#xA;&lt;p&gt;我可以說 Linux 見證我成長。現在的我已經可以擁抱開源生態系統，很顯然，這次，我不再需要換回去了。&lt;/p&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;Canonical 維護著一份主流 GTK 主題的 Snap 移植，但每次安裝新的 Snap 應用都要手動切換，非常不方便。&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:2&#34;&gt;&#xA;&lt;p&gt;相比之下，Homebrew 更像是一個命令行形式的 Appstore。當然我最喜歡的包管理器還得是 pacman。&amp;#160;&lt;a href=&#34;#fnref:2&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:3&#34;&gt;&#xA;&lt;p&gt;Progressive Web APP，漸進式 Web 應用。&amp;#160;&lt;a href=&#34;#fnref:3&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:4&#34;&gt;&#xA;&lt;p&gt;本質上就是調 Xinput。&amp;#160;&lt;a href=&#34;#fnref:4&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;</description>
        </item></channel>
</rss>
