Linux Setup 14-minute read

Deploying the Clash Core from the Linux Command Line: systemd Service and TUN Permissions

Run the Clash core on headless Linux with this step-by-step guide to binary placement, systemd services, TUN permissions, capabilities, startup, and log troubleshooting.

Deployment Architecture and Scope

A Linux server usually does not need a graphical client. A more reliable setup runs the mihomo core as a standalone system service, with systemd handling startup, restarts, privilege reduction, and log collection. Keep configuration files in /etc/mihomo/, writable runtime data in /var/lib/mihomo/, and the binary at /usr/local/bin/mihomo. This separation keeps application updates, configuration changes, and cache cleanup from interfering with one another.

The examples in this guide use Ubuntu 24.04.2 LTS, Debian 12.10, systemd 255/252, and mihomo 1.19.10 as reference environments. Package commands may vary by distribution, but the approach to systemd services, TUN devices, and Linux capabilities is the same. With the original Clash core, some TUN, DNS, and rule-provider options may be unavailable, so check the version output first to confirm which core you are running.

uname -m
/usr/local/bin/mihomo -v
systemctl --version
ip -Version

Common results from uname -m include x86_64, aarch64, and armv7l; the downloaded binary must match the CPU architecture. The version output should clearly show the mihomo version and build details. If the command returns “Exec format error,” the architecture is usually wrong—not the file permissions.

Path Purpose Recommended permissions
/usr/local/bin/mihomo Fixed core executable root:root 0755
/etc/mihomo/config.yaml Main configuration, proxy, and rule entry point root:clash 0640
/var/lib/mihomo/ Geo data, rule cache, and runtime data clash:clash 0750
/etc/systemd/system/mihomo.service systemd service unit root:root 0644

Install the Binary and Create a Dedicated Service User

The following steps assume you already have a mihomo executable matching the current CPU architecture, temporarily saved as /tmp/mihomo. Install it at a fixed path first, then create a non-login system user. A dedicated user prevents the service from running as root indefinitely and limits access to subscription URLs, controller secrets, and proxy details in the configuration to the designated group.

sudo install -o root -g root -m 0755 /tmp/mihomo /usr/local/bin/mihomo

sudo useradd \
  --system \
  --home-dir /var/lib/mihomo \
  --create-home \
  --shell /usr/sbin/nologin \
  clash

sudo install -d -o root -g clash -m 0750 /etc/mihomo
sudo install -d -o clash -g clash -m 0750 /var/lib/mihomo

/usr/local/bin/mihomo -v

If the system already has a user named clash, useradd will report that the user already exists. In that case, use id clash to check its home directory and groups. The service user needs neither a password nor membership in extra groups such as sudo or docker.

Write a Minimal Working Configuration

The configuration below sets up a local mixed proxy port, a REST controller, and TUN takeover. mixed-port: 7890 accepts both HTTP and SOCKS5 connections; the controller is restricted to port 9090 on the loopback address; DNS uses the unprivileged port 1053, avoiding the extra permission required to bind port 53.

mixed-port: 7890
bind-address: 127.0.0.1
allow-lan: false
mode: rule
log-level: info
ipv6: false

external-controller: 127.0.0.1:9090
secret: "Replace with a sufficiently long random controller secret"

profile:
  store-selected: true
  store-fake-ip: true

tun:
  enable: true
  stack: mixed
  auto-route: true
  auto-detect-interface: true
  strict-route: true
  dns-hijack:
    - any:53
    - tcp://any:53

dns:
  enable: true
  listen: 127.0.0.1:1053
  ipv6: false
  enhanced-mode: fake-ip
  fake-ip-range: 198.18.0.1/16
  default-nameserver:
    - 223.5.5.5
    - 1.1.1.1
  nameserver:
    - https://223.5.5.5/dns-query
    - https://1.1.1.1/dns-query

proxies: []

proxy-groups:
  - name: PROXY
    type: select
    proxies:
      - DIRECT

