Why TUN plus systemd on Ubuntu
Compared with “application proxy” workflows that rely on HTTP_PROXY environment variables, TUN mode is closer to what people mean by transparent proxy on Linux: programs do not need to cooperate, and the steering happens once traffic is inside the tunnel interface. That is powerful for browsers, CLI tools, language package managers, and anything that ignores environment variables. The tradeoff is privilege: creating a TUN device and installing routes typically requires capabilities that normal users do not have, and the failure modes show up as kernel-level errors rather than a polite popup.
systemd matters because a long-running proxy is an operational problem, not a one-shot command. You want a fixed WorkingDirectory, a consistent ExecStart, restart behavior, and logs that land in the journal so you can correlate crashes with network changes. On a server, you also want the service to come back after reboot without a logged-in desktop session. If you want the conceptual background first, read Clash TUN mode deep dive; this article stays focused on Ubuntu filesystem conventions, permissions, and a service unit you can adapt.
Terminology: this guide assumes a Mihomo / Clash Meta-class binary (often named mihomo). Exact YAML keys evolve between releases; treat the snippets as patterns and confirm against your core version’s documentation.
Also remember that Ubuntu stacks often combine netplan, NetworkManager, and systemd-resolved. When something “almost works,” compare a foreground run and a service run with the same user and the same -d directory before you assume the kernel is wrong—most surprises are still path, capability, or DNS consistency issues.
-d, the unit’s WorkingDirectory, and where relative paths in config.yaml resolve. Pick one layout and keep it boring.
Install the core binary and pick stable paths
Download the correct architecture build (commonly linux-amd64 or linux-arm64) from a source you trust, extract the single binary, and place it somewhere stable such as /usr/local/bin/mihomo or /opt/clash-meta/mihomo. Make it executable, then verify the version with your binary’s version flag. Avoid running from a Downloads folder that changes permissions or disappears on cleanup; systemd units do not love moving targets.
When you upgrade, either replace the file in place or keep a symlink (for example /usr/local/bin/mihomo pointing to a versioned path). If you use setcap to grant capabilities to the binary, remember that upgrades may require reapplying capability bits—record the exact command in your notes so you are not guessing during an outage.
Where config.yaml lives (and why it matters)
A typical user-directory layout is ~/.config/mihomo/config.yaml, with auxiliary files such as Country.mmdb and on-disk rule caches living alongside it. For a dedicated service user, you must ensure that user owns the entire directory tree; otherwise you will see confusing behavior where manual runs work (because you are your own user) but the service user cannot read providers or write caches.
Another common layout is a system-wide directory such as /etc/mihomo/, referenced explicitly via -d /etc/mihomo. That can be clearer on multi-user machines, but you must manage permissions carefully: configuration often contains subscription URLs with secrets, so restrict file modes (for example chmod 600 for sensitive files) and avoid checking tokens into Git. For operational habits around subscriptions and node health, pair this setup with subscription management and node maintenance so “fetch config” is reliable before you debug TUN.
If you install through a sandboxed packaging format, the real writable configuration directory may differ from what you expect; trust the paths printed in logs, not assumptions from a blog screenshot. When in doubt, be explicit: pass -d and keep WorkingDirectory aligned with that directory.
Enable TUN in YAML: stacks, auto-route, DNS
In config.yaml, enabling TUN usually means a tun: section with enable: true. Additional keys often include stack (for example system versus userspace stacks), auto-route to install routes automatically, and auto-detect-interface to pick the correct egress interface on multi-homed machines. If you run Docker, corporate VPNs, or virtualization bridges, auto-route can interact with existing policy routing; treat “automatic” as “convenient, but not magic,” and be ready to narrow interfaces or adjust routes when multiple networks compete.
DNS is frequently the hidden half of TUN setups. TUN mode may be “up” while some domains fail because resolver behavior, fake-ip modes, and rule ordering disagree. If you see intermittent site failures, revisit DNS alongside TUN: rule routing best practices and the DNS notes in the FAQ are usually faster than chasing phantom TUN bugs.
Illustrative YAML fragment (adapt keys to your core version)tun:
enable: true
stack: system
auto-route: true
auto-detect-interface: true
dns-hijack:
- any:53
Treat the snippet as illustrative: your core version may rename fields, change defaults, or require additional constraints for strict routing. Always read the release notes when upgrading, because TUN behavior is tightly coupled to kernel routing semantics.
Permissions: /dev/net/tun and capabilities
Most Ubuntu systems expose /dev/net/tun when the kernel module is available. If the device cannot be opened, logs typically mention permission errors or missing devices—check that your service user is the same user you think it is, and that security tooling is not blocking TUN creation.
Running as root avoids many capability questions but increases blast radius. A more controlled approach is a dedicated user plus either file capabilities on the binary (for example setcap cap_net_admin,cap_net_bind_service=+ep /path/to/mihomo) or systemd capability injection via AmbientCapabilities= and CapabilityBoundingSet=. Pick one strategy, document it, and keep upgrades in mind: a replaced binary may need capabilities re-applied.
auto-route, validate on a non-production host first, and know how to stop the unit safely if the network becomes unreachable.
A systemd unit that survives reboots
User services can live under ~/.config/systemd/user/, while system-wide services belong in /etc/systemd/system/. A system-wide unit is appropriate when you need the proxy up before interactive login. Regardless of location, include After=network-online.target (and often Wants=network-online.target) so you are not racing DNS or DHCP. Use Restart=on-failure with a sane backoff for transient errors.
[Unit]
Description=Mihomo (Clash Meta)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=%h/.config/mihomo
ExecStart=/usr/local/bin/mihomo -d %h/.config/mihomo
Restart=on-failure
RestartSec=3
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
[Install]
WantedBy=default.target
After editing units, run daemon-reload, then enable and start the service. If you need the user service to run without a login session, investigate linger (loginctl enable-linger)—but only if that matches your security model.
Verify interfaces, routes, and policy routing
When the service is healthy, confirm that a TUN-like interface appears (ip link), and inspect whether policy routing tables were installed as you expect (ip rule, ip route). For application-level confirmation, test both domestic and overseas destinations intentionally—misconfigured rules can look like “TUN is broken” when the tunnel is fine but policy routing sends the wrong traffic to the wrong policy group.
If only some applications misbehave, check for split stacks: containers, snaps, or static binaries that bypass the host routing table. Linux transparency is strong, but not a universal guarantee for every sandboxed runtime.
Troubleshoot with journalctl and layered thinking
Start with the first error line in the journal after startup—timeouts and TLS errors have different meanings once you know whether the tunnel ever came up. For layered diagnosis consistent with other guides on this site, use the timeout and TLS log walkthrough to separate local routing issues from remote node issues.
When “manual foreground run works but systemd fails,” compare environment, working directory, and user identity line by line. A mismatch in -d is still the most common explanation, followed by capability differences and sandbox hardening profiles.
Upstream open source and trust boundaries
Mihomo and related projects are developed in the open, which helps with auditing and community review. If you want build recipes, issue trackers, or protocol discussions, visit the upstream repositories directly. For day-to-day client downloads, prefer the site’s download page as the primary end-user entry point so you do not confuse “source releases” with “what you should install on a family member’s laptop.” Keeping those two channels mentally separate reduces mistakes during upgrades.
Wrap-up: from “it runs once” to “it keeps running”
A solid Linux deployment is mostly boring operations: stable paths, explicit configuration directories, correct privileges, and a service manager that restarts sanely. TUN adds kernel coupling, so treat routing changes with the same respect you would give firewall edits: test safely, keep rollback steps, and document the capability strategy you chose.
Compared with ad-hoc terminal sessions, systemd turns Clash into infrastructure: it comes back after reboot, it logs coherently, and it is easier to reason about when something else on the machine changes. Pair that with a desktop client when you want visual editing, but keep one source of truth for the configuration directory so GUI and headless runs do not fight each other.
→ Download Clash for free and experience the difference—start from a maintained client path, then map the same routing ideas back to your Ubuntu service with fewer surprises.