<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Distorted Scribbles</title>
        <link>https://blog.l3zc.com/en/</link>
        <description>Recent content on Distorted Scribbles</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>en</language><atom:link href="https://blog.l3zc.com/en/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>First impressions of Terraform IaC</title>
            <link>https://blog.l3zc.com/en/2026/04/iac-with-terraform/</link>
            <pubDate>Thu, 09 Apr 2026 07:13:35 +0000</pubDate>
            <guid>https://blog.l3zc.com/en/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 First impressions of Terraform IaC&#34; /&gt;&lt;p&gt;Terraform is an IaC tool. IaC stands for ‘Infrastructure as Code’: we write our infrastructure as declarative code, then use &lt;code&gt;terraform apply&lt;/code&gt; to deploy it. With the same configuration, you will always get exactly the same infrastructure every time (Nix OS users rejoice).&lt;/p&gt;&#xA;&lt;h2 id=&#34;why-use-terraform&#34;&gt;Why use Terraform&#xA;&lt;/h2&gt;&lt;p&gt;Traditional infrastructure management mostly relies on manual work and the dashboards provided by various cloud vendors, which brings the following pain points:&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;Pain point&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: left&#34;&gt;Explanation&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;Hard to reproduce&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;Configuring things by clicking around in a dashboard makes it easy to miss or misconfigure something, and hard to reproduce later&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;Environment drift&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;Manual changes gradually cause production and test environments to diverge, so in extreme cases testing is fine but production falls over&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;Hard to scale&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;Adding a new environment requires repeating lots of manual steps, which is time-consuming and error-prone&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;Hard to audit&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;There is no change history, &lt;del&gt;so when things go wrong it is harder to pass the buck&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;Hard to collaborate&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;Infrastructure ends up controlled by a small number of ‘people who know’, and anyone who wants to change something has to go through them, which is inefficient&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;To solve these problems, the concept of ‘Infrastructure as Code’ &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; was introduced, and Terraform is one of the best-known solutions.&lt;/p&gt;&#xA;&lt;p&gt;Take a realistic use case: suppose you buy a new GCP account with $300 trial credit every year. It is cheap, but every year you have to go back into the GCP console and recreate your machines. With Terraform, if you want to deploy the same setup again, you just replace the API token after switching accounts, then run &lt;code&gt;terraform apply&lt;/code&gt;. In a few minutes, you can recreate exactly the same machines, VPCs, S3, firewall rules, and so on as in your previous account.&lt;/p&gt;&#xA;&lt;p&gt;Another example is managing and migrating Cloudflare DNS and Tunnel. You only need to copy the old Tunnel’s Ingress Rule to the new Tunnel’s Ingress Rule. Even if you later migrate to other providers such as AliDNS or Route 53, you can still copy the data across as-is &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;This becomes especially useful when working with other people. Combined with Git, every change leaves a trace, merge conflicts are far less worrying, PRs can automatically generate previews of changes, and if something goes wrong you can roll back to the previous version immediately. None of this is really possible with the traditional approach of people directly operating a provider’s dashboard by hand.&lt;/p&gt;&#xA;&lt;h2 id=&#34;installing-terraform&#34;&gt;Installing Terraform&#xA;&lt;/h2&gt;&lt;p&gt;Terraform is written in Go, and the compiled output is naturally a single executable file, so installation is very straightforward. On Windows, you can install it directly with 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;If you do not want to use Winget, you can also use Scoop or another package manager, or place the precompiled binary into &lt;code&gt;$PATH&lt;/code&gt; to complete the installation.&lt;/p&gt;&#xA;&lt;p&gt;If you are on Linux, just use the appropriate package manager. If you install it by placing the binary into &lt;code&gt;$PATH&lt;/code&gt;, remember to use &lt;code&gt;sudo chmod +x terraform&lt;/code&gt; to make the file executable.&lt;/p&gt;&#xA;&lt;h2 id=&#34;two-basic-terraform-concepts&#34;&gt;Two basic Terraform concepts&#xA;&lt;/h2&gt;&lt;h3 id=&#34;providers&#34;&gt;Provider(s)&#xA;&lt;/h3&gt;&lt;p&gt;As mentioned earlier, Terraform is an Infrastructure as Code tool. As a tool, it is not tied to any specific platform. Instead, it connects to different platforms through Provider(s). To see what Provider(s) are available, you can browse &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’s 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 has a rich Provider(s) ecosystem&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;h3 id=&#34;state-management&#34;&gt;State management&#xA;&lt;/h3&gt;&lt;p&gt;Terraform stores the state information from each infrastructure change operation in a state file. By default, this is saved as the &lt;code&gt;terraform.tfstate&lt;/code&gt; file in the current working directory &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;, though you can also configure a different backend such as S3 or Postgres. Every time you run &lt;code&gt;terraform apply&lt;/code&gt;, Terraform compares the state declared in the current configuration files with the existing state file, calculates the differences, works out the correct order of operations, and then tells the Provider to apply those changes.&lt;/p&gt;&#xA;&lt;h2 id=&#34;terraforms-resource-import-problem&#34;&gt;Terraform’s resource import problem&#xA;&lt;/h2&gt;&lt;p&gt;Importing existing resources has long been one of Terraform’s most criticised pain points. Hashi Corp. seems to have stuck to a stubborn and frankly silly idea for years: all your infrastructure should have been created with Terraform from the very beginning, so there is no such thing as a resource import problem.&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Time&lt;/th&gt;&#xA;          &lt;th&gt;Version&lt;/th&gt;&#xA;          &lt;th&gt;Progress&lt;/th&gt;&#xA;          &lt;th&gt;Problem&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;Only &lt;code&gt;terraform import&lt;/code&gt;, one resource at a time, and it did not generate any configuration&lt;/td&gt;&#xA;          &lt;td&gt;After importing, you still had to hand-write the 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;Introduced the &lt;code&gt;import&lt;/code&gt; block and the &lt;code&gt;-generate-config-out&lt;/code&gt; parameter, so it could generate configuration&lt;/td&gt;&#xA;          &lt;td&gt;But you still had to write them one by one, and still had to provide the existing resource IDs yourself&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;The &lt;code&gt;import&lt;/code&gt; block gained support for &lt;code&gt;for_each&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Batch import at last, but you still had to obtain the IDs yourself&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Second half of 2024&lt;/td&gt;&#xA;          &lt;td&gt;v1.12&lt;/td&gt;&#xA;          &lt;td&gt;Introduced the &lt;code&gt;terraform query&lt;/code&gt; and &lt;code&gt;list&lt;/code&gt; blocks, finally enabling automatic resource discovery&lt;/td&gt;&#xA;          &lt;td&gt;But this feature has to be implemented by each Provider, and many Providers simply have not caught up&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;The community has been complaining about this for years, yet the question ‘I already have a pile of existing resources — how do I import them into Terraform?’ was never taken seriously. As an Infrastructure as Code tool, Terraform’s design philosophy is declarative configuration and idempotence &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. firmly believes that ‘the state declared in code is the only source of truth, so resources should be created from scratch with Terraform’. But in reality, most companies already have a large amount of legacy infrastructure. Having resources first and code later is the norm. Hashi Corp. acknowledged this contradiction very late; the &lt;code&gt;terraform query&lt;/code&gt; in &lt;code&gt;v1.12&lt;/code&gt; was really the first time the official tooling took the issue seriously — though as for Provider ecosystem support&amp;hellip; well, it has been rough.&lt;/p&gt;&#xA;&lt;h2 id=&#34;terraform-file-structure&#34;&gt;Terraform file structure&#xA;&lt;/h2&gt;&lt;p&gt;Terraform’s file structure is very simple. When it runs, the main program blindly reads all &lt;code&gt;.tf&lt;/code&gt; files in the working directory. As long as the required information is present, you can name the files whatever you like. For example, my file structure looks like this:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&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                 # Basic configuration (terraform block)&#xA;├── moved.tf                # Moved resources&#xA;├── provider.tf             # Configuration for each Provider&#xA;├── README.md&#xA;├── rename_resources.ps1&#xA;└── variables.tf            # Custom variables&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;main.tf&lt;/code&gt; is used to store the basic configuration:&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; is used to store the configuration for each 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;It is left empty here because we can pass credentials through environment variables instead of writing them directly into the configuration file. For example, the Cloudflare Provider accepts &lt;code&gt;CLOUDFLARE_API_TOKEN&lt;/code&gt; as an alternative to the &lt;code&gt;api_token&lt;/code&gt; variable.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;variables.tf&lt;/code&gt; is used to declare custom variables:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&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;These variables can be passed in through environment variables prefixed with &lt;code&gt;TF_VAR_&lt;/code&gt;. Terraform will also automatically read variables from the &lt;code&gt;terraform.tfvars&lt;/code&gt; file. The main purpose of variables is to be referenced from other configuration files, for example:&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  # the cloudflare_zone_example_com variable is referenced here&#xA;  settings = {&#xA;    flatten_cname = false&#xA;  }&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;With these basic settings in place, we can run &lt;code&gt;terraform init&lt;/code&gt; to initialise the Terraform environment and lock dependency versions. After that, the rest is just declaring our resources.&lt;/p&gt;&#xA;&lt;h2 id=&#34;importing-existing-resources-into-terraform&#34;&gt;Importing existing resources into Terraform&#xA;&lt;/h2&gt;&lt;p&gt;As mentioned earlier, Terraform uses state management. This is great, but it also creates a problem during initialisation: by default, Terraform’s state file is obviously empty.&lt;/p&gt;&#xA;&lt;p&gt;At this point, what we need to do is import the state of the existing resources from the cloud provider into Terraform, so that Terraform can take over and manage our infrastructure seamlessly. As you have probably noticed from the earlier rant, resource import in Terraform is a sore point. Taking DNS records hosted on Cloudflare as an example, if the Provider supports it, you can use &lt;code&gt;terraform query&lt;/code&gt;, introduced in Terraform 1.12. In most cases, though, Providers have not implemented this new feature, and Cloudflare is one of those that does not support it.&lt;/p&gt;&#xA;&lt;p&gt;Fortunately, even if Hashi Corp. has not taken the problem seriously, companies that use Terraform heavily to manage infrastructure have come up with their own solutions. Cloudflare maintains an import tool called &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;, which saves a lot of manual effort. First, install &lt;code&gt;cf-terraforming&lt;/code&gt;. This tool is written in Go, so you either need a Go environment to install it, or you can place the official precompiled binary into &lt;code&gt;$PATH&lt;/code&gt; and make it executable.&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;The tool is fairly straightforward to use. First, you need the following environment variables:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;# If you use an API Token&#xA;export CLOUDFLARE_API_TOKEN=&#39;Hzsq3Vub-7Y-hSTlAaLH3Jq_YfTUOCcgf22_Fs-j&#39;&#xA;&#xA;# If you use an API Key&#xA;export CLOUDFLARE_EMAIL=&#39;user@example.com&#39;&#xA;export CLOUDFLARE_API_KEY=&#39;1150bed3f45247b99f7db9696fffa17cbx9&#39;&#xA;&#xA;# Specify the zone ID of the domain to import; this is not needed for account-level resources (such as 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;Tip&lt;/span&gt;&#xA;        &lt;/div&gt;&#xA;        &lt;div class=&#34;alert-body&#34;&gt;&#xA;            &lt;p&gt;The commands here assume you are using Bash. If your shell is not compatible with Bash syntax, you will need to adjust them. For example, in PowerShell on Windows, the syntax for setting an environment variable is:&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;Usually, you only need to set &lt;code&gt;CLOUDFLARE_API_TOKEN&lt;/code&gt; and &lt;code&gt;CLOUDFLARE_ZONE_ID&lt;/code&gt;. When creating the API token in the console, remember to grant it the necessary permissions. In this case, we are only importing DNS records, so giving it permission to edit zone DNS is enough.&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;Grant the permissions required to operate on the resources&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;Right, all the preparation is done. Now we can start generating the configuration files.&lt;/p&gt;&#xA;&lt;p&gt;First, import the domain configuration in the account, namely &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;This step generates a file called &lt;code&gt;zone.tf&lt;/code&gt; in the current directory, containing content in the following format:&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;At this point, the domain resources have been imported, but their internal configuration has not. Next, import the DNS records under the domain:&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;This step generates a configuration file called &lt;code&gt;dns.tf&lt;/code&gt; in the current directory, containing content in the following format:&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;If you need to import multiple domains, just set the &lt;code&gt;CLOUDFLARE_ZONE_ID&lt;/code&gt; environment variable separately each time and rerun the command.&lt;/p&gt;&#xA;&lt;p&gt;The generated configuration file here can be used directly — it is the Terraform configuration file we need later. However, at this point we have only generated the configuration file; Terraform’s state is still empty. If you run &lt;code&gt;terraform apply&lt;/code&gt; now, Terraform will blindly treat all the declarations we just imported as new resources and throw a pile of ‘Alredy Exists’ errors. So next, we need to import the generated configuration into Terraform’s &lt;code&gt;terraform.tfstate&lt;/code&gt; state.&lt;/p&gt;&#xA;&lt;p&gt;Terraform introduced the &lt;code&gt;import&lt;/code&gt; block in version 1.5, which is much more modern than typing import commands one line at a time. The process is to generate an &lt;code&gt;.tf&lt;/code&gt; file containing &lt;code&gt;import&lt;/code&gt; blocks. The next time you run &lt;code&gt;terraform apply&lt;/code&gt;, Terraform will automatically perform the import for you.&lt;/p&gt;&#xA;&lt;p&gt;Generate the &lt;code&gt;import&lt;/code&gt; blocks for &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 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;Generate the &lt;code&gt;import&lt;/code&gt; blocks for &lt;code&gt;cloudflare_dns_record&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;This step generates &lt;code&gt;import.tf&lt;/code&gt; in the current directory. It contains the import information needed to tell Terraform which cloud-provider resource ID corresponds to each &lt;code&gt;resource&lt;/code&gt; block generated in the previous step. This ID is the code the cloud provider uses internally to identify a resource. You would not normally see it in the control panel; you only get it by requesting it through the API. Terraform needs this ID during import to confirm that the local definition matches the cloud resource, ensuring strict idempotence.&lt;/p&gt;&#xA;&lt;p&gt;Right, now let us run &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;If the numbers for Add, Change, and Destroy are all 0, then the import has gone correctly. Just run &lt;code&gt;terraform apply --auto-approve&lt;/code&gt;, and the resources will be imported.&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;state-storage-and-continuous-integration&#34;&gt;State storage and continuous integration&#xA;&lt;/h2&gt;&lt;p&gt;One of IaC’s core strengths is that it makes Git-based collaboration and CI easy, but before that there is another problem to solve: where exactly should &lt;code&gt;terraform.tfstate&lt;/code&gt; live? Nobody wants to painstakingly import state for each environment only to lose it every time they switch.&lt;/p&gt;&#xA;&lt;p&gt;Terraform currently supports the following state backends:&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;If you do not have any special requirements, you can choose &lt;code&gt;s3&lt;/code&gt; as I do. Cloudflare R2 has a free tier, after all, so you might as well use it.&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 does not need this AWS validation&#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;For the S3 backend, it is recommended to store credentials in the two environment variables &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt; and &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt;:&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;Once configured, run &lt;code&gt;terraform init -migrate-state&lt;/code&gt; and the state will be stored successfully in the cloud. After that, no matter where you edit the configuration or run &lt;code&gt;terraform apply&lt;/code&gt;, you will not need to worry about Terraform state getting out of sync.&lt;/p&gt;&#xA;&lt;p&gt;Next comes the GitHub CI configuration. It is actually very simple: on each &lt;code&gt;git push&lt;/code&gt;, just trigger &lt;code&gt;terraform init&lt;/code&gt;, &lt;code&gt;terraform fmt&lt;/code&gt;, and &lt;code&gt;terraform apply&lt;/code&gt;. Here is my &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;For PRs, CI should automatically attach the output of &lt;code&gt;terraform plan&lt;/code&gt; to each PR:&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;Expand Plan details&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;Every PR will have Plan output&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;h2 id=&#34;ongoing-workflow&#34;&gt;Ongoing workflow&#xA;&lt;/h2&gt;&lt;p&gt;At this point, Terraform’s initial ‘takeover’ is complete, and after that you move into day-to-day maintenance. At this stage there are really only three things to do:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Create new resources&lt;/li&gt;&#xA;&lt;li&gt;Modify existing resources&lt;/li&gt;&#xA;&lt;li&gt;Delete resources that are no longer needed&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;There are only two common operations: &lt;code&gt;terraform plan&lt;/code&gt; and &lt;code&gt;terraform apply&lt;/code&gt;. If you are working alone, you can usually just commit small changes directly. If you are working in a team, though, each change should follow the principle of using a PR whenever possible rather than committing directly.&lt;/p&gt;&#xA;&lt;h3 id=&#34;creating-infrastructure&#34;&gt;Creating infrastructure&#xA;&lt;/h3&gt;&lt;p&gt;Suppose you want to add a new DNS record, create a new Tunnel, or create a new object storage bucket. The process looks like this:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-mermaid&#34;&gt;flowchart TD&#xA;  C1[Create a new branch&#xA;such as feat/add-minio-record] --&gt; C2[Add a new resource block]&#xA;  C2 --&gt; C3[terraform fmt + validate]&#xA;  C3 --&gt; C4[terraform plan]&#xA;  C4 --&gt; C5{Only the expected new resources?}&#xA;  C5 -- No --&gt; C6[Fix the configuration and rerun plan]&#xA;  C6 --&gt; C4&#xA;  C5 -- Yes --&gt; C7[Submit PR]&#xA;  C7 --&gt; C8[After merge, CI apply]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The ideal &lt;code&gt;plan&lt;/code&gt; output is:&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;If you only meant to add something but &lt;code&gt;to destroy&lt;/code&gt; appears, do not rush in. Usually it means a bad reference, a wrong variable, or that you accidentally changed a resource address. Check carefully to see what went wrong.&lt;/p&gt;&#xA;&lt;h3 id=&#34;modifying-and-deleting-infrastructure&#34;&gt;Modifying and deleting infrastructure&#xA;&lt;/h3&gt;&lt;p&gt;The process for modifying resources is similar to creating them, but there is one extra step: evaluate whether the change will trigger a rebuild.&lt;/p&gt;&#xA;&lt;p&gt;That is because many Provider fields are &lt;code&gt;ForceNew&lt;/code&gt;. You think you are only changing one field, and Terraform replies: ‘Right then, delete and recreate it.’ In a DNS scenario like this, that is not a huge issue, but for something like a cloud instance, deleting and recreating it can obviously cause real damage.&lt;/p&gt;&#xA;&lt;p&gt;It is best to follow this order:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-mermaid&#34;&gt;flowchart TD&#xA;  M1[Modify .tf] --&gt; M2[terraform plan]&#xA;  M2 --&gt; M3{replace/destroy appears?}&#xA;  M3 -- No --&gt; M7[Confirm the scope of impact]&#xA;  M7 --&gt; M8[terraform apply]&#xA;  M3 -- Yes --&gt; M4[Pause and review the change]&#xA;  M4 --&gt; M5[Add lifecycle protection if needed]&#xA;  M5 --&gt; M6[Schedule a maintenance window]&#xA;  M6 --&gt; M8&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;For production environments, being a bit slower when creating or modifying things is not a big problem. What matters most is correctness. Go a bit slower; do not make mistakes. IaC is not a speed contest — it is about predictability.&lt;/p&gt;&#xA;&lt;p&gt;If you are deleting resources instead (for example, retiring a DNS record or cleaning up an abandoned Tunnel), follow the process below &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[Confirm the resource is no longer needed; check dependencies in services, monitoring, and scripts] --&gt; D2[Delete the resource block or adjust count/for_each]&#xA;  D2 --&gt; D3[terraform plan]&#xA;  D3 --&gt; D4{Does to destroy match expectations?}&#xA;  D4 -- No --&gt; D5[Revert the change and continue checking dependencies]&#xA;  D5 --&gt; D1&#xA;  D4 -- Yes --&gt; D6[Prepare a rollback plan and choose a low-traffic window]&#xA;  D6 --&gt; D7[PR approved]&#xA;  D7 --&gt; D8[terraform apply]&#xA;  D8 --&gt; D9[Availability check after deletion]&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;suggestions-for-day-to-day-collaboration&#34;&gt;Suggestions for day-to-day collaboration&#xA;&lt;/h3&gt;&lt;ul&gt;&#xA;&lt;li&gt;Put credentials in environment variables or CI secrets; do not write them into &lt;code&gt;.tf&lt;/code&gt; or the repository&lt;/li&gt;&#xA;&lt;li&gt;Enable protection policies for critical resources to prevent accidental deletion&lt;/li&gt;&#xA;&lt;li&gt;Split directories by resource type&lt;/li&gt;&#xA;&lt;li&gt;Run &lt;code&gt;terraform plan&lt;/code&gt; regularly to check for and correct infrastructure drift &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;, so you do not end up with manual dashboard changes by mistake&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Although this workflow may look a bit cumbersome, every change is recorded, auditable, and reversible — and most importantly, reproducible. That is where IaC delivers its real value.&lt;/p&gt;&#xA;&lt;h2 id=&#34;references&#34;&gt;References&#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;Using Terraform to manage DNS records on CloudFlare - 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; refers to a method of defining and deploying the required infrastructure using machine-readable configuration files.&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;Each provider uses different field names and formats in its configuration files, but these can be converted fairly easily with a script.&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;One especially important thing to note is that the state file may contain sensitive information stored in plain text, such as database passwords and API keys, so you must never commit the &lt;code&gt;.tfstate&lt;/code&gt; file to a public code repository.&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, a declarative configuration language developed by HashiCorp, designed to balance machine readability with human readability.&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 means that when a computer system or interface receives the same request multiple times, the effect is the same as if it had been executed once. No matter how many times it runs, the system’s final state remains consistent.&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;If you only want Terraform to stop managing a resource, rather than actually destroying it in the cloud, you should use the &lt;code&gt;terraform state rm&lt;/code&gt; command instead of deleting the resource block from the code and then running &lt;code&gt;apply&lt;/code&gt;, otherwise the real resource in the cloud will be destroyed as well.&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 refers to a situation where infrastructure is modified in reality through non-IaC means such as clicking around in a console, causing the actual state to differ from the state declared in code.&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>Perpetually Busy</title>
            <link>https://blog.l3zc.com/en/2026/03/busy-for-eternity/</link>
            <pubDate>Fri, 13 Mar 2026 23:14:26 +0800</pubDate>
            <guid>https://blog.l3zc.com/en/2026/03/busy-for-eternity/</guid>
            <description>&lt;p&gt;Work, work, until the day we die.&lt;/p&gt;&#xA;&lt;p&gt;Tutoring others during the winter break (see &lt;a class=&#34;link&#34; href=&#34;https://blog.l3zc.com/en/2026/02/buns-soaked-in-human-blood/&#34; &gt;here&lt;/a&gt;), I barely had a moment to catch my breath. The Lunar New Year was technically a holiday, but in reality, it was just a different form of work—endless socialising and the exhaustion of travelling. No sooner had I settled back into my flat than I was throwing myself headfirst into teaching again. To be honest, tutoring is far from easy. When chatting with my mates who are also teaching assistants at the same agency, we all joke that getting through a lesson is an absolute nightmare: the students can&amp;rsquo;t remember what they&amp;rsquo;ve tried to rote-learn, their basic arithmetic is completely absent, key concepts are constantly muddled, and trying to hold a normal, flowing conversation is virtually impossible. To be fair, you can&amp;rsquo;t really blame them; if anyone&amp;rsquo;s at fault, it&amp;rsquo;s the educational environment they&amp;rsquo;ve been subjected to. Every family has its own underlying struggles. For various reasons, most of these students ended up taking alternative vocational pathways rather than facing the rigorous gauntlet of the standard university entrance exams. Some barely even managed to scrape through secondary school. Ultimately, they just want to secure a job, which is why they are willing to pay for our one-on-one sessions.&lt;/p&gt;&#xA;&lt;p&gt;Initially, I worked as a teaching assistant at an agency, but I eventually decided to go freelance. This not only bumped up my earnings but also saved me a fair bit of lesson preparation time. I&amp;rsquo;d teach every day from 7:30 AM to 11:00 AM for 400 RMB. An hourly rate crossing the 100 RMB mark might seem quite decent, but when you factor in the students&amp;rsquo; extremely poor foundational knowledge, I can frankly say I earn every single penny with a clear conscience. Honestly, it is incredibly hard-earned cash.&lt;/p&gt;&#xA;&lt;p&gt;Getting back to the point, the new term has started, and I have to plunge straight back into my own academic commitments without missing a beat. It genuinely feels like I am perpetually busy. What I call &amp;ldquo;resting&amp;rdquo; is essentially just swapping to a slightly less taxing activity. If I&amp;rsquo;m exhausted from teaching, I&amp;rsquo;ll go home and unwind with a bit of Vibe Coding; if staring at the screen drives me up the wall, I&amp;rsquo;ll switch to tidying up the flat and doing the chores. When it comes to everyday drive and our finite human energy, I can acutely feel the gulf between different types of people. Some are naturally gifted, bursting with boundless energy. They churn out project after project, publish paper after paper, and maintain incredibly high productivity. It seems the only thing holding them back is the number of hours in a day, rather than any lack of stamina or motivation. Meanwhile, most of us are mere mortals like myself. Even if we desire that kind of lifestyle, our spirits are willing but our flesh is weak. Yet, seeing the sheer output of that first group inevitably brews anxiety, leaving us with no choice but to push our limits and treat ourselves as &amp;ldquo;practical perpetual motion machines&amp;rdquo; &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;The pressure-cooker educational system we’ve been put through since childhood has genuinely warped us. There&amp;rsquo;s a famous quote by a well-known intensive tuition teacher here: &amp;ldquo;How on earth can you sleep at your age?&amp;rdquo; Though originally meant to relentlessly tell off students nodding off in his lectures, it has subtly and profoundly conditioned our entire generation, driving us into a state that&amp;rsquo;s almost pathological. In life, there is no ultimate &amp;lsquo;finish line&amp;rsquo;. In primary school, you’re scrambling to secure a spot at a top secondary school; then you’re fighting to pass your GCSE-equivalents; later, you’re battling through the brutal university entrance exams. Once at university, the rat race continues as you desperately try to secure a master&amp;rsquo;s spot, a PhD, a stable civil service role, or simply a respectable corporate job. And even after you&amp;rsquo;ve finished your education and entered the workforce, you&amp;rsquo;re faced with endless promotions, performance metrics, and the daily grind of paying the bills. Work, work, until the day we die. Hoping to cross that final finish line and rest on your laurels forever? I&amp;rsquo;m afraid that’s simply impossible.&lt;/p&gt;&#xA;&lt;p&gt;Despite knowing all this perfectly well, I still feel a dreadful sense of emptiness whenever I actually stop to rest. And so, I remain perpetually busy, endlessly working. I give my body the bare minimum of sleep it requires and use physical exercise to keep my hormones in check. My idea of a &amp;ldquo;break&amp;rdquo; is just switching to a lighter task that I enjoy. Having been so deeply conditioned by a high-stakes education system and relentless meritocracy, I suspect this way of living won&amp;rsquo;t be changing for a very long time.&lt;/p&gt;&#xA;&lt;p&gt;Looking on the bright side, however, this recent stint of hard graft has meant I&amp;rsquo;ve earned enough to cover my own living expenses for the entire term. I&amp;rsquo;ve even got enough left over to buy my mum a new mobile phone for her birthday. Suddenly, being perpetually busy doesn&amp;rsquo;t seem quite so terrible after all.&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;I am borrowing the engineering distinction between ideal and practical current sources here to draw a parallel between an &amp;ldquo;ideal perpetual motion machine&amp;rdquo; and a practical one. The former is exactly what it says on the tin, whilst the latter refers to us ordinary folk, pushing our flesh-and-blood bodies to the absolute limit just to approximate it.&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>Buns Soaked in Human Blood</title>
            <link>https://blog.l3zc.com/en/2026/02/buns-soaked-in-human-blood/</link>
            <pubDate>Wed, 11 Feb 2026 17:06:57 +0800</pubDate>
            <guid>https://blog.l3zc.com/en/2026/02/buns-soaked-in-human-blood/</guid>
            <description>&lt;p&gt;Recently, I’ve been doing one-on-one coaching at a private training agency for students preparing for the second round of the State Grid Corporation of China (SGCC) recruitment exams. I teach for three and a half hours every evening, and &lt;strong&gt;they pay me 200 RMB (approx. £22).&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;The fact that the pay is abysmal isn&amp;rsquo;t even the biggest issue. The real problem lies in how they charge the students. They offer two main packages: the &amp;ldquo;Excellence Plan,&amp;rdquo; which costs a staggering 120,000 RMB (£13,000) but includes unlimited one-on-one sessions; and the &amp;ldquo;Contract Class,&amp;rdquo; which costs 70,000 RMB (£7,500), but with a catch—&lt;strong&gt;students must pay an additional 450 RMB (£50) for every one-on-one session.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;Yesterday, I arranged a private session with one of the students in the afternoon. My plan was to fly under the agency&amp;rsquo;s radar; the student would pay me 300 RMB directly. It seemed like a win-win: I’d make an extra hundred, and they’d save 150. However, the student didn&amp;rsquo;t quite catch on. When they asked the coordinator for leave, they explicitly mentioned they were &amp;ldquo;getting extra tutoring from me.&amp;rdquo; The agency immediately &amp;ldquo;benevolently&amp;rdquo; stepped in to help me collect the 450 RMB fee from the parents.&lt;/p&gt;&#xA;&lt;p&gt;It didn&amp;rsquo;t stop there. When it came time to calculate my earnings, the agency classified this as an &amp;ldquo;extra lesson.&amp;rdquo; They claimed I had &amp;ldquo;misused&amp;rdquo; my afternoon lesson-preparation time to teach a student not enrolled in the &amp;ldquo;Excellence Plan&amp;rdquo;. For those three and a half hours of work, they only gave me 120 RMB.&lt;/p&gt;&#xA;&lt;p&gt;To put it simply:&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;&lt;strong&gt;Student pays 450 → Agency pockets 330 → I receive 120&lt;/strong&gt;&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;p&gt;Naturally, I’m annoyed that the 300 RMB I should have earned was whittled down to 120. But beyond my personal loss, what truly unsettles me is the predatory nature of this business. Most of these students are vocational college graduates from humble backgrounds. In China’s job market, they already face significant discrimination, and they are doing everything in their power to climb the social ladder. They have staked everything—tens, or even hundreds of thousands of RMB, potentially their family’s entire life savings—on these courses to get a stable job at the State Grid. They believe they are buying a &amp;ldquo;cure&amp;rdquo; to change their fate, unaware that the medicine is being served with a side of their own lifeblood. Meanwhile, as the one actually doing the hard work of teaching, I receive a smaller cut than the agency that simply sits back and collects the rent.&lt;/p&gt;&#xA;&lt;p&gt;In Chinese literature, we call this &amp;ldquo;eating buns soaked in human blood&amp;rdquo;—profiting off the desperation and suffering of others.&lt;/p&gt;&#xA;&lt;p&gt;On this single transaction, the agency made a net profit of 330 RMB for doing absolutely nothing. It is, quite frankly, unacceptable. I have already advised the student to be more &amp;ldquo;flexible&amp;rdquo; when asking for leave in the future. I’ve also had a word with their coordinator, making it clear she should look the other way regarding our private arrangements. Given that she’s also a working-class employee with performance targets tied to student results, I doubt she’ll be foolish enough to jeopardize her own KPIs.&lt;/p&gt;&#xA;&lt;p&gt;That said, this agency had better watch its step. The joke I made during &amp;ldquo;lesson prep&amp;rdquo;—about the teaching assistants forming a temporary union and going on strike for better pay—might just become a self-fulfilling prophecy one day.&lt;/p&gt;&#xA;&lt;p&gt;I can’t help but wonder who the real &amp;ldquo;ruthless capitalists&amp;rdquo; are meant to be. This agency seems to have mastered the art of the blood-soaked business quite effectively.&lt;/p&gt;&#xA;</description>
        </item><item>
            <title>2025 Year in Review</title>
            <link>https://blog.l3zc.com/en/2025/12/2025-end-of-the-year-summary/</link>
            <pubDate>Wed, 31 Dec 2025 19:28:17 +0800</pubDate>
            <guid>https://blog.l3zc.com/en/2025/12/2025-end-of-the-year-summary/</guid>
            <description>&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/cover_hu_546f5de4e94beb95.webp&#34; alt=&#34;Featured image of post 2025 Year in Review&#34; /&gt;&lt;p&gt;What a busy year it has been. Before I knew it, we reached the end of the year again. There was indeed a lot going on—plenty of rushing about—but I’ve managed to achieve some significant milestones. I take up my pen once again to write this year-end summary as a testament to the year that was.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-grid-the-final-destination-for-electrical-engineers&#34;&gt;The Grid: The Final Destination for Electrical Engineers&#xA;&lt;/h2&gt;&lt;h3 id=&#34;running-ragged-for-a-job&#34;&gt;Running Ragged for a Job&#xA;&lt;/h3&gt;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/IMG_20250920_154353_hu_11efd88f784ccf12.webp&#34; alt=&#34;Attended a job fair&#34; /&gt;&#xA; &#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/IMG_20251220_153620_hu_7fe2627d8f597cfd.webp&#34; alt=&#34;Hainan Power Grid interview at the Furama Hotel&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;After a full year of suspension, I returned to my studies on schedule to begin my final year of university, officially becoming a &amp;ldquo;fresh graduate&amp;rdquo;. Consequently, job hunting naturally moved to the top of my agenda. As an Electrical Engineering student from a university with historical ties to the former Ministry of Electric Power, finding a job isn&amp;rsquo;t exactly impossible. However, saying it&amp;rsquo;s easy wouldn&amp;rsquo;t be accurate either. given the current climate: slowing economic growth, an uncertain international situation, reduced hiring demand, and the &amp;ldquo;rat race&amp;rdquo; becoming increasingly intense everywhere.&lt;/p&gt;&#xA;&lt;p&gt;In September, I attended a graduate job fair, handed out a few CVs in person, and applied for four or five roles online. I only received interview invitations from two companies. I experienced primarily that the prestige of being from a &amp;ldquo;former Power Ministry affiliated university&amp;rdquo; is really only recognised within the power system itself&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;. The employment market is just the way it is right now. Apart from the power system, decent-paying jobs are not easily found by students from &amp;ldquo;standard universities&amp;rdquo; (non-elite institutions) like ours. But in comparison, at least they still accept our CVs; for some other majors, companies won&amp;rsquo;t even look at them.&lt;/p&gt;&#xA;&lt;h3 id=&#34;written-exams-for-the-two-grid-giants&#34;&gt;Written Exams for the Two Grid Giants&#xA;&lt;/h3&gt;&lt;p&gt;Working for the Power Grid represents the most relevant and stable career path for my major. The largest employers in our field—State Grid (SGCC) and China Southern Power Grid (CSG)—both require candidates to pass a written exam to qualify for an interview. The exam covers eight subjects in total: &lt;em&gt;Circuit Theory&lt;/em&gt;, &lt;em&gt;Electrical Machines&lt;/em&gt;, &lt;em&gt;Power System Analysis&lt;/em&gt;, &lt;em&gt;Power System Protection&lt;/em&gt;, &lt;em&gt;Power Electronics&lt;/em&gt;, &lt;em&gt;High Voltage Engineering&lt;/em&gt;, &lt;em&gt;Electrical Equipment &amp;amp; Main Systems&lt;/em&gt; (called &amp;ldquo;Electrical Part of Power Plants&amp;rdquo; at our uni), and the &lt;em&gt;Administrative Aptitude Test&lt;/em&gt; (essentially a civil service competency test).&lt;/p&gt;&#xA;&lt;p&gt;The first seven are technical subjects, while the Aptitude Test covers a bizarre range of topics, including corporate culture, which you simply have to memorise. Current affairs and politics(Yes, this is also a subject) are mandatory, but what do they test? Extremely recent events—editorials published in &lt;em&gt;Qiushi&lt;/em&gt; (the Party&amp;rsquo;s&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; theoretical journal) just days ago appeared directly in the exam a few days later. I was honestly speechless.&lt;/p&gt;&#xA;&lt;p&gt;This year&amp;rsquo;s exam was strange, particularly regarding difficulty. Historically, there are always a few calculation questions, and Circuit Theory usually tests flexible problem-solving ability. Based on this intelligence, I had mastered numerous calculation types—electrical computations, numerical settings, equivalent transformations—and felt quite confident. The result? The first batch of State Grid exams didn&amp;rsquo;t feature a single calculation question. The Aptitude Test was simple enough for primary school children, and the technical part tested pure concepts. If it weren&amp;rsquo;t for the CSG exam still requiring practical application, my study efforts would have been practically wasted. Unsurprisingly, classmates who were strong at calculations and did well in mock tests essentially bombed this exam. Conversely, those with less impressive academic records who struggled with maths managed to get decent scores by rote learning. At the end of the day, it&amp;rsquo;s just a corporate recruitment test; outsiders can never guess how they&amp;rsquo;ll set the questions. I did what I had to do, and the final hiring outcome was excellent, which is all that matters.&lt;/p&gt;&#xA;&lt;h3 id=&#34;interviews-interviews-and-more-interviews&#34;&gt;Interviews, Interviews, and More Interviews&#xA;&lt;/h3&gt;&lt;p&gt;In December, I rushed to four interviews. For the CSG Guangxi Power Grid Ltd., the only interview location was Guilin, forcing me to &lt;del&gt;skive off classes&lt;/del&gt; travel there. This resulted in my final trip of the year. Visiting Guilin twice in two years felt quite nice.&lt;/p&gt;&#xA;&lt;p&gt;The State Grid interview made me a bit nervous. To prevent interview questions from leaking to subsequent candidates&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;, they employed fully closed-loop management. The process was long—mostly spent waiting in a holding room. All electronic devices were sealed away starting at 1:30 PM. After an hour-long psychometric test, the long wait began. They screened the film &lt;em&gt;The Battle at Lake Changjin&lt;/em&gt; to keep us entertained, and provided tea and snacks. Dressed in suits and sitting upright, silently reciting my one-minute self-introduction until I knew it backwards, nervously waiting while watching the movie—it&amp;rsquo;s a flavour of anxiety you only understand if you&amp;rsquo;ve been there. Looking back, it was actually quite interesting.&lt;/p&gt;&#xA;&lt;p&gt;With the experience (and Offer) from the State Grid interview under my belt, the subsequent Southern Power Grid (CSG) interview was far less nerve-wracking. The CSG format differs from the State Grid. State Grid uses a semi-structured, double-blind interview: the first candidate draws a set of questions from sealed envelopes, and subsequent candidates answer the same ones, perhaps with a few follow-up queries. The interviewers cannot see your CV and can only score you based on the ID number you drew; you are not allowed to state your name or background. CSG, however, is interviewer-led. They ask various questions based on your CV, including technical and supplementary questions. The difficulty depends entirely on the interviewer&amp;rsquo;s mood. For instance, during my Hainan Power Grid interview, the technical interviewer drilled down relentlessly into my internship experience with incredibly detailed technical questions. In contrast, the technical questions at the Guangxi Power Grid interview were much friendlier, involving basic switching operations and lightning protection facilities.&lt;/p&gt;&#xA;&lt;h3 id=&#34;the-bittersweet-grid-training&#34;&gt;The Bittersweet Grid Training&#xA;&lt;/h3&gt;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/IMG_20250819_182040_hu_9f035c63414ae82d.webp&#34; alt=&#34;Evening stroll&#34; /&gt;&#xA; &#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/IMG_20250819_184119_hu_f173b6fa7aa0237a.webp&#34; alt=&#34;Clouds encountered by chance&#34; /&gt;&#xA; &#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/IMG_20250819_181921_hu_1055fee83bee50f7.webp&#34; alt=&#34;Nearby high-voltage lines&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/Screenshot_2025-10-10-20-57-03-452_com.tencent.mm_hu_200838b0f8909812.webp&#34; alt=&#34;Grinding questions, doing papers, repeat&#34; /&gt;&#xA; &#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/screenshot_2025-11-27_23-35-58_hu_63f5f0b6afaf6e93.webp&#34; alt=&#34;Corporate culture must be memorised by heart&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;Most people taking the Grid exams sign up with a training institute, and I was no exception. As early as last November, I methodically enrolled in a national cram school. They had two campuses; I chose the one further out in the suburbs since the rent was cheaper.&lt;/p&gt;&#xA;&lt;p&gt;The training schedule was intense and the workload heavy. The summer session involved 41 consecutive days of classes with only three rest days in between. Mobile phones had to be handed in before class every day. Fortunately, iPads were allowed for note-taking purposes, which gave me a rare opportunity to slack off occasionally.&lt;/p&gt;&#xA;&lt;p&gt;Actually, during that period, I found a rare chance to focus entirely on one thing. Apart from classes, I didn&amp;rsquo;t have to worry about anything else. So, while my body was tired, my &amp;ldquo;mind&amp;rdquo; wasn&amp;rsquo;t actually that weary. Now that I&amp;rsquo;m back at university, having to worry about big and small matters alike, my body isn&amp;rsquo;t as tired, but my &amp;ldquo;mind&amp;rdquo; is far more exhausted than before. Coupled with a recent cold and cough, sleeping 12 hours a day hasn&amp;rsquo;t brought much improvement. I can only wait until everything is sorted, then take leave, go home, and get some proper rest.&lt;/p&gt;&#xA;&lt;h2 id=&#34;retracing-steps&#34;&gt;Retracing Steps&#xA;&lt;/h2&gt;&lt;h3 id=&#34;yongzhou&#34;&gt;Yongzhou&#xA;&lt;/h3&gt;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/IMG_20250122_121431_hu_9139d942ff443734.webp&#34; alt=&#34;Yongzhou&#34; /&gt;&#xA; &#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/IMG_20250122_153223_hu_119bbf370aade398.webp&#34; alt=&#34;Yinlong Computer City—childhood memories&#34; /&gt;&#xA; &#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/IMG_20250122_153407_hu_75f5c2193137f130.webp&#34; alt=&#34;Twenty years and barely changed&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;I returned to Yongzhou early this year to visit relatives. My grandmother is nearly 90 years old but still quite hale and hearty; visiting often is simply the right thing to do. She has looked after me since I was a child, and my gratitude is beyond words. I often feel at a loss regarding how to repay her, so visiting Yongzhou at least once a year is my way of fulfilling some duty as a grandson.&lt;/p&gt;&#xA;&lt;p&gt;As for Yongzhou itself, it’s still the same; nothing has changed. After all, this isn&amp;rsquo;t an era of economic explosion, so how could a small city like this change much? Stagnation isn&amp;rsquo;t necessarily a tragedy, and change isn&amp;rsquo;t always a blessing. Like after the Cultural Revolution in the last century when everything changed, but everything also became unrecognisable. The commercial complex that was bustling when I was a child now basically only has clothing shops open on the ground floor. The cinema and old arcade closed long ago; the floors converted into dining areas are now dim and dreary. Any new restaurants, whether chains or independent ventures, inevitably fail after the initial hype washes away.&lt;/p&gt;&#xA;&lt;h3 id=&#34;hong-kong&#34;&gt;Hong Kong&#xA;&lt;/h3&gt;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/DSCF0187.JPG_hu_80338f753b923972.webp&#34; alt=&#34;Government Headquarters&#34; /&gt;&#xA; &#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/DSCF0189.JPG_hu_cb13da2068e9876.webp&#34; alt=&#34;Central&#34; /&gt;&#xA; &#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/IMG_20250422_112641_hu_1e96cecdbc2d2313.webp&#34; alt=&#34;Hotel exterior&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/DSCF0156.JPG_hu_63e4a420e01095c5.webp&#34; alt=&#34;Alleyway&#34; /&gt;&#xA; &#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/DSCF0169.JPG_hu_49d9f960a8af333c.webp&#34; alt=&#34;Street sign&#34; /&gt;&#xA; &#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/DSCF0142.JPG_hu_3fc15df218b0937c.webp&#34; alt=&#34;Tram&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/IMG_20250424_101442_hu_fb51beb94ea422c5.webp&#34; alt=&#34;Bank of China efficiency&#34; /&gt;&#xA; &#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/IMG_20250423_182408_hu_870bb99e461edf1b.webp&#34; alt=&#34;Blood is thicker than wine (jk)&#34; /&gt;&#xA; &#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/IMG_20250421_174511_hu_b0f44927a1040192.webp&#34; alt=&#34;Hong Kong flat&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;My trip to Hong Kong this time was just for fun, with no special objective other than mooching off the hotel room during my mum&amp;rsquo;s business trip. Of course, I opened a Bank of China account&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; while I was there. The whole itinerary was rather unremarkable. The only unique part was staying in a flat in Wan Chai for a few days, where I could buy groceries and cook for myself (&lt;del&gt;or eat &amp;ldquo;two-dish rice&amp;rdquo; takeouts&lt;/del&gt;), experiencing a slice of working-class life.&lt;/p&gt;&#xA;&lt;h3 id=&#34;guilin&#34;&gt;Guilin&#xA;&lt;/h3&gt;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/IMG_20251224_194924_hu_15f0e9b608e95be5.webp&#34; alt=&#34;Nice lighting on this sign&#34; /&gt;&#xA; &#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/IMG_20251224_112927_hu_4fd7a7bc2479dd30.webp&#34; alt=&#34;Chunji Roast Goose&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;The main reason for going to Guilin this time was the interview for the Guangxi branch of the Southern Power Grid. Naturally, I also took the opportunity to eat &lt;em&gt;Chunji Roast Goose&lt;/em&gt; and &lt;em&gt;Haitian Rice Noodle Rolls&lt;/em&gt;. The only downside was that it rained during the two days of the interview, leaving me with little inclination to walk around or sightsee. However, I ate my fill of rice rolls and roast goose—tasted great, would come again.&lt;/p&gt;&#xA;&lt;h2 id=&#34;old-for-new&#34;&gt;Old for New&#xA;&lt;/h2&gt;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/IMG_20250425_155402_hu_64b9ab0f62f7b107.webp&#34; alt=&#34;New computer, yay&#34; /&gt;&#xA; &#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/IMG_20250425_180234_hu_95f6f3aea77b7f19.webp&#34; alt=&#34;Bloody Windows 11&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;My previous computer was an i5 12450H + RTX 3060 Laptop configuration. I’d used it for over two years, and just a few months ago, I bought two sticks of Crucial&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; memory to upgrade it to 32GB. Frankly, there was no issue continuing to use it.&lt;/p&gt;&#xA;&lt;p&gt;However, memory chip prices were still at a low point at that time, and government trade-in subsidies further reduced the cost of new devices. Combined with the recent launch of the 50-series graphics cards, the price of new laptops was incredibly attractive. Unfortunately, my pockets were shallow, and I didn&amp;rsquo;t have the budget for the latest generation CPU + latest generation GPU combo. I had to settle for the second-best option: an N-1 generation processor paired with the latest generation graphics card&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;. I&amp;rsquo;ve been using it for nearly half a year now, and the daily experience is excellent, though the howling fan noise really kills the mood. Overall, I consider the laptop a case where the flaws don&amp;rsquo;t obscure the virtues. With storage prices skyrocketing and government subsidies tapering off, I doubt we&amp;rsquo;ll see laptops this cheap again anytime soon.&lt;/p&gt;&#xA;&lt;h2 id=&#34;my-cat&#34;&gt;My Cat&#xA;&lt;/h2&gt;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/IMG_20250427_125807_hu_5b61bb8c30db752f.webp&#34; alt=&#34;Crawling into any space he finds&#34; /&gt;&#xA; &#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/IMG_20250208_141725_hu_c37c72856d52fb99.webp&#34; alt=&#34;What’s this? A cat head! Pat Pat&#34; /&gt;&#xA; &#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/IMG_20250626_152438_hu_827db49001ba7222.webp&#34; alt=&#34;Living a better life than humans&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;My cat turned one year old this year (probably)&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;. Amidst the celebrations, for a male cat, growing from a kitten to an adult means his testicles start doing their job, leading to scenes like this:&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/mmexport1745478106719_hu_aa931e8ed6a41965.webp&#34; alt=&#34;M Y  D U V E T ~~&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;While I was away in Hong Kong, this stinky cat actually climbed onto my bed to pee and poop ~~ (probably because my dad didn&amp;rsquo;t scoop the litter)~~. Unforgivable. He had to get the snip!&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/mmexport1749356267417_hu_48575c3a7508956e.webp&#34; alt=&#34;Feels good man&#34; /&gt;&#xA; &#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/IMG_20250610_105723_hu_d5f83d680ed53460.webp&#34; alt=&#34;That’ll teach you to act up&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;The cat has brought a lot of joy to my life. Aside from the destruction, he is very cute. The sofa at home has been reduced to shreds after a year of claw service. You have to pay a price; that&amp;rsquo;s just how keeping a cat is. Scratch marks, cat hair everywhere during shedding season, and occasional acts of mischief are all part of the package. Since we accept the emotional value the cat brings, we must correspondingly accept these little flaws. I almost view him as family. From that perspective, you instantly forgive these shortcomings. Thinking back, perhaps I really do love him—so much so that I&amp;rsquo;ve started to love even his flaws. Because without them, he wouldn&amp;rsquo;t be a complete cat.&lt;/p&gt;&#xA;&lt;h2 id=&#34;still-lifting-still-on-meds&#34;&gt;Still Lifting, Still on Meds&#xA;&lt;/h2&gt;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/IMG_20251231_181040_hu_ada4c10cba556476.webp&#34; alt=&#34;Some empty medication boxes&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;Pumping iron, especially leg day, is truly exhilarating. Feeling that sheer release after your muscles exert power is genuinely addictive. During the time I wasn&amp;rsquo;t at the Grid training, I consistently went to the gym, not just enjoying that feeling of release but also building muscle. honestly, not exercising feels terrible.&lt;/p&gt;&#xA;&lt;p&gt;I am still taking medication every day. I really don&amp;rsquo;t want to suffer an emotional breakdown amidst my relentless schedule. My original plan to gradually taper off the medication after a year has been indefinitely postponed. Why? Put simply, the pressure from various sources has been significant lately. While staying on meds forever isn&amp;rsquo;t a solution, and I must slowly taper off once things settle, I am far from living a stable, certain life at this point nearing graduation.&lt;/p&gt;&#xA;&lt;p&gt;This period is continuously filled with possibilities and opportunities, but also pressure. The money spent on medicine is a small price to pay, but the &amp;ldquo;shield&amp;rdquo; it provides for emotional stability plays a crucial role.&lt;/p&gt;&#xA;&lt;h2 id=&#34;no-time-for-gaming&#34;&gt;No Time for Gaming&#xA;&lt;/h2&gt;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/steam-2025-recap_hu_9c34df2ccc0aaf15.webp&#34; alt=&#34;My Steam Year in Review&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;As you can tell from &lt;a class=&#34;link&#34; href=&#34;https://s.team/y25/gqhwfdqc?l=schinese&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;my Steam Year in Review&lt;/a&gt;, I really didn&amp;rsquo;t have much time for games this year. &amp;ldquo;Stealing moments of leisure from a busy life&amp;rdquo; was the theme. At the end of the year, I downloaded &lt;em&gt;Delta Force&lt;/em&gt;. sometimes, after Grid training finished at 10 PM, I&amp;rsquo;d go home and play until midnight. I&amp;rsquo;ve clocked 75 hours so far, and it feels pretty good.&lt;/p&gt;&#xA;&lt;p&gt;My &lt;em&gt;osu!&lt;/em&gt; playing essentially dropped off in the second half of the year, though my PP still grew from 3,439pp last year to 4,701pp this year. This game really requires perseverance; if you stop for a while and lose your muscle memory, rehabilitation takes time. Thinking about finishing class at 10 PM, completely drained, and then trying to play a game requiring such intense concentration and reaction speed—well, &amp;ldquo;I simply cannot do it&amp;rdquo;. Ideally, mid-year, my feel for the mouse was at its peak—I could snap to anything. I thought, &amp;ldquo;No one can stop me on my road to 5 digit rank&amp;rdquo;&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;, yet here I am, still hovering on the edge of 5 digits. Quite ironic, really.&lt;/p&gt;&#xA;&lt;h2 id=&#34;my-online-presence&#34;&gt;My Online Presence&#xA;&lt;/h2&gt;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/12/2025-end-of-the-year-summary/image_hu_1b13d8cf59e9184e.webp&#34; alt=&#34;Blog statistics for this year&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;In 2025, traffic to my blog rose steadily, with 48.3K UV and 95.4K PV for the year. Traffic primarily came from Google (approx. 15.8K visitors) and Bing (approx. 15.2K visitors).&lt;/p&gt;&#xA;&lt;p&gt;My personal override rules on GitHub garnered 194 Stars, and I gained some new Followers.&lt;/p&gt;&#xA;&lt;p&gt;One of my articles was featured on Hacker News this year, resulting in significant traffic for a few days. Additionally, the translated English and Japanese versions of the blog have attracted quite a few readers, making up a not-insignificant portion of my visitors. So, a phenomenon occurred where, despite my busyness causing a lower update frequency than previous years, traffic actually doubled. I ultimately have to make concessions to life; pressure from living costs, studies, and employment inevitably impacts my willingness and energy to update the blog. On this point, I ask for my readers&amp;rsquo; understanding.&lt;/p&gt;&#xA;&lt;h2 id=&#34;a-new-year-approaches-wishes-for-myself-and-everyone&#34;&gt;A New Year Approaches: Wishes for Myself and Everyone&#xA;&lt;/h2&gt;&lt;p&gt;The New Year is coming. My biggest wish this year is, of course, to resolve the lingering issues from my suspension and graduate smoothly. Once I truly step into the workforce, I hope to focus more on the technical aspects required by the power system, dealing more with equipment and less with unnecessary, or even harmful, office politics. Hard work is fine as long as it&amp;rsquo;s safe, grounded, and meaningful: safe production, less fuss, more tolerance. I hope that in this upcoming job, I can grow from a fledgling novice into a true engineer.&lt;/p&gt;&#xA;&lt;p&gt;I also send my best wishes to you behind the screen: May your anxieties find a resting place, and may your efforts echo back to you in the New Year. May you live your days steadily and warmly at your own pace—you don&amp;rsquo;t have to win every step, as long as you know where you&amp;rsquo;re going. May you and those you care about be healthy and safe, with less senseless exhaustion and more definite happiness. And of course, I hope everyone finds more time to play games outside of study and work.&lt;/p&gt;&#xA;&lt;p&gt;As for me, I will continue learning where I should learn, and hit the road when it&amp;rsquo;s time to move. See you next year.&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;Refers to enterprises derived from the former Ministry of Electric Power, i.e., the State Power Corporation prior to 2002, and the State Grid, China Southern Power Grid, and Inner Mongolia Power Grid formed after 2002.&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;Communist Party of China, obviously.&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;The first candidate draws a set of questions from sealed envelopes, and subsequent candidates answer the same ones. Therefore, they had to prevent subsequent candidates from knowing their questions in advance.&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;Bank cards issued by banks in the Chinese mainland are subject to the financial regulations of the mainland. Meanwhile, mainland banks rarely issue bank cards with Visa and MasterCard logos. So, for my overseas payments, I had to open a bank account in Hong Kong.&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;Referring to Crucial memory, which recently axed its entire consumer product line.&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;I bought a &lt;a class=&#34;link&#34; href=&#34;https://blog.l3zc.com/2025/05/new-laptop-briefing/&#34; &gt;Mechrevo Aurora X Pro&lt;/a&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;When I picked him up last year, he was still a &lt;a class=&#34;link&#34; href=&#34;https://blog.l3zc.com/2024/12/2024-end-of-the-year-summary-public/#%e5%a4%a9%e4%b8%8a%e4%b8%8d%e4%bc%9a%e6%8e%89%e9%a6%85%e9%a5%bc%e4%bd%86%e4%bc%9a%e6%8e%89%e5%b0%8f%e7%8c%ab&#34; &gt;tiny kitten&lt;/a&gt;.&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;Refers to the number of digits in the ranking. For example, rank #114514 is 6 digits, while #11451 is 5 digits. Obviously, the fewer digits, the higher the player&amp;rsquo;s skill level.&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;/ol&gt;&#xA;&lt;/div&gt;&#xA;</description>
        </item><item>
            <title>My LLM Confidant, and My Writing in Suspension</title>
            <link>https://blog.l3zc.com/en/2025/09/satisfactory-llm/</link>
            <pubDate>Mon, 08 Sep 2025 23:53:48 +0800</pubDate>
            <guid>https://blog.l3zc.com/en/2025/09/satisfactory-llm/</guid>
            <description>&lt;p&gt;I have been rather occupied of late, yet in my spare moments, words remain my sanctuary. My schedule involves classes starting at 8:30 AM and running until evening self-study ends at 9:00 PM. Occasionally, I even put in &amp;ldquo;overtime&amp;rdquo;, not returning home until 10:00 PM. This routine persisted for forty-one consecutive days. Given my mental state under such intensity, returning home to play reaction-based games like &lt;em&gt;osu!&lt;/em&gt; was out of the question; I didn&amp;rsquo;t even have the energy to click through a single chapter of a visual novel.&lt;/p&gt;&#xA;&lt;p&gt;The only viable activity was to engage in small talk with an LLM (Large Language Model).&lt;/p&gt;&#xA;&lt;p&gt;I must confess, although I maintain a blog, I have never managed to articulate my views or philosophies (or perhaps simply my &amp;ldquo;ideas&amp;rdquo;) in a systematic fashion. I have often thought that this collection of overly broad and disparate viewpoints is difficult to convey through plain narrative. To address this, I attempted various methods, such as using a random event as a cross-section to &amp;ldquo;slice open&amp;rdquo; the tangled mess of my thoughts and depict their internal structure.&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; Yet, these methods never allowed me to express my inner voice freely. Writing does not flow effortlessly from my pen; I cannot simply write whenever I wish. Ultimately, a blog is meant for an audience, so when the words wouldn&amp;rsquo;t come, I resorted to writing simple technical posts to garner some search engine traffic.&lt;/p&gt;&#xA;&lt;p&gt;The advent of LLMs, however, has altered this dynamic. All my thoughts can now receive immediate responses in relative privacy. Why have I not written a blog post in so long? Fatigue is naturally a major factor, but the fact that LLMs satisfy a significant portion of my need for expression and validation—without the need for self-censorship—has diluted my desire to blog during this period.&lt;/p&gt;&#xA;&lt;p&gt;There is no doubt that composing long-form text is beneficial for the brain. The impact of LLMs on my ability to write at length is likely akin to, if not greater than, the impact micro-blogging (like Twitter or Weibo) had when it first appeared. Micro-blogging habituated people to fragmented expression and immediate feedback, weakening the ability to construct complex arguments and long-form narratives. LLMs go a step further: they not only provide a channel for expression but also directly engage in providing emotional value, acting as a substitute for a &amp;ldquo;confidant&amp;rdquo;. When posting on social media, one must consider privacy, controversy, or simply whether the content is worth exposing; the publisher invariably exercises some degree of caution.&lt;/p&gt;&#xA;&lt;p&gt;LLMs are different. The worst-case scenario for content sent to an API is that it gets used to train the model, not that Sam Altman will turn up at your doorstep the next day. As long as you aren&amp;rsquo;t sharing bank passwords or mentioning your real name, you can safely treat the large model as a close friend regarding current affairs commentary, psychological counselling, and the like. (Naturally, in this specific context, one wouldn&amp;rsquo;t touch domestic Chinese models with a bargepole due to censorship and privacy concerns). Every grumble receives a precise, earnest response—an affirmation akin to that of a soulmate. I fear no other place can offer such treatment.&lt;/p&gt;&#xA;&lt;p&gt;When a tool stimulates a &amp;ldquo;confidant&amp;rdquo; so perfectly, we may abandon the search for real human connection, or cease striving to become individuals capable of independent thought and self-integration. Writing these words serves as a summary, but also as a reflection. The LLM itself is merely a technology; used well, it helps immensely, but used poorly, the consequences can be severe. The &lt;em&gt;New York Times&lt;/em&gt; report &amp;ldquo;&lt;a class=&#34;link&#34; href=&#34;https://archive.is/ALVeI&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;Chatbots Can Go Into a Delusional Spiral. Here’s How It Happens.&lt;/a&gt;&amp;rdquo; serves as a prime example. Although OpenAI has begun to address the issue of &amp;ldquo;sycophancy&amp;rdquo; in its models, I must acknowledge that while I can treat the model as a confidant and receive so-called &amp;ldquo;understanding and affirmation&amp;rdquo;, I cannot live permanently within that sensation. Joint studies from OpenAI and MIT found a positive correlation between ChatGPT usage and user loneliness: the more users utilised ChatGPT, the lonelier they felt.&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; I could also argue that it is precisely because one is lonely that one turns to these LLMs, creating a vicious cycle. An LLM can serve as a seasoning for life, but it must never become the sole sustenance for the soul.&lt;/p&gt;&#xA;&lt;p&gt;(To Be Continued)&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://blog.l3zc.com/2024/06/the-worst-way-to-spend-a-day/&#34; &gt;The Worst Way To Spend A Day&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://www.engadget.com/ai/joint-studies-from-openai-and-mit-found-links-between-loneliness-and-chatgpt-use-193537421.html&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;Joint studies from OpenAI and MIT found links between loneliness and ChatGPT use&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;/ol&gt;&#xA;&lt;/div&gt;&#xA;</description>
        </item><item>
            <title>Ditch the Client Applications: Using the Mihomo Core Directly</title>
            <link>https://blog.l3zc.com/en/2025/07/switch-to-pure-mihomo-kernel/</link>
            <pubDate>Thu, 03 Jul 2025 18:30:00 +0800</pubDate>
            <guid>https://blog.l3zc.com/en/2025/07/switch-to-pure-mihomo-kernel/</guid>
            <description>&lt;img src=&#34;https://blog.l3zc.com/2025/07/switch-to-pure-mihomo-kernel/cover_hu_1a93b6f26c22bbfe.webp&#34; alt=&#34;Featured image of post Ditch the Client Applications: Using the Mihomo Core Directly&#34; /&gt;&lt;p&gt;Like most people, when I started using Clash/Mihomo, I opted for graphical clients based on the core for the sake of convenience. Essentially, these Mihomo clients are very similar: they all use the same backend, with the primary purpose of offering a GUI, managing config files, handling subscription updates, and system proxy settings. With this in mind, I think the usefulness of a Mihomo client largely depends on how well it implements &lt;em&gt;config overrides&lt;/em&gt;. Every configuration file obtained or subscribed to through a client goes through various override steps—such as changing the &lt;code&gt;mixed-port&lt;/code&gt;, adding &lt;code&gt;sniffer&lt;/code&gt; settings, and so on—before it’s handed over to the Mihomo core for startup.&lt;/p&gt;&#xA;&lt;p&gt;However, not all clients do this basic job adequately. Take ShellCrash, for example—the override mechanism is frequently buggy and feels more like an afterthought. If the client can&amp;rsquo;t even reliably update and tweak config files, it hardly deserves to be called a decent client.&lt;/p&gt;&#xA;&lt;p&gt;Instead of depending on these unreliable, black-box Mihomo clients, why not just take direct control? Manage your own configuration files and start the core yourself. This way, you get a cleaner, more reliable, and fully transparent setup.&lt;/p&gt;&#xA;&lt;h2 id=&#34;what-you-need-to-know&#34;&gt;What You Need to Know&#xA;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;Basic Linux skills&lt;/li&gt;&#xA;&lt;li&gt;Familiarity with CLI editors, like &lt;code&gt;nano&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;Have a Substore instance set up (optional)&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;installing-the-mihomo-core&#34;&gt;Installing the Mihomo Core&#xA;&lt;/h2&gt;&lt;p&gt;On Debian-based systems, you can install precompiled &lt;code&gt;.deb&lt;/code&gt; packages. For other &lt;code&gt;systemd&lt;/code&gt;-enabled distributions, just download the &lt;a class=&#34;link&#34; href=&#34;https://github.com/MetaCubeX/mihomo/releases&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;compiled binary&lt;/a&gt;, rename it to &lt;code&gt;mihomo&lt;/code&gt;, and place it in &lt;code&gt;/usr/local/bin&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;sudo curl -o /usr/local/bin/mihomo &lt;download_link&gt;&#xA;sudo chmod +x /usr/local/bin/mihomo&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Next, create &lt;code&gt;/etc/systemd/system/mihomo.service&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-systemd&#34;&gt;[Unit]&#xA;Description=mihomo Daemon, Another Clash Kernel.&#xA;After=network.target NetworkManager.service systemd-networkd.service iwd.service&#xA;&#xA;[Service]&#xA;Type=simple&#xA;LimitNPROC=500&#xA;LimitNOFILE=1000000&#xA;CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE&#xA;AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE&#xA;Restart=always&#xA;ExecStartPre=/usr/bin/sleep 1s&#xA;ExecStart=/usr/local/bin/mihomo -d /etc/mihomo&#xA;ExecReload=/bin/kill -HUP $MAINPID&#xA;&#xA;[Install]&#xA;WantedBy=multi-user.target&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Run &lt;code&gt;systemctl daemon-reload&lt;/code&gt; to refresh &lt;code&gt;systemd&lt;/code&gt;. Since there’s no config file yet, you can’t actually start the core, but you can enable it to start on boot using &lt;code&gt;systemctl enable mihomo&lt;/code&gt;—ready for when your config is set up.&lt;/p&gt;&#xA;&lt;h2 id=&#34;configuration-files&#34;&gt;Configuration Files&#xA;&lt;/h2&gt;&lt;p&gt;When starting, the core reads &lt;code&gt;/etc/mihomo/config.yaml&lt;/code&gt;. With the black-box clients out of the picture, you’re free to customise your config files however you like. Some VPN/proxy providers supply a complete config file you can download with &lt;code&gt;curl&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;For subscription management, I’m currently using Substore. I’ve previously shared a &lt;a class=&#34;link&#34; href=&#34;https://blog.l3zc.com/2025/03/clash-subscription-convert/&#34; &gt;Quickstart Guide to Substore Subscription Management&lt;/a&gt; if you want to refer to that for custom subscription workflows. To support a pure-core setup, my Substore &lt;a class=&#34;link&#34; href=&#34;https://github.com/powerfullz/override-rules/blob/main/convert.js&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;custom override script&lt;/a&gt; now includes a &lt;code&gt;full&lt;/code&gt; parameter, generating a standalone config file with all necessary ports, unified delay, external-controller settings, and more—ready to use out of the box.&lt;/p&gt;&#xA;&lt;p&gt;Once you’ve set up Substore, just download your config file and start the core:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;curl -o /etc/mihomo/config.yaml &lt;your-config-link&gt;&#xA;systemctl start mihomo&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;custom-override-rules&#34;&gt;Custom Override Rules&#xA;&lt;/h3&gt;&lt;p&gt;If my example config doesn’t work for your needs, that’s no problem—just tweak or add any overrides you like.&lt;/p&gt;&#xA;&lt;p&gt;Over the years, I’ve tested various override rules, sometimes even writing my own from scratch. Even with that, there are always edge cases—private domains for stuff like SSH on non-standard ports, for instance—that you won’t want to publish on GitHub. In those cases, you’ll want to append your own private overrides on top.&lt;/p&gt;&#xA;&lt;p&gt;The good thing is that Substore supports chaining multiple override scripts. All you need to do is add your custom script during config generation.&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-js&#34;&gt;function main(config) {&#xA;  config[&#34;rules&#34;].unshift(&#34;DOMAIN-SUFFIX,xxx,DIRECT&#34;)&#xA;  return config&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Note: When overriding rules, always use &lt;code&gt;.unshift()&lt;/code&gt; to add to the top of the list—don’t use &lt;code&gt;.push()&lt;/code&gt; to add at the end, or they’ll never match (since anything after &lt;code&gt;MATCH&lt;/code&gt; is ignored).&lt;/p&gt;&#xA;&lt;h3 id=&#34;building-your-own-config-from-scratch&#34;&gt;Building Your Own Config From Scratch&#xA;&lt;/h3&gt;&lt;p&gt;Don’t like my override rules, or prefer not to use Substore at all? No problem! Just refer to the &lt;a class=&#34;link&#34; href=&#34;https://wiki.metacubex.one/config/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;Mihomo Docs&lt;/a&gt; and build your own config from the ground up:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;mode: rule&#xA;mixed-port: 7890&#xA;redir-port: 7892&#xA;tproxy-port: 7893&#xA;allow-lan: true&#xA;log-level: info&#xA;ipv6: true&#xA;external-controller: 127.0.0.1:8000&#xA;# secret: yoursecret&#xA;unified-delay: true&#xA;routing-mark: 7894&#xA;tcp-concurrent: true&#xA;disable-keep-alive: true # Recommended when proxying mobile devices to prevent excessive standby drain&#xA;&#xA;dns:&#xA;  # Your DNS configuration&#xA;&#xA;sniffer:&#xA;  # Your domain sniffing config&#xA;&#xA;geodata-mode: true&#xA;geox-url:&#xA;  # Custom GeoData file URL&#xA;&#xA;proxy-providers:&#xA;  # Your proxy subscriptions&#xA;&#xA;rule-providers:&#xA;  # External routing rules&#xA;&#xA;rules:&#xA;  # Proxy rules&#xA;&#xA;proxy-groups:&#xA;  # Custom proxy groups&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;control-panel--dashboard&#34;&gt;Control Panel / Dashboard&#xA;&lt;/h2&gt;&lt;p&gt;Choose whichever dashboard you like. For example, I ran into some odd issues with Mihomo&amp;rsquo;s built-in &lt;code&gt;external-ui&lt;/code&gt;. So, I just deploy a separate Docker web dashboard—after all, it’s just a simple web UI. Just make sure your core’s API uses HTTP, and set your web panel to use HTTP as well (if you try HTTPS, you’ll be blocked by CORS policy).&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;$ mkdir zashboard &amp;&amp; cd zashboard&#xA;$ nvim compose.yml&#xA;$ docker compose up -d&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;compose.yml&lt;/code&gt; example:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;services:&#xA;  zashboard:&#xA;    image: ghcr.io/zephyruso/zashboard:latest&#xA;    ports:&#xA;      - &#34;8899:80&#34;&#xA;    restart: &#34;unless-stopped&#34;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;automatic-maintenance&#34;&gt;Automatic Maintenance&#xA;&lt;/h2&gt;&lt;p&gt;With the core running, how do you handle auto-updates for your subscription configs?&lt;/p&gt;&#xA;&lt;p&gt;Simple—write a shell script and automate it with cron. For instance, if you want to update your config and restart Mihomo at 3am daily, create &lt;code&gt;/etc/mihomo/auto_update.sh&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;#!/bin/bash&#xA;&#xA;# === Config Info ===&#xA;CONFIG_URL=&#34;&#34;&#xA;CONFIG_PATH=&#34;/etc/mihomo/config.yaml&#34;&#xA;BACKUP_DIR=&#34;/etc/mihomo&#34;&#xA;BACKUP_PREFIX=&#34;config.yaml&#34;&#xA;MAX_BACKUPS=7&#xA;TMP_PATH=&#34;/tmp/config.yaml.tmp&#34;&#xA;LOG_FILE=&#34;/var/log/mihomo_update.log&#34;&#xA;&#xA;# === Logging ===&#xA;log() {&#xA;    echo &#34;$(date &#39;+%F %T&#39;) $1&#34; | tee -a &#34;$LOG_FILE&#34;&#xA;}&#xA;&#xA;# === Backup existing config and clean old backups ===&#xA;backup_config() {&#xA;    if [ -f &#34;$CONFIG_PATH&#34; ]; then&#xA;        backup_file=&#34;$BACKUP_DIR/${BACKUP_PREFIX}.$(date &#39;+%Y%m%d_%H%M%S&#39;).bak&#34;&#xA;        cp &#34;$CONFIG_PATH&#34; &#34;$backup_file&#34;&#xA;        log &#34;Config backed up to $backup_file&#34;&#xA;        # Retain only the latest $MAX_BACKUPS backups&#xA;        old_backups=$(ls -1t $BACKUP_DIR/${BACKUP_PREFIX}.*.bak 2&gt;/dev/null | tail -n +$(($MAX_BACKUPS+1)))&#xA;        for f in $old_backups; do&#xA;            rm -f &#34;$f&#34; &amp;&amp; log &#34;Deleted old backup $f&#34;&#xA;        done&#xA;    else&#xA;        log &#34;No existing config found, skipping backup&#34;&#xA;    fi&#xA;}&#xA;&#xA;# === Download new config ===&#xA;download_config() {&#xA;    log &#34;Downloading new config...&#34;&#xA;    curl -fsSL -o &#34;$TMP_PATH&#34; &#34;$CONFIG_URL&#34;&#xA;    if [ $? -ne 0 ]; then&#xA;        log &#34;Download failed—check network or URL&#34;&#xA;        return 1&#xA;    fi&#xA;    # Basic validation: check file size&#xA;    if [ ! -s &#34;$TMP_PATH&#34; ]; then&#xA;        log &#34;Config file is empty—update aborted&#34;&#xA;        return 2&#xA;    fi&#xA;    log &#34;Config downloaded&#34;&#xA;    return 0&#xA;}&#xA;&#xA;# === Update config file ===&#xA;replace_config() {&#xA;    mv &#34;$TMP_PATH&#34; &#34;$CONFIG_PATH&#34;&#xA;    log &#34;Config updated&#34;&#xA;}&#xA;&#xA;# === Restart Mihomo service ===&#xA;restart_service() {&#xA;    systemctl restart mihomo&#xA;    if [ $? -eq 0 ]; then&#xA;        log &#34;Mihomo restarted&#34;&#xA;    else&#xA;        log &#34;Failed to restart Mihomo—please check manually&#34;&#xA;    fi&#xA;}&#xA;&#xA;main() {&#xA;    backup_config&#xA;&#xA;    download_config&#xA;    DL_STATUS=$?&#xA;    if [ &#34;$DL_STATUS&#34; -ne 0 ]; then&#xA;        log &#34;Aborted: config not updated, keeping old config&#34;&#xA;        exit 1&#xA;    fi&#xA;&#xA;    replace_config&#xA;&#xA;    restart_service&#xA;&#xA;    log &#34;=== Update complete ===&#34;&#xA;}&#xA;&#xA;main &#34;$@&#34;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Use &lt;code&gt;crontab -e&lt;/code&gt; to edit your crontab and schedule auto-updates at 3am daily:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-crontab&#34;&gt;0 3 * * * /etc/mihomo/update_config.sh&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;firewall-configuration&#34;&gt;Firewall Configuration&#xA;&lt;/h2&gt;&lt;p&gt;Manually setting up firewall rules to direct traffic through the Mihomo core isn’t as tricky as it sounds. I’ve previously covered the details in my article “&lt;a class=&#34;link&#34; href=&#34;https://blog.l3zc.com/2025/04/tailscale-setup-recap/#%E5%9C%A8-exit-node-%E5%8A%AB%E6%8C%81%E6%B5%81%E9%87%8F&#34; &gt;From Beginner to Advanced: Tailscale + ShellCrash for Remote Networking and Bypassing Internet Censorship&lt;/a&gt;”. Here, I’ll just summarise the practical steps.&lt;/p&gt;&#xA;&lt;p&gt;For my setup, I want all traffic from the &lt;code&gt;tailscale0&lt;/code&gt; interface to be transparently proxied by Mihomo. If all you need is TCP interception, &lt;code&gt;iptables&lt;/code&gt; REDIRECT is sufficient; but for UDP, QUIC, etc., you’ll need TPROXY. &lt;strong&gt;Don’t forget IPv6!&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;Here’s my firewall config:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;# Create custom chain&#xA;iptables -t mangle -N MIHOMO&#xA;&#xA;# Exclude local traffic as needed&#xA;iptables -t mangle -A MIHOMO -d 127.0.0.1/8 -j RETURN&#xA;iptables -t mangle -A MIHOMO -d 100.64.0.0/10 -j RETURN&#xA;iptables -t mangle -A MIHOMO -d 192.168.1.0/24 -j RETURN&#xA;iptables -t mangle -A MIHOMO -d 172.17.0.0/16 -j RETURN&#xA;&#xA;# Mark TCP and UDP for proxy&#xA;iptables -t mangle -A MIHOMO -p tcp -j TPROXY --on-port 7893 --tproxy-mark 233&#xA;iptables -t mangle -A MIHOMO -p udp -j TPROXY --on-port 7893 --tproxy-mark 233&#xA;&#xA;# Hook into the interface&#xA;iptables -t mangle -A PREROUTING -i tailscale0 -j MIHOMO&#xA;&#xA;# Routing table&#xA;echo &#34;233 mihomo&#34; | tee -a /etc/iproute2/rt_tables&#xA;ip rule add fwmark 233 lookup mihomo&#xA;ip route add local 0.0.0.0/0 dev lo table mihomo&#xA;&#xA;# IPv6&#xA;# Create chain&#xA;ip6tables -t mangle -N MIHOMO6&#xA;&#xA;# Skip local addresses&#xA;ip6tables -t mangle -A MIHOMO6 -d ::1/128 -j RETURN&#xA;ip6tables -t mangle -A MIHOMO6 -d fd7a:115c:a1e0::/48 -j RETURN&#xA;&#xA;# Mark TCP/UDP&#xA;ip6tables -t mangle -A MIHOMO6 -i tailscale0 -p tcp -j TPROXY --on-port 7893 --tproxy-mark 233&#xA;ip6tables -t mangle -A MIHOMO6 -i tailscale0 -p udp -j TPROXY --on-port 7893 --tproxy-mark 233&#xA;&#xA;# Interface hook&#xA;ip6tables -t mangle -A PREROUTING -i tailscale0 -j MIHOMO6&#xA;&#xA;# Routing table&#xA;echo &#34;233 mihomo&#34; | tee -a /etc/iproute2/rt_tables&#xA;ip -6 rule add fwmark 233 lookup mihomo&#xA;ip -6 route add local ::/0 dev lo table mihomo&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Most modern distributions do not persist firewall rules by default. For details about rule persistence, see the “Routing Rule Persistence” and “iptables Rule Persistence” sections referenced in my Tailscale article.&lt;/p&gt;&#xA;&lt;h3 id=&#34;how-do-i-configure-local-proxying&#34;&gt;How Do I Configure Local Proxying?&#xA;&lt;/h3&gt;&lt;p&gt;Most proxy clients, such as the default for ShellCrash, use REDIRECT as standard, which is usually sufficient for most use cases. Example for REDIRECT:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;# IPv4, intercept eth0&#xA;iptables -t nat -A PREROUTING -i eth0 -p tcp -j REDIRECT --to-ports 7892&#xA;&#xA;# IPv6, intercept eth0&#xA;ip6tables -t nat -A PREROUTING -i eth0 -p tcp -j REDIRECT --to-ports 7892&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here, 7892 should match the &lt;code&gt;redir-port&lt;/code&gt; in your Mihomo config, and &lt;code&gt;eth0&lt;/code&gt; is the interface you want to intercept. Compared to TPROXY, this is remarkably straightforward.&lt;/p&gt;&#xA;&lt;p&gt;Personally, I dislike forcing all local traffic through the firewall proxy route. Instead, I prefer to use environment variables, &lt;code&gt;proxychains&lt;/code&gt;, or per-application proxy settings as needed to route traffic through Mihomo. For example, if you want Docker to use the proxy, you only need to edit Docker&amp;rsquo;s &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt; and specify the proxy endpoints, rather than intercepting all network traffic at the interface level:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{&#xA;  &#34;proxies&#34;: {&#xA;    &#34;http-proxy&#34;: &#34;http://127.0.0.1:7890&#34;,&#xA;    &#34;https-proxy&#34;: &#34;http://127.0.0.1:7890&#34;,&#xA;    &#34;no-proxy&#34;: &#34;127.0.0.0/8&#34;&#xA;  }&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;why-go-to-all-this-trouble-isnt-using-a-gui-client-easier&#34;&gt;Why Go to All This Trouble? Isn’t Using a GUI Client Easier?&#xA;&lt;/h2&gt;&lt;p&gt;Don’t ask. Some of us just prefer living in the terminal void.&lt;/p&gt;&#xA;</description>
        </item><item>
            <title>The World Is More Foolish Than You Think: A Brief Look at BNPL</title>
            <link>https://blog.l3zc.com/en/2025/06/on-buy-now-pay-later/</link>
            <pubDate>Thu, 05 Jun 2025 19:54:56 +0800</pubDate>
            <guid>https://blog.l3zc.com/en/2025/06/on-buy-now-pay-later/</guid>
            <description>&lt;img src=&#34;https://blog.l3zc.com/2025/06/on-buy-now-pay-later/mobile-payments-1920x720_hu_4cbcee30d76215b2.webp&#34; alt=&#34;Featured image of post The World Is More Foolish Than You Think: A Brief Look at BNPL&#34; /&gt;&lt;p&gt;When I first saw headlines such as “Americans using ‘buy now, pay later’ plans to buy groceries, survey finds,” my instinctive reaction was: isn’t this just business as usual? Only after reading the article did it strike me—this world is dafter than even my most cynical estimates.&lt;/p&gt;&#xA;&lt;p&gt;As a financial innovation, Buy Now, Pay Later (BNPL) certainly holds a certain appeal. It allows consumers to acquire goods and services immediately, with the payment split into later instalments—a model that’s taken the worlds of e-commerce, travel and even dining by storm in recent years.&lt;/p&gt;&#xA;&lt;p&gt;But as always, there’s no such thing as a free lunch. In the end, the cost falls on consumers. In order to make a profit, BNPL providers rely on two main avenues: charging merchants fees higher than those of traditional payment services, and slapping late fees on delinquent customers. At its core, BNPL exploits a psychological trap—minimising the “pain” of paying in the moment, thus encouraging more spending. This is precisely why so many merchants are eager to shoulder those higher fees for BNPL integration.&lt;/p&gt;&#xA;&lt;p&gt;To be clear, I have nothing against BNPL—I’m an avid user myself. My phone was purchased through a 24-month interest-free instalment plan with JD.com; at the moment, I’m only six payments in. Most of my day-to-day expenses go through Huabei (a Chinese BNPL solution) and are automatically settled from my savings or bank account. When a platform truly provides a genuine interest-free or “zero-fee” offer, BNPL becomes an opportunity for free leverage—it’s a costless way to put someone else’s money to work on your behalf. In simple terms: the financial provider pays for your consumption, letting your own cash sit untouched, potentially earning a modest return elsewhere. Financial efficiency, maximised.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;However, there are three absolutely critical prerequisites:&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;Pay on time, every time—no excuses;&lt;/strong&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Never fall into habitual overspending;&lt;/strong&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Maintain at least basic financial literacy and management skills.&lt;/strong&gt;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;Contrast this with what the headlines are pointing out—&lt;strong&gt;“Americans using BNPL for groceries.”&lt;/strong&gt; Skimming the survey data, I saw that &lt;strong&gt;one third of BNPL users have missed at least one repayment?? Some even defaulted on takeaway orders??&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;The ideal is promising, but reality bites hard. Data shows that &lt;strong&gt;the number of people with poor financial self-management is greater than I had imagined.&lt;/strong&gt; 46% of BNPL users already carry existing credit card debt, and among them, 28% are aged just 18-24&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;. This demographic typically has weaker credit, with 63% juggling loans from multiple BNPL platforms simultaneously&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;—a glaring sign of questionable spending habits.&lt;/p&gt;&#xA;&lt;p&gt;What’s even more concerning is how BNPL, by design, appears particularly seductive to individuals with poor financial discipline. Compared to credit cards, approval is instant and the credit barriers are low, all the while trumpeting “interest-free instalments.” For many, BNPL feels like an inexhaustible source of easy money—buy what you want today for a small upfront payment, and worry about the rest later. But, needless to say, the world does not work that way.&lt;/p&gt;&#xA;&lt;p&gt;Inevitably, the day of reckoning comes. As bills mount, many are shocked by the size of their accumulated debt. Missed payments lead to late fees and penalties, which quickly snowball. Add on steep merchant charges, and it’s little wonder BNPL providers are making a killing.&lt;/p&gt;&#xA;&lt;p&gt;To reiterate: I am not against BNPL. As a free financial lever, it’s simply common sense to take advantage of it. For instance, when I bought a £429 phone, I could have paid it off in full straight away, but I opted for a 24-month interest-free plan. The point is, I am fully aware that I have advanced the total purchase price, not merely spent £17.87 per month. This is reflected in my accounts as an immediate reduction in assets, so my net worth accurately includes the liability. Live within your means, avoid piling on unnecessary debt—or, worse, layering leverage upon leverage. Even if idle funds only generate minimal returns in a savings or low-risk investment account, this keeps my finances healthy and ensures I’ll never default. While a modest bit of borrowing can enhance quality of life, solid financial habits are always more prudent when your income isn’t rock-solid.&lt;/p&gt;&#xA;&lt;p&gt;Still, such warnings will do little to sway those long accustomed to unhealthy spending. Even without the alluring convenience of BNPL, these individuals would almost certainly max out their credit cards and then flounder, sinking beneath 20% APRs.&lt;/p&gt;&#xA;&lt;p&gt;The problem isn’t BNPL itself (it’s just another tool), but the underlying disconnect: fragile finances on one side and runaway consumerism on the other. For those who lack self-control, underestimate financial risk, or whose circumstances are already precarious, any easily accessible credit—whether credit cards, BNPL, or the far more dangerous payday loans—serves only as a different route to the same pit of debt. BNPL’s particular twist is its ease of access and immediate gratification, meaning those on the edge tumble even faster—and crash that much sooner.&lt;/p&gt;&#xA;&lt;p&gt;A sizeable portion of BNPL delinquents are already mired in credit card debt. This suggests a robbing-Peter-to-pay-Paul cycle, borrowing from one source to stave off another, ultimately piling interest upon fees and sinking into ever-deepening crisis. Even if BNPL vanished tomorrow, these vulnerable consumers would likely stumble into other financial traps, whether it’s payday loans or some new “convenient” scheme. In the haste to stave off disaster, some might even resort to pawning off family heirlooms.&lt;/p&gt;&#xA;&lt;p&gt;So the real concern isn’t any one financial product, but the underlying structural malaise: economic pressures, the omnipresence of consumerism, inadequate financial education, and the widespread prioritisation of instant gratification over long-term planning. The rise of BNPL merely makes these chronic issues more visible—sharper and harder to ignore.&lt;/p&gt;&#xA;&lt;p&gt;In the end, if we truly want to “save” these individuals, simply restricting access to financial products (be it BNPL or credit cards) is merely treating symptoms, not causes. The real solution—if there is one—lies in tackling the roots: boosting earnings, improving financial literacy, and reshaping spending norms. But these foundational changes are far harder than wishing BNPL companies would simply shut up shop.&lt;/p&gt;&#xA;&lt;p&gt;One last piece of advice: borrowed money always needs to be repaid; don’t make needless offerings to financial middlemen.&lt;/p&gt;&#xA;&lt;p&gt;Alas, all told—the world really is more foolish than I had ever imagined.&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;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;Source: &lt;a class=&#34;link&#34; href=&#34;https://archive.is/T3vGo&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;CFPB Research Reveals Heavy Buy Now, Pay Later Use Among Borrowers with High Credit Balances and Multiple Pay-in-Four Loans&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;Source: &lt;a class=&#34;link&#34; href=&#34;https://archive.is/6HVCl&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;Buy now, pay later users pile on debt, CFPB finds&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;Cover image source: &lt;a class=&#34;link&#34; href=&#34;https://www.visa.com.tw/pay-with-visa/featured-technologies/mobile-payments.html&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;https://www.visa.com.tw/pay-with-visa/featured-technologies/mobile-payments.html&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>Quick Review of the MECHREVO Aurora X Pro</title>
            <link>https://blog.l3zc.com/en/2025/05/new-laptop-briefing/</link>
            <pubDate>Sun, 04 May 2025 23:18:43 +0800</pubDate>
            <guid>https://blog.l3zc.com/en/2025/05/new-laptop-briefing/</guid>
            <description>&lt;img src=&#34;https://blog.l3zc.com/2025/05/new-laptop-briefing/DSCF0267_hu_9e18f49bd8ce081.webp&#34; alt=&#34;Featured image of post Quick Review of the MECHREVO Aurora X Pro&#34; /&gt;&lt;h2 id=&#34;disappointing-right-out-of-the-box&#34;&gt;Disappointing Right Out of the Box&#xA;&lt;/h2&gt;&lt;p&gt;From the moment I placed the order for the MECHREVO Aurora X Pro, I was quite looking forward to it. However, the courier didn’t arrive before my trip to Hong Kong, and by the time I got home and unboxed it, the novelty had already worn off. When I tried to install another SSD, I found the M.2 screw was stripped, so I had to make do with some electrical tape—my fondness for the new laptop instantly halved.&lt;/p&gt;&#xA;&lt;h2 id=&#34;specs-and-price&#34;&gt;Specs and Price&#xA;&lt;/h2&gt;&lt;p&gt;i9-14900HX + RTX 5070 Ti, and at 8,699 yuan after subsidy, the price is fairly reasonable. As for peripherals, to put it simply, I’ve happily used a Hasee before, so there’s really nothing I can’t get on with.&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Screen&lt;/strong&gt;: 2560x1600@300Hz, 100% sRGB colour gamut, with decent colour accuracy. When playing &lt;em&gt;Cities: Skylines 2&lt;/em&gt;, the red of the brake lights in traffic jams is even more vivid than in real life.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Keyboard&lt;/strong&gt;: The key travel is rather short and the feel is average, but since I mostly use an external keyboard, I don’t really mind.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Wi-Fi Card&lt;/strong&gt;: The AX201 is rather stingy, and the antenna design seems poor—you can clearly tell the speed is much slower than wired, even when right next to the router.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;SSD&lt;/strong&gt;: Comes with a 1TB Zhiti drive, which is average; the 2TB Crucial I added myself is the main workhorse.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Battery&lt;/strong&gt;: The 80Wh battery is fine for emergencies on a gaming laptop. I usually keep it in workstation mode to prolong battery life.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Discrete GPU Direct Connection&lt;/strong&gt;: Supports hot switching, which is great—no need to reboot to change modes.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;BIOS&lt;/strong&gt;: The AMI BIOS has a rather weird GUI, which is still much better than my previous Hasee Laptop.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;user-experience&#34;&gt;User Experience&#xA;&lt;/h2&gt;&lt;p&gt;&lt;em&gt;Cities: Skylines 2&lt;/em&gt;—even with a population of 120,000, simulation speed could still be kept at 3x, though the fans were roaring by then. &lt;em&gt;Cyberpunk 2077&lt;/em&gt; runs with ray tracing on Ultra, and &lt;em&gt;Forza Horizon 5&lt;/em&gt; also runs at max setting without issue—but honestly, the actual gaming experience feels much the same as with my old RTX 3060. Sure, reflections are more realistic and details richer, but just for the sake of improved visuals, I doubt I’d spend more time on these games that have already kept me entertained for over a hundred hours.&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/05/new-laptop-briefing/image_hu_8774b59db0175e9a.webp&#34; alt=&#34;Simulation speed in Cities: Skylines 2&#34; /&gt;&#xA; &#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/05/new-laptop-briefing/image-1_hu_47a3feef604ad189.webp&#34; alt=&#34;Honestly, I can’t feel any difference&#34; /&gt;&#xA; &#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/05/new-laptop-briefing/DSCF0267_hu_9e18f49bd8ce081.webp&#34; alt=&#34;It just sits there, like a mute brick&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;The skin-like coating on the palm rest does feel nice, but with peripherals connected, I’m mostly just touching the external keyboard. Nahimic audio effects have serious latency—turning them off actually makes osu! more responsive, a textbook case of negative optimisation.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-meaning-of-tools&#34;&gt;The Meaning of Tools&#xA;&lt;/h2&gt;&lt;p&gt;&#xA;&lt;img src=&#34;https://blog.l3zc.com/2025/05/new-laptop-briefing/DSCF0273_hu_e62683739afe024c.webp&#34; alt=&#34;Desktop after switching to the new laptop&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;I’ve used this laptop for two weeks—no surprises, good or bad. The Wi-Fi card is so rubbish I’ve had to use wired, the fan is extremely loud in turbo mode, and the southbridge gets very hot due to lack of cooling. Apart from that, it’s just a machine that lights up when it should and makes noise when it must.&lt;/p&gt;&#xA;&lt;p&gt;Perhaps that’s how good tools should be: as I write this, I realise I’ve been staring blankly at the battery icon in the bottom right corner for five minutes, and it’s quietly holding at 98%—just sitting there, like a mute brick.&lt;/p&gt;&#xA;</description>
        </item><item>
            <title>From Beginner to Advanced: Remote Networking and Internet Access with Tailscale &#43; ShellCrash</title>
            <link>https://blog.l3zc.com/en/2025/04/tailscale-setup-recap/</link>
            <pubDate>Mon, 14 Apr 2025 18:10:25 +0800</pubDate>
            <guid>https://blog.l3zc.com/en/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 From Beginner to Advanced: Remote Networking and Internet Access with Tailscale + ShellCrash&#34; /&gt;&lt;p&gt;During several trips, I attempted to upload newly taken photos to my Immich server at home in Changsha. I used to rely on Cloudflare Tunnel for reverse proxying, but due to the particularities of the Chinese network environment, slow connections, frequent disconnections, and failed uploads became the norm. Later, I started experimenting with Tailscale — a NAT traversal tool based on WireGuard — and it finally allowed me to stably and quickly access my home NAS and photo library from anywhere in China.&lt;/p&gt;&#xA;&lt;p&gt;However, a new problem emerged: when Tailscale runs on Android, it needs to take over the system traffic as a VPN service. Meanwhile, my usual tool Clash also relies on the VPN interface for traffic routing. Due to Android&amp;rsquo;s limitation of allowing only one VPN service at a time, the two cannot coexist. This meant I had to choose between &amp;ldquo;accessing home services&amp;rdquo; and &amp;ldquo;accessing the open internet&amp;rdquo;.&lt;/p&gt;&#xA;&lt;p&gt;This article walks through the principles of Tailscale, how to set up your own DERP server to improve connection quality, and how to hijack traffic from a Tailscale Exit Node using iptables and forward it to ShellCrash, enabling flexible traffic routing and secure tunnelling. Whether you&amp;rsquo;re looking to access your home NAS or photo library remotely, or protect your data on untrusted networks, this guide offers a practical and stable solution.&lt;/p&gt;&#xA;&lt;h2 id=&#34;what-is-tailscale&#34;&gt;What is Tailscale?&#xA;&lt;/h2&gt;&lt;p&gt;Tailscale is a zero-config virtual private network (VPN) tool based on the WireGuard protocol. It allows devices located in different network environments to communicate as if they were on the same secure local network. By automatically traversing NATs and firewalls, Tailscale enables access to your home NAS, personal servers, development environments, and other internal resources without requiring a public IP or port forwarding. Its key strengths lie in its simplicity, security, and stability — it&amp;rsquo;s ready to use out of the box, encrypts all traffic end-to-end, and is suitable for developers, remote workers, and home users alike.&lt;/p&gt;&#xA;&lt;p&gt;Tailscale&amp;rsquo;s technical implementation is quite ingenious. It builds on the WireGuard encryption protocol but reimagines traditional VPN IP allocation. Each device authenticates using SSO/OAuth2 and receives a node key that is permanently bound to its identity. This identity-based networking model allows a &amp;ldquo;NAS in Changsha&amp;rdquo; and a &amp;ldquo;phone in Hong Kong&amp;rdquo; to communicate as if they were coworkers in the same office.&lt;/p&gt;&#xA;&lt;h2 id=&#34;how-tailscale-establishes-connections&#34;&gt;How Tailscale Establishes Connections&#xA;&lt;/h2&gt;&lt;h3 id=&#34;control-server&#34;&gt;Control Server&#xA;&lt;/h3&gt;&lt;p&gt;When a Tailscale client starts, it first connects to the control server (controlplane) to authenticate and fetch information about other nodes in the network, including each device’s public IP, port, NAT type, etc. This step is essentially the &amp;ldquo;getting to know your peers&amp;rdquo; phase.&#xA;The control server does not relay any traffic — it only coordinates connections, acting like a dispatcher.&lt;/p&gt;&#xA;&lt;h3 id=&#34;derp-servers&#34;&gt;DERP Servers&#xA;&lt;/h3&gt;&lt;p&gt;A key factor behind Tailscale’s high connection success rate is its custom relay protocol called DERP. In Tailscale’s architecture, DERP (Designated Encrypted Relay for Packets) is a crucial component that only steps in when needed. In simple terms, it’s an encrypted HTTP-based relay server that acts as an intermediary when two devices cannot connect directly.&#xA;All clients initially connect via DERP (relay mode), meaning the connection is established instantly with no waiting. Then, both sides begin path discovery in parallel, and within a few seconds, Tailscale typically finds a better route and transparently upgrades the connection to a direct peer-to-peer tunnel.&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;Important notes:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;All traffic relayed via DERP is end-to-end encrypted, so DERP servers cannot see the content;&lt;/li&gt;&#xA;&lt;li&gt;Tailscale uses DERP only when necessary — once a direct connection is established, it automatically switches;&lt;/li&gt;&#xA;&lt;li&gt;Official DERP nodes are distributed globally, and clients automatically select the one with the lowest latency;&lt;/li&gt;&#xA;&lt;li&gt;You can also deploy your own DERP server (for example, within China) to improve latency and reliability.&#xA;Think of DERP as a fallback mechanism — although it’s not as performant as direct connections, it ensures devices can always stay connected even when NAT traversal fails.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 id=&#34;nat-traversal&#34;&gt;NAT Traversal&#xA;&lt;/h3&gt;&lt;p&gt;After obtaining the peer’s address info, Tailscale attempts to establish a peer-to-peer (P2P) connection via NAT traversal. This process uses the STUN protocol, where both sides send probe packets to try to punch a hole through the NAT router and create a direct UDP tunnel. If the network conditions allow, a direct tunnel is established, offering fast speeds and low latency.&lt;/p&gt;&#xA;&lt;p&gt;Due to space constraints, we won’t go into detail about NAT traversal here. For more information, refer to Tailscale’s official article “&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;full-connection-flow-diagram&#34;&gt;Full Connection Flow Diagram&#xA;&lt;/h3&gt;&lt;pre&gt;&lt;code class=&#34;language-mermaid&#34;&gt;flowchart TD&#xA;    A[Device A starts Tailscale] --&gt; B[Initial connection via DERP server]&#xA;    B --&gt; C[Exchange network info and WireGuard keys]&#xA;    C --&gt; D[Both sides perform NAT type detection]&#xA;    D --&gt; E{Direct connection possible?}&#xA;    E -- Yes --&gt; F[Establish P2P tunnel]&#xA;    F --&gt; G[Periodically check connection quality]&#xA;    G --&gt; H{Is P2P better than DERP?}&#xA;    H -- Yes --&gt; I[Switch most traffic to P2P]&#xA;    H -- No --&gt; J[Continue relaying traffic via DERP]&#xA;    E -- No --&gt; J&#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;hosting-your-own-derp-server&#34;&gt;Hosting Your Own DERP Server&#xA;&lt;/h2&gt;&lt;p&gt;Tailscale installation is straightforward across platforms, and the official documentation covers it in detail. This article assumes you have already installed and logged into Tailscale on the relevant devices.&lt;/p&gt;&#xA;&lt;h3 id=&#34;why-host-your-own-derp&#34;&gt;Why Host Your Own DERP?&#xA;&lt;/h3&gt;&lt;p&gt;Tailscale has deployed numerous DERP relay servers globally&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;. However, for well-known reasons, there are no official DERP nodes within mainland China. This leads to the following issues:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;When NAT traversal fails, all traffic must be routed via overseas DERP nodes, resulting in high latency and poor performance;&lt;/li&gt;&#xA;&lt;li&gt;Some official DERP nodes may be disrupted by the Great Firewall (GFW), causing connection drops or handshake failures;&lt;/li&gt;&#xA;&lt;li&gt;Even when direct connections succeed, Tailscale still relies on DERP for exchanging route info and WireGuard keys — if DERP is unreachable, connection quality suffers.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Therefore, in the Chinese network environment, hosting a local DERP node can significantly improve connection stability and performance, while also avoiding unexpected issues caused by network restrictions — making it a worthwhile optimisation.&lt;/p&gt;&#xA;&lt;h3 id=&#34;prerequisites&#34;&gt;Prerequisites&#xA;&lt;/h3&gt;&lt;p&gt;As DERP is HTTP-based, you’ll need an HTTP reverse proxy and an SSL certificate. This guide uses Docker for deployment. Before proceeding, ensure your server has Docker and Docker Compose installed. You’ll also need basic knowledge of editors like &lt;code&gt;nano&lt;/code&gt; to modify config files. For optimal results, your server should have a static public IPv4+IPv6 dual-stack address.&#xA;If all requirements are met, you can begin deployment:&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;The &lt;code&gt;network_mode: host&lt;/code&gt; setting is crucial. It allows the container to share the host&amp;rsquo;s network stack. If Docker’s default &lt;code&gt;bridge&lt;/code&gt; mode is used, the container’s network will be NATed, and the DERP STUN service will detect a &lt;code&gt;172.17.0.0/16&lt;/code&gt; address instead of the client’s real public IP, causing connection issues.&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;As mentioned earlier, DERP traffic is end-to-end encrypted, and DERP servers do not know who is using them. Without proper access control, anyone who knows your DERP address and port could use it. This volume mounts the Tailscale socket file into the container, allowing the DERP service to authenticate clients via the local &lt;code&gt;tailscaled&lt;/code&gt; service. Combined with &lt;code&gt;DERP_VERIFY_CLIENTS=true&lt;/code&gt;, it prevents freeloaders from using your DERP node.&lt;/p&gt;&#xA;&lt;p&gt;Notes:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Your host must already have the Tailscale client (&lt;code&gt;tailscaled&lt;/code&gt;) installed and logged in, or the socket file won&amp;rsquo;t exist and the container will fail;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;tailscaled&lt;/code&gt; must run with root privileges to create this socket.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 id=&#34;reverse-proxy&#34;&gt;Reverse Proxy&#xA;&lt;/h3&gt;&lt;p&gt;Using Caddy as an example, reverse proxy port 4433 and deploy an SSL certificate:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-caddyfile&#34;&gt;{&#xA;        email webmaster@l3zc.com&#xA;}&#xA;*.l3zc.com {&#xA;        encode gzip&#xA;        tls {&#xA;                dns dnspod APP_ID,APP_KEY&#xA;                resolvers 119.29.29.29 223.5.5.5&#xA;        }&#xA;        @derp host derp.l3zc.com&#xA;        reverse_proxy @derp :4433&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Note: If you&amp;rsquo;re using MagicDNS with Caddy and DNS providers to request a wildcard certificate, you may encounter local certificate validation errors. Setting the &lt;code&gt;resolvers&lt;/code&gt; parameter resolves this issue.&#xA;Visit the proxied node — if you see the following page, the setup is correct:&lt;/p&gt;&#xA;&lt;h3 id=&#34;configuring-acl-policies&#34;&gt;Configuring ACL Policies&#xA;&lt;/h3&gt;&lt;p&gt;Go to the Tailscale admin console&amp;rsquo;s page and add your custom DERP configuration.&lt;/p&gt;&#xA;&lt;p&gt;Tailscale’s ACL policies are written in 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;. To edit in VSCode, set the language mode to “JSON with Comments (jsonc)”. Here&amp;rsquo;s an example configuration:&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;  // Custom DERP configuration&#xA;  &#34;derpMap&#34;: {&#xA;    &#34;OmitDefaultRegions&#34;: false, // Set to true to exclude official DERPs&#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 reserves RegionIDs 1–899 for official nodes. Custom DERPs must use IDs 900 and above.&lt;/p&gt;&#xA;&lt;h3 id=&#34;testing-the-connection&#34;&gt;Testing the Connection&#xA;&lt;/h3&gt;&lt;p&gt;Once you’ve saved your ACL configuration, Tailscale will automatically sync it to all clients. After a short wait, run &lt;code&gt;tailscale netcheck&lt;/code&gt; on a client to test the connection:&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;Testing with tailscale netcheck&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;Pay attention to whether the returned IP is your actual public IP. If it shows an address in the &lt;code&gt;172.17.0.0/16&lt;/code&gt; range, your Docker configuration is incorrect.&lt;/p&gt;&#xA;&lt;h2 id=&#34;coexisting-with-internet-access-hijacking-exit-node-traffic-to-the-clash-engine&#34;&gt;Coexisting with Internet Access: Hijacking Exit Node Traffic to the Clash Engine&#xA;&lt;/h2&gt;&lt;h3 id=&#34;declaring-an-exit-node&#34;&gt;Declaring an Exit Node&#xA;&lt;/h3&gt;&lt;p&gt;Prepare a device at home that stays on 24/7 — this could be a Raspberry Pi or a Mac Mini. Install Tailscale and declare it as an Exit Node. You can also advertise your home LAN subnet if needed. Once enabled in the Tailscale console, this device becomes a free VPN, allowing you to securely access the internet from unfamiliar networks.&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;Find 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;Enable Settings&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;Once broadcasting is complete, you can access all devices on your home network from anywhere in the world, as long as you&amp;rsquo;re connected to the Tailscale network.&lt;/p&gt;&#xA;&lt;h3 id=&#34;enabling-ip-forwarding-and-disabling-udp-gro&#34;&gt;Enabling IP Forwarding and Disabling 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;Enabling IP forwarding is essential for devices like the Raspberry Pi to function as an Exit Node. The following commands are for Raspberry Pi — if you&amp;rsquo;re using a different device, refer to the official Tailscale documentation.&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;According to 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;, disabling 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; can improve forwarding performance. Although the official persistence method may not work on Raspberry Pi, we can configure it manually.&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;# Install ethtool&#xA;sudo apt update &amp;&amp; sudo apt install ethtool -y&#xA;# Disable UDP GRO&#xA;sudo ethtool -K eth0 gro off&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;To persist this setting, create a systemd service:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;# Create the service file&#xA;sudo nano /etc/systemd/system/ethtool.service&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Add the following content:&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;[Service]&#xA;Type=oneshot&#xA;ExecStart=/sbin/ethtool -K eth0 gro off&#xA;[Install]&#xA;WantedBy=multi-user.target&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then enable and start the service:&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;hijacking-traffic-on-the-exit-node&#34;&gt;Hijacking Traffic on the Exit Node&#xA;&lt;/h3&gt;&lt;p&gt;My Exit Node is a Raspberry Pi. After configuring it as an Exit Node, the final step is to hijack the traffic sent from my phone to the Exit Node in order to enable open internet access. For stability reasons, I prefer not to run proxy software directly on the main home router. Therefore, hijacking traffic on the Pi is the only viable solution.&lt;/p&gt;&#xA;&lt;p&gt;First, install ShellCrash. Follow the prompts to import configuration files and set up automation as needed:&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;Start the service and change the firewall mode to “Clean Mode”. I also recommend enabling SNI sniffing and switching the DNS mode from &lt;code&gt;fake-ip&lt;/code&gt; to &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;, and enabling IPv6 transparent proxy.&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;Adjust firewall hijack scope&#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;Enable domain sniffing&#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;Modify port settings&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;Setting Clean Mode allows us to manually configure iptables for more precise traffic hijacking. Use &lt;code&gt;ifconfig&lt;/code&gt; to check the Tailscale interface name — it’s usually &lt;code&gt;tailscale0&lt;/code&gt; by default:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;root@raspberrypi:~# ifconfig&#xA;eth0: ......&#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;        ...&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Hijack all traffic from &lt;code&gt;tailscale0&lt;/code&gt; to the local Clash engine’s listening port &lt;code&gt;7892&lt;/code&gt;. This is called the “static route port” in ShellCrash. Don’t forget to also hijack IPv6 traffic:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;# IPv4: Hijack tailscale0 TCP traffic&#xA;iptables -t nat -A PREROUTING -i tailscale0 -p tcp -j REDIRECT --to-ports 7892&#xA;# IPv6: Hijack tailscale0 TCP traffic&#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;advanced-hijacking-udp-traffic-with-tproxy&#34;&gt;Advanced: Hijacking UDP Traffic with TProxy&#xA;&lt;/h3&gt;&lt;p&gt;The &lt;code&gt;iptables&lt;/code&gt; REDIRECT target can only redirect TCP traffic. Since UDP is a connectionless protocol, REDIRECT cannot retain the original destination address, which prevents transparent proxies from identifying the original destination.&lt;/p&gt;&#xA;&lt;p&gt;So, if you attempt to hijack UDP traffic using a rule like this:&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;This rule will either not work or cause abnormal proxy behaviour.&lt;/p&gt;&#xA;&lt;p&gt;Is there a way to proxy UDP traffic? Yes, indeed. But the prerequisites are:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;The proxy core must support UDP transparent proxying (Clash Premium and Mihomo both support this);&lt;/li&gt;&#xA;&lt;li&gt;You must use TProxy mode instead of REDIRECT;&lt;/li&gt;&#xA;&lt;li&gt;The &lt;code&gt;iptables&lt;/code&gt; mangle table and policy routing must be correctly configured;&lt;/li&gt;&#xA;&lt;li&gt;UDP proxying must be enabled in the proxy configuration file (e.g. &lt;code&gt;mode: rule&lt;/code&gt; and &lt;code&gt;udp: true&lt;/code&gt;).&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Assuming you&amp;rsquo;ve fulfilled the first, second, and last conditions, here is an example&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;# Create a custom chain&#xA;sudo iptables -t mangle -N SHELLCRASH&#xA;&#xA;# Exclude local traffic as needed&#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 TCP and UDP traffic for the proxy&#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;# Interface redirection&#xA;sudo iptables -t mangle -A PREROUTING -i tailscale0 -j SHELLCRASH&#xA;&#xA;# Routing table configuration&#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;And of course, don’t forget about IPv6:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;# Create a chain&#xA;sudo ip6tables -t mangle -N SHELLCRASH6&#xA;&#xA;# Exclude local addresses&#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;# Mark TCP/UDP traffic&#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;# Interface redirection&#xA;sudo ip6tables -t mangle -A PREROUTING -i tailscale0 -j SHELLCRASH6&#xA;&#xA;# Routing table configuration&#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;persisting-routing-rules&#34;&gt;Persisting Routing Rules&#xA;&lt;/h3&gt;&lt;p&gt;Rules created with &lt;code&gt;ip rule&lt;/code&gt; and &lt;code&gt;ip route&lt;/code&gt; are lost after a reboot, so we need to persist them manually. The simplest method is to create a script and add it to &lt;code&gt;crontab&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Create a script:&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;Edit it as follows:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;#!/bin/bash&#xA;&#xA;# IPv4 policy routing&#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 policy routing&#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;After granting execution permissions, edit &lt;code&gt;crontab&lt;/code&gt;:&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;Add the following line at the end of the crontab file:&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;how-it-works-how-does-tproxy-forward-udp-traffic&#34;&gt;How It Works: How Does TProxy Forward UDP Traffic?&#xA;&lt;/h3&gt;&lt;p&gt;If you&amp;rsquo;ve read this far, you might be wondering: why, throughout the entire process, we never specified &lt;code&gt;--to-ports&lt;/code&gt; in &lt;code&gt;iptables&lt;/code&gt;, nor did we see the destination address being modified, yet UDP traffic was somehow successfully proxied? How is that possible?&lt;/p&gt;&#xA;&lt;p&gt;To explain this, let’s first look at the fundamental differences between TProxy and REDIRECT:&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;REDIRECT Mode:&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Uses the &lt;code&gt;iptables&lt;/code&gt; &lt;code&gt;nat&lt;/code&gt; table;&lt;/li&gt;&#xA;&lt;li&gt;Rewrites the destination address to a local one (e.g. &lt;code&gt;127.0.0.1:7892&lt;/code&gt;);&lt;/li&gt;&#xA;&lt;li&gt;Typically used for TCP traffic;&lt;/li&gt;&#xA;&lt;li&gt;Cannot preserve the original destination address;&lt;/li&gt;&#xA;&lt;li&gt;Requires specifying &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[Client device&lt;br&gt;initiates TCP request via Tailscale]&#xA;    B[tailscale0 interface receives traffic]&#xA;    C[iptables NAT PREROUTING&lt;br&gt;REDIRECT --to-ports 7892]&#xA;    D[ShellCrash listens locally&lt;br&gt;on 127.0.0.1:7892]&#xA;    E[ShellCrash initiates new TCP connection&lt;br&gt;→ target server]&#xA;    F[Response returns from the network&lt;br&gt;ShellCrash forwards it]&#xA;    G[Response reaches client device]&#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 Mode:&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Uses the &lt;code&gt;iptables&lt;/code&gt; &lt;code&gt;mangle&lt;/code&gt; table;&lt;/li&gt;&#xA;&lt;li&gt;Does not modify the destination IP, preserving the original target;&lt;/li&gt;&#xA;&lt;li&gt;Uses &lt;code&gt;fwmark&lt;/code&gt; and policy routing to route packets to &lt;code&gt;lo&lt;/code&gt;;&lt;/li&gt;&#xA;&lt;li&gt;The proxy listens on a special port (e.g. 7893) with &lt;code&gt;IP_TRANSPARENT&lt;/code&gt; enabled;&lt;/li&gt;&#xA;&lt;li&gt;Supports both UDP and TCP;&lt;/li&gt;&#xA;&lt;li&gt;No need to specify &lt;code&gt;--to-ports&lt;/code&gt; in &lt;code&gt;iptables&lt;/code&gt; since this is not NAT, but marking + routing.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-mermaid&#34;&gt;flowchart TB&#xA;    A[Client device&lt;br&gt;initiates TCP/UDP request via Tailscale]&#xA;    B[tailscale0 interface receives traffic]&#xA;    C[iptables MANGLE PREROUTING&lt;br&gt;assign fwmark 233]&#xA;    D[ip rule: fwmark 233&lt;br&gt;use routing table shellcrash]&#xA;    E[ip route: local 0.0.0.0/0&lt;br&gt;dev lo table shellcrash]&#xA;    F[ShellCrash listens on lo:7893&lt;br&gt;in IP_TRANSPARENT mode]&#xA;    G[ShellCrash retrieves original destination&lt;br&gt;initiates proxy connection]&#xA;    H[Response returns from the network&lt;br&gt;ShellCrash forwards it]&#xA;    I[Response reaches client device]&#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 does not use DNAT/REDIRECT. Instead, it marks packets using the &lt;code&gt;mangle&lt;/code&gt; table, then uses policy routing (&lt;code&gt;ip rule&lt;/code&gt; + &lt;code&gt;ip route&lt;/code&gt;) to route those packets to the &lt;code&gt;lo&lt;/code&gt; interface. The proxy application (e.g. Clash / ShellCrash) listens on a port on &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;, and with the &lt;code&gt;IP_TRANSPARENT&lt;/code&gt; option enabled, it can read the original destination IP and port from the packet and forward the traffic accordingly.&lt;/p&gt;&#xA;&lt;p&gt;In short, TProxy mode only requires:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;iptables&lt;/code&gt; to mark packets;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;ip rule&lt;/code&gt; + &lt;code&gt;ip route&lt;/code&gt; to route them to &lt;code&gt;lo&lt;/code&gt;;&lt;/li&gt;&#xA;&lt;li&gt;The proxy to listen on &lt;code&gt;lo&lt;/code&gt; with &lt;code&gt;IP_TRANSPARENT&lt;/code&gt; enabled.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Thus, there&amp;rsquo;s no need to specify &lt;code&gt;--to-ports&lt;/code&gt; in &lt;code&gt;iptables&lt;/code&gt;, because the destination IP and port remain unchanged, and the proxy can detect and handle them itself.&lt;/p&gt;&#xA;&lt;h3 id=&#34;persisting-iptables-rules&#34;&gt;Persisting iptables Rules&#xA;&lt;/h3&gt;&lt;p&gt;Install &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;During installation, you&amp;rsquo;ll be prompted to save the current IPv4 and IPv6 rules — select &amp;ldquo;Yes&amp;rdquo;. If you later add new rules, remember to save them:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;# Save current IPv4/IPv6 rules&#xA;sudo netfilter-persistent save&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Saved rules are stored in:&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;You can also edit the &lt;code&gt;rules.v4&lt;/code&gt;/&lt;code&gt;rules.v6&lt;/code&gt; files directly as needed.&lt;/p&gt;&#xA;&lt;h2 id=&#34;final-result&#34;&gt;Final Result&#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;NAT traversal Successful&#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;Overall Speed Is Acceptable&#34; /&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;The performance of this setup largely depends on your home network&amp;rsquo;s upload bandwidth. I have a 500Mbps down / 60Mbps up connection, and I haven’t encountered a single NAT traversal failure so far. Speeds are consistently high, latency is acceptable, and I can securely access my Immich server, OpenWRT router, and other home devices from anywhere with end-to-end encryption — all while enjoying unrestricted internet access.&#xA;Overall, I’m quite satisfied with the solution.&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;Reference: &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;Reference: &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;About 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;Reference: &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;Reference: &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) is a Linux kernel network optimisation that merges small packets to improve efficiency. However, when acting as a forwarding node, this can increase latency and reduce throughput under packet loss conditions.&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;Compared to &lt;code&gt;fake-ip&lt;/code&gt;, &lt;code&gt;redir-host&lt;/code&gt; offers better compatibility, fewer issues, and avoids temporary disconnects caused by fake IP residue when toggling proxy modes. My Pi uses &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;a preconfigured SmartDNS setup&lt;/a&gt; with no DNS pollution, resulting in a smooth experience.&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;Reference: &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; is the default loopback interface in Linux systems. In transparent proxy setups, it not only handles localhost traffic but is also used to receive network connections originally destined for external addresses, enabling local hijacking and forwarding of external traffic.&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>Google Play Store&#39;s CDN in China: From Cryptography Basics to Traffic Routing Optimisation</title>
            <link>https://blog.l3zc.com/en/2025/03/chinese-cdn-used-by-playstore/</link>
            <pubDate>Sat, 15 Mar 2025 21:15:01 +0800</pubDate>
            <guid>https://blog.l3zc.com/en/2025/03/chinese-cdn-used-by-playstore/</guid>
            <description>&lt;img src=&#34;https://blog.l3zc.com/2025/03/chinese-cdn-used-by-playstore/cover_hu_d434a5f79e21c50a.webp&#34; alt=&#34;Featured image of post Google Play Store&#39;s CDN in China: From Cryptography Basics to Traffic Routing Optimisation&#34; /&gt;&lt;p&gt;After switching to my own Mihomo override rules, my phone kept spinning when downloading apps from Google Play, but using the rules from the VPN service worked fine, which was very strange. So, I checked the Mihomo kernel logs.&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-log&#34;&gt;play-lh.googleusercontent.com:443 match RuleSet(cdn_domainset) using Static Resources[🇭🇰 Hong Kong 07]&#xA;play.googleapis.com:443 match GeoSite(GFW) using Node Selection[🇭🇰 Hong Kong 07]&#xA;play-lh.googleusercontent.com:443 match RuleSet(cdn_domainset) using Static Resources[🇭🇰 Hong Kong 07]&#xA;play-fe.googleapis.com:443 match GeoSite(GFW) using Node Selection[🇭🇰 Hong Kong 07]&#xA;services.googleapis.cn:443 match GeoSite(CN) using Direct Connection[DIRECT]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;(The above logs have been simplified.)&lt;/p&gt;&#xA;&lt;p&gt;The Google Play services pre-installed on Chinese smartphones use the domain &lt;code&gt;services.googleapis.cn&lt;/code&gt; instead of &lt;code&gt;services.googleapis.com&lt;/code&gt;, and this domain is set to direct connection in most traffic routing rules. Yes, that&amp;rsquo;s where the problem lies. Modifying the rules to route this domain through a proxy solves the issue!&lt;/p&gt;&#xA;&lt;p&gt;&amp;hellip;Or does it?&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;rr4---sn-j5o7dn7s.xn--ngstr-lra8j.com:443 match Match using Leakage[🇭🇰 Hong Kong 07]&#xA;rr2---sn-j5o7dn7s.xn--ngstr-lra8j.com:443 match Match using Leakage[🇭🇰 Hong Kong 07]&#xA;rr1---sn-j5o76n7z.xn--ngstr-lra8j.com:443 match Match using Leakage[🇭🇰 Hong Kong 07]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Wait, why do these strange domains appear every time I download an app from the Play Store? After some research online, a new world opened up.&lt;/p&gt;&#xA;&lt;h2 id=&#34;ångströ-the-opposite-yet-similar-counterpart-to-google&#34;&gt;Ångströ: The Opposite Yet Similar Counterpart to Google&#xA;&lt;/h2&gt;&lt;p&gt;Seeing &lt;code&gt;xn--ngstr-lra8j.com&lt;/code&gt;, those familiar with domain names will know that this is PunyCode encoding, which decodes to &lt;code&gt;ångströ.com&lt;/code&gt;. Anders Jonas Ångström was a Swedish physicist and a pioneer in spectroscopy.&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; The ångström (Å) is a unit of length named in his honour, where $ 1 Å = 10^{-10} m = \frac{1}{10} nm $. It is commonly used to describe very short distances, such as atomic and molecular sizes or the wavelength of light. It represents an extremely small scale, especially in physics and chemistry for fine measurements.&lt;/p&gt;&#xA;&lt;p&gt;Although Google&amp;rsquo;s name is not directly derived from &amp;ldquo;Googol,&amp;rdquo; it was inspired by the term. &amp;ldquo;Googol&amp;rdquo; is a mathematical term representing $10^{100}$, a 1 followed by 100 zeros, an extremely large number often used to denote vast quantities in computer science.&lt;/p&gt;&#xA;&lt;p&gt;In terms of naming philosophy, Google and Ångströ, two seemingly opposite technological concepts, exhibit a fascinating symmetry. The former originates from the creative adaptation of the mathematical term &amp;ldquo;Googol,&amp;rdquo; while the latter stems from the reimagining of the physical unit &amp;ldquo;Ångström.&amp;rdquo; These artistically transformed names resemble the double helix of technological civilization: Google embodies the ambition to navigate the vast digital universe, while Ångströ hints at the meticulous crafting of atomic-scale technological landscapes. When these creatively altered professional terms meet in Silicon Valley, they form a perfect cognitive coordinate system—pointing to the limits of human information processing while heralding the grand journey of technological civilization towards both the macro and micro scales.&lt;/p&gt;&#xA;&lt;h2 id=&#34;introduction-to-cryptography-the-patterns-of-googles-infrastructure-domains&#34;&gt;Introduction to Cryptography: The Patterns of Google&amp;rsquo;s Infrastructure Domains&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;/h2&gt;&lt;p&gt;OK, enough. Now let&amp;rsquo;s turn our attention back to Google&amp;rsquo;s infrastructure. First, let&amp;rsquo;s look at some complete connection domains:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;rr4---sn-j5o7dn7s.xn--ngstr-lra8j.com&#xA;rr1---sn-j5o76n7z.xn--ngstr-lra8j.com&#xA;rr5---sn-i3b7knzs.xn--ngstr-lra8j.com&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;These seemingly random strings are actually the result of some simple cryptographic encryption. Let&amp;rsquo;s break down the core components.&lt;/p&gt;&#xA;&lt;h3 id=&#34;city-name-conversion-rules&#34;&gt;City Name Conversion Rules&#xA;&lt;/h3&gt;&lt;p&gt;Taking &lt;code&gt;rr1---sn-j5o76n7z&lt;/code&gt; as an example, the key information segment is the 8 characters after &lt;code&gt;sn-&lt;/code&gt;: &lt;code&gt;r1---sn-[123][45][6][78]&lt;/code&gt;. The first three characters, in this case &lt;code&gt;j5o&lt;/code&gt;, represent the city name, derived from the IATA code of the city&amp;rsquo;s main airport through a cryptographic transformation.&lt;/p&gt;&#xA;&lt;p&gt;First, construct a 5 * 7 alphanumeric table:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;Row0: 1 0 2 3 4&#xA;Row1: 5 6 7 8 9&#xA;Row2: a b c d e&#xA;Row3: f g h i j&#xA;Row4: k l m n o&#xA;Row5: p q r s t&#xA;Row6: u v w x y&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Rotate this table counterclockwise, starting from the bottom-left corner of the new table, and copy the data from the original table in the order &amp;ldquo;left to right, top to bottom&amp;rdquo; into the new table in the order &amp;ldquo;bottom to top, left to right.&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;After rotation, we get the following table:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;| 6 d k r y |&#xA;| --------- |&#xA;| 5 c j q x |&#xA;| 4 b i p w |&#xA;| 3 a h o v |&#xA;| 2 9 g n u |&#xA;| 0 8 f m t |&#xA;| --------- |&#xA;| 1 7 e l s |&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The 5*5 section in the middle of the table, enclosed by lines, is the final cipher table, containing 25 characters representing letters &lt;code&gt;a&lt;/code&gt; to &lt;code&gt;y&lt;/code&gt; in order from left to right, top to bottom. For example, the IATA code for Shanghai Hongqiao International Airport is &lt;code&gt;sha&lt;/code&gt;. We can use this table to get the encrypted ciphertext:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;s&lt;/code&gt; is the 19th letter in the alphabet. Counting from left to right, top to bottom in the cipher table, the 19th character is &lt;code&gt;n&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;h&lt;/code&gt; is the 8th letter in the alphabet. Counting from left to right, top to bottom in the cipher table, the 8th character is &lt;code&gt;i&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;a&lt;/code&gt; is the 1st letter in the alphabet. Counting from left to right, top to bottom in the cipher table, the 1st character is &lt;code&gt;5&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;The encrypted ciphertext is &lt;code&gt;ni5&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Conversely, going back to the original city name &lt;code&gt;j5o&lt;/code&gt;, we can reverse-engineer the plaintext from the cipher table:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;j&lt;/code&gt; is the 3rd character in the cipher table, which corresponds to &lt;code&gt;c&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;5&lt;/code&gt; is the 1st character in the cipher table, which corresponds to &lt;code&gt;a&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;o&lt;/code&gt; is the 14th letter in the alphabet, which corresponds to &lt;code&gt;n&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;The decrypted plaintext is &lt;code&gt;can&lt;/code&gt;, which is the IATA code for Guangzhou Baiyun International Airport. Clearly, the server we connected to is located in Guangzhou.&lt;/p&gt;&#xA;&lt;p&gt;What if Google had a server in Zurich, and Zurich Airport&amp;rsquo;s IATA code is &lt;code&gt;zrh&lt;/code&gt;, which includes the letter &lt;code&gt;z&lt;/code&gt;? Our cipher table only goes up to &lt;code&gt;y&lt;/code&gt;. Don&amp;rsquo;t worry, Google has considered this issue—&lt;code&gt;z&lt;/code&gt; corresponds to &lt;code&gt;1&lt;/code&gt; in the cipher table.&lt;/p&gt;&#xA;&lt;p&gt;Here&amp;rsquo;s the code representation of these rules:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;table = &#34;5cjqx4bipw3ahov29gnu08fmt1&#34;&#xA;def iata2cipher(iata):&#xA;    global table&#xA;    iata = iata.upper()&#xA;    cipher = &#34;&#34;&#xA;    for element in iata:&#xA;        index = ord(element) - 65&#xA;        cipher += table[index]&#xA;    return cipher&#xA;&#xA;def cipher2iata(cipher):&#xA;    global table&#xA;    cipher = cipher.lower()&#xA;    iata = &#34;&#34;&#xA;    for element in cipher:&#xA;        index = table.find(element)&#xA;        iata += chr(65 + index)&#xA;    return iata&#xA;&#xA;# print(iata2cipher(input(&#34;IATA:&#34;)))&#xA;# print(cipher2iata(input(&#34;Cipher:&#34;)))&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;server-group-number&#34;&gt;Server Group Number&#xA;&lt;/h3&gt;&lt;p&gt;The [45] and [78] positions, such as &lt;code&gt;76&lt;/code&gt; and &lt;code&gt;7z&lt;/code&gt;, represent the server group (access point) number, composed of characters from the first column of the following table (separated by a line). The characters &lt;code&gt;7elsz6dkry&lt;/code&gt; represent &lt;code&gt;0123456789&lt;/code&gt;. Therefore, &lt;code&gt;76&lt;/code&gt; translates to &lt;code&gt;05&lt;/code&gt;, and &lt;code&gt;7z&lt;/code&gt; translates to &lt;code&gt;04&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;  | 1 2 3 4 5 6&#xA;7 | 8 9 a b c d&#xA;e | f g h i j k&#xA;l | m n o p q r&#xA;s | t u v w x y&#xA;z | 0 1 2 3 4 5&#xA;6 | 7 8 9 a b c&#xA;d | e f g h i j&#xA;k | l m n o p q&#xA;r | s t u v w x&#xA;y | z&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here&amp;rsquo;s the Python representation:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;codeTable = &#34;7elsz6dkry&#34;&#xA;def cipher2code(cipher):&#xA;    global codeTable&#xA;    cipher = cipher.lower()&#xA;    codeString = &#34;&#34;&#xA;    for element in cipher:&#xA;        index = codeTable.find(element)&#xA;        codeString += chr(index + 48)&#xA;    return codeString&#xA;&#xA;# print(cipher2code(input(&#34;Cipher:&#34;)))&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Similarly, encrypting numbers into ciphertext follows the same logic, which we won&amp;rsquo;t elaborate on here.&lt;/p&gt;&#xA;&lt;h3 id=&#34;supported-protocols&#34;&gt;Supported Protocols&#xA;&lt;/h3&gt;&lt;p&gt;The [6] position indicates network protocol information:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;n&lt;/code&gt;: IPv6 (address range 0x000-0x3FF)&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;u&lt;/code&gt;: IPv6 (address range 0x400-0x7FF)&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;m&lt;/code&gt;: IPv4 only&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;For example:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;a5mekn7r&lt;/code&gt;, IPv6 prefix: &lt;code&gt;2607:f8b0:4007:a::/64&lt;/code&gt;, IPv4 prefix: &lt;code&gt;74.125.103.0/24&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;a5m7zu7r&lt;/code&gt;, IPv6 prefix: &lt;code&gt;2607:f8b0:4007:407::/64&lt;/code&gt;, IPv4 prefix: &lt;code&gt;74.125.215.0/24&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;a5mekm76&lt;/code&gt;, IPv4 only, prefix: &lt;code&gt;208.117.242.0/24&lt;/code&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;correct-traffic-routing&#34;&gt;Correct Traffic Routing&#xA;&lt;/h2&gt;&lt;p&gt;Understanding the cryptographic patterns of Google&amp;rsquo;s infrastructure domains allows us to implement more precise traffic routing strategies. Currently, it&amp;rsquo;s known that Google&amp;rsquo;s CDN nodes in China are mainly located in Beijing (&lt;code&gt;2x3&lt;/code&gt;), Shanghai (&lt;code&gt;ni5&lt;/code&gt;), and Guangzhou (&lt;code&gt;j5o&lt;/code&gt;). The corresponding domain characteristics can be identified using regular expressions:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;rules:&#xA;  - DOMAIN-REGEX,^r+[0-9]+(---|\.)sn-(2x3|ni5|j5o)\w{5}\.xn--ngstr-lra8j\.com$,DIRECT&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This rule will match all domains like &lt;code&gt;rr1---sn-j5o76n7z.xn--ngstr-lra8j.com&lt;/code&gt; that belong to domestic CDNs and mark them for direct connection. For other Google domains not covered (such as &lt;code&gt;play.googleapis.com&lt;/code&gt;), the original proxy rules will still apply.&lt;/p&gt;&#xA;&lt;p&gt;In fact, the upstream GeoSite database &lt;a class=&#34;link&#34; href=&#34;https://github.com/v2fly/domain-list-community&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;v2fly/domain-list-community&lt;/a&gt; has already optimised the rules for Google Play&amp;rsquo;s domestic CDN nodes&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;. Simply enabling the &lt;code&gt;GEOSITE,GOOGLE-PLAY@CN&lt;/code&gt; rule in Mihomo&amp;rsquo;s configuration will automatically implement intelligent traffic routing for domestic CDN direct connections and overseas domain proxies:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;rules:&#xA;  - GEOSITE,GOOGLE-PLAY@CN,Direct Connection&#xA;  - GEOSITE,GOOGLE,Proxy Selection&lt;/code&gt;&lt;/pre&gt;&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;Reference: &lt;a class=&#34;link&#34; href=&#34;https://en.wikipedia.org/wiki/Anders_Jonas_%C3%85ngstr%C3%B6m&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;https://en.wikipedia.org/wiki/Anders_Jonas_%C3%85ngstr%C3%B6m&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;Reference: &lt;a class=&#34;link&#34; href=&#34;https://github.com/lennylxx/ipv6-hosts/wiki/sn-domains&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;https://github.com/lennylxx/ipv6-hosts/wiki/sn-domains&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;Specific commit: &lt;a class=&#34;link&#34; href=&#34;https://github.com/v2fly/domain-list-community/pull/2436/commits/a86c1bf3d9bf577869180874d87c76ddf6282fc1&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;https://github.com/v2fly/domain-list-community/pull/2436/commits/a86c1bf3d9bf577869180874d87c76ddf6282fc1&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></channel>
</rss>