rules:
  - GEOIP,CN,DIRECT
  - MATCH,PROXY

This configuration can verify that the process, ports, and TUN are working, but the PROXY group currently contains only DIRECT, so it will not proxy traffic in practice. For production use, convert an existing subscription into a mihomo-compatible configuration or reference a provider's compatible subscription through proxy-providers. Do not enter a web subscription URL as if it were a single proxy node.

After saving, tighten the permissions and run the syntax check as the service user. -d selects the runtime data directory, while -f explicitly selects the configuration file. Keeping them separate prevents Geo data and rule caches downloaded by mihomo from being written to /etc.

sudo chown root:clash /etc/mihomo/config.yaml
sudo chmod 0640 /etc/mihomo/config.yaml

sudo -u clash /usr/local/bin/mihomo \
  -t \
  -d /var/lib/mihomo \
  -f /etc/mihomo/config.yaml

Configure the TUN Device and Capabilities

TUN mode requires the kernel to provide /dev/net/tun and the process to be allowed to modify routes, policy routing, and virtual network interfaces. The key capability is CAP_NET_ADMIN. The default ports 7890, 9090, and 1053 are all above 1024, so CAP_NET_BIND_SERVICE is not needed.

Check the TUN Module

test -c /dev/net/tun && echo "TUN device ready"
ls -l /dev/net/tun
sudo modprobe tun
cat /sys/class/misc/tun/dev

Under normal conditions, the final command outputs 10:200. If modprobe tun succeeds but the device is still missing, check whether the current kernel was built without TUN support. In a container, the host must also expose the character device 10:200; creating a same-named file inside the container does not provide TUN functionality.

If the module must be loaded explicitly at every boot, add it to the modules-load configuration:

echo tun | sudo tee /etc/modules-load.d/tun.conf
sudo systemctl restart systemd-modules-load.service

Grant Capabilities Through systemd First

Linux commonly handles this in one of two ways: assigning a file capability to the binary or granting the capability when systemd starts the process. The service-based approach is preferable here. Replacing the binary will not remove the capability, and the permitted scope remains visible in the service unit.

Create /etc/systemd/system/mihomo.service:

[Unit]
Description=mihomo proxy service
Documentation=https://wiki.metacubex.one/
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=clash
Group=clash
WorkingDirectory=/var/lib/mihomo

ExecStartPre=/usr/local/bin/mihomo -t -d /var/lib/mihomo -f /etc/mihomo/config.yaml
ExecStart=/usr/local/bin/mihomo -d /var/lib/mihomo -f /etc/mihomo/config.yaml

Restart=on-failure
RestartSec=3s
TimeoutStopSec=15s
LimitNOFILE=1048576

AmbientCapabilities=CAP_NET_ADMIN
CapabilityBoundingSet=CAP_NET_ADMIN
NoNewPrivileges=true

DevicePolicy=closed
DeviceAllow=/dev/net/tun rw
PrivateDevices=false

ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/mihomo
PrivateTmp=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK

[Install]
WantedBy=multi-user.target

AmbientCapabilities passes CAP_NET_ADMIN to the non-root service process, while CapabilityBoundingSet prevents it from acquiring capabilities outside the list. DevicePolicy=closed combined with DeviceAllow=/dev/net/tun rw exposes only the TUN character device required at runtime. ProtectSystem=strict makes system directories read-only, while ReadWritePaths explicitly allows mihomo to write to its state directory.

If a distribution or older systemd release does not support one of these hardening directives, the logs will identify the unknown field. Adjust that line for the installed systemd version rather than reverting to running the service as root just to bypass one error.

File Capabilities as a Fallback

Without systemd management, you can grant the binary CAP_NET_ADMIN, but the attribute disappears when a new binary is installed over it. With this approach, do not also configure systemd's AmbientCapabilities.

sudo setcap cap_net_admin=+ep /usr/local/bin/mihomo
getcap /usr/local/bin/mihomo

# Revoke when needed
sudo setcap -r /usr/local/bin/mihomo

Some filesystems mounted with nosuid ignore file capabilities, and some container runtimes filter capabilities as well. When you see Operation not permitted, check the mount options, the container's capability set, and the host's device mapping together.

Start the Service, Enable Boot Startup, and Verify Runtime

After writing the service unit, have systemd reload its configuration before starting the service and adding it to the boot target. enable --now creates the startup link and starts the service in one step.

sudo systemctl daemon-reload
sudo systemctl enable --now mihomo.service
sudo systemctl status mihomo.service --no-pager

A healthy status should show Active: active (running), with the main process owned by clash. In the reference environment, a configuration with about 620 rules and 2 proxy providers takes roughly 0.8 to 1.2 seconds to cold-start and uses about 45 to 85 MB of resident memory after stabilizing. Memory use will vary with the number of rule sets, Geo data, and connections.

Check Listening Ports and Interfaces

sudo ss -lntup | grep -E ':(7890|9090|1053)\b'
ip tuntap show
ip rule show
ip route show table all | grep -E '198\.18\.|default'
systemctl show mihomo.service -p User -p MainPID

The listening addresses should match the configuration: 7890, 9090, and 1053 should all listen only on 127.0.0.1. The TUN interface created by mihomo can vary by version and configuration, so do not rely on a fixed name alone. Checking ip tuntap show, policy routes, and service logs together is more reliable.

Verify the HTTP Proxy and TUN Routing

First test the proxy entry point by explicitly specifying the mixed port, then test a normal connection without proxy environment variables. Because the example policy group currently points to DIRECT, the goal here is to confirm that the request passes through mihomo, DNS does not time out, and routing does not form a loop.

curl --proxy http://127.0.0.1:7890 \
  --connect-timeout 5 \
  https://www.example.com/ -I

env -u http_proxy -u https_proxy -u all_proxy \
  curl --connect-timeout 5 \
  https://www.example.com/ -I

sudo journalctl -u mihomo.service -n 50 --no-pager

After importing actual proxy nodes, temporarily set log-level to debug to see which rule matches the destination and which policy group is selected. Switch it back to info after troubleshooting; otherwise, long-running services will generate a large volume of logs.

Update the Configuration and Reload Safely

Do not restart immediately after editing YAML. Run the test command first to verify indentation, rule syntax, policy-group references, and provider paths, then let systemd restart the service. YAML uses spaces for indentation; tab characters cause parsing failures. Policy-group names referenced by rules must also match proxy-groups exactly.

sudo -u clash /usr/local/bin/mihomo \
  -t \
  -d /var/lib/mihomo \
  -f /etc/mihomo/config.yaml

sudo systemctl restart mihomo.service
sudo systemctl status mihomo.service --no-pager
sudo journalctl -u mihomo.service --since "2 minutes ago" --no-pager

If the configuration comes from a subscription, download it to a temporary file first, test its syntax, and then atomically replace the active configuration. This ensures systemd reads only a complete file after restarting instead of encountering a partially downloaded YAML file.

sudo install -o root -g clash -m 0640 \
  /tmp/config.yaml \
  /etc/mihomo/config.yaml.new

sudo -u clash /usr/local/bin/mihomo \
  -t \
  -d /var/lib/mihomo \
  -f /etc/mihomo/config.yaml.new

sudo mv /etc/mihomo/config.yaml.new /etc/mihomo/config.yaml
sudo systemctl restart mihomo.service

mihomo's external controller also supports configuration reloads, but remote automation scripts must handle the controller secret, listening scope, and rollback on failure carefully. For a single-server setup, testing first and then running systemctl restart is clearer; the interruption is usually only a few seconds.

Log Diagnosis and Common Failures

The Service Keeps Restarting

Start by checking the logs and exit code for the current startup cycle. Because the service uses Restart=on-failure, a syntax error can trigger repeated restarts. During troubleshooting, stop the service first and run the configuration test directly.

sudo journalctl -u mihomo.service -b --no-pager
sudo systemctl show mihomo.service \
  -p ExecMainCode \
  -p ExecMainStatus \
  -p NRestarts

sudo systemctl stop mihomo.service
sudo -u clash /usr/local/bin/mihomo \
  -t \
  -d /var/lib/mihomo \
  -f /etc/mihomo/config.yaml

Common errors include inconsistent YAML indentation, references to missing policy groups, a configuration file that the clash user cannot read, and a provider directory that is not writable. Use namei -l /etc/mihomo/config.yaml to inspect permissions on each directory component.

“permission denied” When Creating the TUN Device

Check the TUN device, service capabilities, and systemd device policy in order. It is not enough to confirm that /dev/net/tun exists; without CAP_NET_ADMIN, the process still cannot create the interface or write policy routes.

ls -l /dev/net/tun
systemctl show mihomo.service \
  -p AmbientCapabilities \
  -p CapabilityBoundingSet \
  -p DevicePolicy
sudo journalctl -u mihomo.service -n 100 --no-pager | \
  grep -Ei 'tun|permission|operation not permitted'

If the service runs in LXC, Docker, or another container, the host must expose /dev/net/tun and grant NET_ADMIN. If the cloud server's kernel lacks TUN support, container-side settings cannot add that kernel capability.

DNS Times Out After Enabling TUN

First confirm that port 1053 is listening, then check whether the upstream DNS servers are reachable. If systemd-resolved is also running, it usually occupies 127.0.0.53:53, which does not directly conflict with mihomo listening on 127.0.0.1:1053. The real concern is whether TUN DNS hijacking sends mihomo's own upstream queries back into TUN, creating a loop.

sudo ss -lnup | grep ':1053'
resolvectl status
dig @127.0.0.1 -p 1053 www.example.com
sudo journalctl -u mihomo.service -n 100 --no-pager | \
  grep -Ei 'dns|timeout|loop'

Follow this order: verify that the default egress interface is detected correctly, confirm that the nameserver is reachable, and then check whether a rule is incorrectly intercepting upstream DNS traffic. On multi-homed servers, explicitly select the interface in the TUN configuration to prevent automatic detection from choosing a Docker, WireGuard, or temporary VPN interface.

Works Locally, but LAN Devices Cannot Connect

The example deliberately uses bind-address: 127.0.0.1 and allow-lan: false, so other devices cannot access 7890. If you need to provide a LAN proxy, change the listening address to the server's private network address and enable LAN access, while allowing only trusted subnets through the firewall.

# Example only: allow 192.168.10.0/24 to access TCP 7890
sudo ufw allow from 192.168.10.0/24 to any port 7890 proto tcp
sudo ufw status numbered

Keep the external controller on 127.0.0.1 and access it through SSH port forwarding. If it must listen on a private network address, set a random secret and restrict the source addresses at minimum. Never expose the control interface directly to the public internet.

Operations and Maintenance Checklist

  • The binary architecture matches uname -m; after upgrading, run mihomo -v again.
  • The main configuration is owned by root:clash, has permissions 0640, and the service user owns the runtime directory.
  • Run mihomo -t before replacing the configuration; restart the service only after the test passes.
  • TUN mode requires only CAP_NET_ADMIN; the default ports do not require low-port binding capability.
  • /dev/net/tun exists; in containers, both host device mapping and capability grants are configured.
  • The controller listens on 127.0.0.1:9090 with a secret configured; remote administration uses SSH forwarding.
  • Keep logging at info; switch to debug only during short troubleshooting sessions.
  • Prepare an automatic stop job before changing TUN routes to prevent a default-route change from breaking a remote SSH session.

After these steps, systemd starts mihomo once the network is ready, retries automatically after a 3-second delay if it exits unexpectedly, and allows 15 seconds for shutdown. Configuration, the binary, and runtime data remain separately managed, giving you clear boundaries for updating the core, changing subscriptions, or migrating the server.

Download Clash Client View available versions by platform