Skip to main content
EN

Home / Blog / Deployment

Mihomo as a home gateway — transparent proxying on a soft router or NAS

Deployment2026-06-051791 words4 min read
Mihomo as a home gateway — transparent proxying on a soft router or NAS

Installing a client on every device is tedious, and televisions, projectors and IoT devices cannot run one at all. Deploy Mihomo on a soft router or NAS and every device on the Wi-Fi is covered automatically.

Three ways to hook it up

The trade-offsSide router with a manual gatewaychange thelowest risk, a failure affects only that deviceeach device configured individuallyrecommended for a first attemptTProxy transparent proxyingredirect tzero device configurationcomplex, and a mistake takes the whole house offlineTUN with auto-routeenable TUNsimple to configurehigher overhead, a soft router's CPU may not cope
Get it working with a manually pointed side router first, then decide whether transparent proxying is worth it

This article takes side router plus TProxy as its main line, the most practical combination for a home.

1. Installing Mihomo

Using a Linux amd64 soft router or NAS:

# download and extract (pick the architecture to match)
gunzip mihomo-linux-amd64-v1.19.29.gz
chmod +x mihomo-linux-amd64-v1.19.29
sudo mv mihomo-linux-amd64-v1.19.29 /usr/local/bin/mihomo
mihomo -v

# prepare the working directory
sudo mkdir -p /etc/mihomo

ARM devices (Raspberry Pi, ARM NAS) take the linux-arm64 build; uname -m printing aarch64 means that one.

2. The configuration file

/etc/mihomo/config.yaml:

# ===== basics =====
mixed-port: 7890
tproxy-port: 7895          # for TProxy
redir-port: 7892           # for REDIRECT (optional)
allow-lan: true
bind-address: "*"
mode: rule
log-level: info
ipv6: false
unified-delay: true
tcp-concurrent: true

external-controller: 0.0.0.0:9090
secret: "set a sufficiently long random password"
external-ui: /etc/mihomo/ui

# ===== DNS =====
dns:
  enable: true
  listen: 0.0.0.0:1053
  ipv6: false
  enhanced-mode: fake-ip
  fake-ip-range: 198.18.0.1/16
  fake-ip-filter:
    - "*.lan"
    - "*.local"
    - "+.home.arpa"
    - "+.pool.ntp.org"
    - "time.*.com"
    - "+.msftconnecttest.com"
    - "+.msftncsi.com"
    - "captive.apple.com"
  default-nameserver: [223.5.5.5, 119.29.29.29]
  nameserver:
    - https://doh.pub/dns-query
    - https://dns.alidns.com/dns-query
  fallback:
    - https://1.1.1.1/dns-query
  fallback-filter:
    geoip: true
    geoip-code: CN

# ===== where nodes come from =====
proxy-providers:
  main:
    type: http
    url: "your subscription URL"
    interval: 3600
    path: ./providers/main.yaml
    header:
      User-Agent: ["clash.meta"]
    exclude-filter: "(?i)remaining|expire|website|traffic"
    health-check:
      enable: true
      url: http://www.gstatic.com/generate_204
      interval: 300
      lazy: true

# ===== policy groups =====
proxy-groups:
  - name: "PROXY"
    type: select
    proxies: ["AUTO", "HK", "JP", "SG", DIRECT]
  - name: "AUTO"
    type: url-test
    use: [main]
    url: http://www.gstatic.com/generate_204
    interval: 300
    tolerance: 50
    lazy: true
  - name: "HK"
    type: url-test
    use: [main]
    filter: "(?i)HK|Hong ?Kong"
    interval: 300
    tolerance: 50
  - name: "JP"
    type: url-test
    use: [main]
    filter: "(?i)JP|Japan"
    interval: 300
    tolerance: 50
  - name: "SG"
    type: url-test
    use: [main]
    filter: "(?i)SG|Singapore"
    interval: 300
    tolerance: 50
  - name: "FINAL"
    type: select
    proxies: ["PROXY", DIRECT]

# ===== rules =====
rules:
  - IP-CIDR,127.0.0.0/8,DIRECT,no-resolve
  - IP-CIDR,192.168.0.0/16,DIRECT,no-resolve
  - IP-CIDR,10.0.0.0/8,DIRECT,no-resolve
  - IP-CIDR,172.16.0.0/12,DIRECT,no-resolve
  - IP-CIDR,100.64.0.0/10,DIRECT,no-resolve
  - DOMAIN-SUFFIX,lan,DIRECT
  - DOMAIN-SUFFIX,local,DIRECT
  # per-device routing (examples)
  - SRC-IP-CIDR,192.168.1.50/32,DIRECT          # a family member's phone stays direct
  - SRC-IP-CIDR,192.168.1.60/32,PROXY           # the TV is always proxied
  # ordinary routing
  - GEOIP,CN,DIRECT
  - MATCH,FINAL

3. The systemd service

/etc/systemd/system/mihomo.service:

[Unit]
Description=Mihomo Daemon
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=root
LimitNOFILE=1000000
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
Restart=always
RestartSec=5
ExecStartPre=/usr/local/bin/mihomo -t -d /etc/mihomo
ExecStart=/usr/local/bin/mihomo -d /etc/mihomo

[Install]
WantedBy=multi-user.target

The -t in ExecStartPre is a config check — a broken config then fails to start rather than taking down a working service, and that detail saves a great deal of trouble.

sudo systemctl daemon-reload
sudo systemctl enable --now mihomo
sudo systemctl status mihomo
journalctl -u mihomo -f

4. Approach A: point the gateway manually (start here)

Touch no firewall rules; on the devices that need proxying, change:

  • Gateway: to the soft router's address (192.168.1.2, say)
  • DNS: also 192.168.1.2
Where a side router sitsDevicegateway points at the side routerSide router running Mihomohandles it by ruleMain routerhandles the internet connectionInternetDevices you leave alone take the original path and are unaffected
The big advantage: when something breaks, everything else still works

The strength of this approach is fault tolerance — if Mihomo dies it only affects the devices you changed, and nobody else in the house notices.

5. Approach B: TProxy transparent proxying

Zero device configuration, at the price of firewall rules on the gateway.

Enable kernel forwarding

sudo tee /etc/sysctl.d/99-mihomo.conf > /dev/null <<'EOF'
net.ipv4.ip_forward = 1
net.ipv4.conf.all.route_localnet = 1
EOF
sudo sysctl --system

iptables rules

#!/bin/bash
# /etc/mihomo/tproxy.sh
TPROXY_PORT=7895
ROUTE_TABLE=100
FWMARK=1

# policy routing: marked packets go to the local table
ip rule add fwmark $FWMARK table $ROUTE_TABLE 2>/dev/null
ip route add local default dev lo table $ROUTE_TABLE 2>/dev/null

# our own chain
iptables -t mangle -N MIHOMO 2>/dev/null
iptables -t mangle -F MIHOMO

# let private and reserved ranges through untouched
for net in 0.0.0.0/8 10.0.0.0/8 127.0.0.0/8 169.254.0.0/16 \
           172.16.0.0/12 192.168.0.0/16 224.0.0.0/4 240.0.0.0/4; do
  iptables -t mangle -A MIHOMO -d $net -j RETURN
done

# mark everything else and hand it to the TProxy port
iptables -t mangle -A MIHOMO -p tcp -j TPROXY --on-port $TPROXY_PORT --tproxy-mark $FWMARK
iptables -t mangle -A MIHOMO -p udp -j TPROXY --on-port $TPROXY_PORT --tproxy-mark $FWMARK

# apply to forwarded traffic (from other devices)
iptables -t mangle -A PREROUTING -j MIHOMO

# DNS hijacking: redirect port 53 queries from devices to Mihomo
iptables -t nat -A PREROUTING -p udp --dport 53 -j REDIRECT --to-ports 1053
iptables -t nat -A PREROUTING -p tcp --dport 53 -j REDIRECT --to-ports 1053

A cleanup script:

#!/bin/bash
# /etc/mihomo/tproxy-clear.sh
iptables -t mangle -D PREROUTING -j MIHOMO 2>/dev/null
iptables -t mangle -F MIHOMO 2>/dev/null
iptables -t mangle -X MIHOMO 2>/dev/null
iptables -t nat -F PREROUTING 2>/dev/null
ip rule del fwmark 1 table 100 2>/dev/null
ip route del local default dev lo table 100 2>/dev/null

Attach the rules to systemd so they come and go with the service:

[Service]
ExecStartPost=/etc/mihomo/tproxy.sh
ExecStopPost=/etc/mihomo/tproxy-clear.sh

Getting devices to use it

In the main router's DHCP settings, set both the default gateway and the DNS server to the soft router's address. Devices pick it up after reconnecting to the Wi-Fi.

6. Fault tolerance: do not annoy your household

This is the most important section for a home deployment.

Measures that are not optionalSet Restart=always in systemd so a dead process comes backUse -t in ExecStartPre so a broken config never startsHave the subscription update script download to a temp file, validate, and only then replaceKeep one device that does not go through the side router, so you can look things up when it breaksConfigure two DNS servers in the main router's DHCP: the side router plus a public resolver as backupGive important devices (a relative's phone, the work laptop) a SRC-IP-CIDR rule set to DIRECT

A safe subscription update script

#!/bin/bash
# /usr/local/bin/update-mihomo.sh
set -euo pipefail
CONF=/etc/mihomo/config.yaml
TMP=$(mktemp)
trap 'rm -f "$TMP"' EXIT

curl -fsSL --max-time 30 -A "clash.meta" -o "$TMP" "your subscription URL"

# basic sanity check
grep -q "^proxies:\|^proxy-providers:" "$TMP" || { echo "subscription content looks wrong"; exit 1; }

# let mihomo validate it too
cp "$CONF" "${CONF}.bak"
cp "$TMP" "$CONF"
if mihomo -t -d /etc/mihomo; then
  systemctl restart mihomo
  echo "updated"
else
  cp "${CONF}.bak" "$CONF"
  echo "validation failed, rolled back"
  exit 1
fi

Add it to cron:

0 4 * * * /usr/local/bin/update-mihomo.sh >> /var/log/mihomo-update.log 2>&1

With a proxy-provider you do not actually need this script — the core pulls nodes on its own schedule and the main config never changes. That is an important reason to prefer a provider over a whole subscription.

7. The web panel

external-ui: /etc/mihomo/ui
external-ui-name: metacubexd

Put the panel's files in /etc/mihomo/ui, then visit http://router-ip:9090/ui in a browser, enter the secret, and you can switch nodes, inspect connections and read logs from a web page.

Other people in the house can use that page to switch nodes themselves rather than asking you.

8. Performance and resources

Rough capacity of different hardware (indicative)Raspberry Pi 4Baround 100 Mbps, fine for a householdJ4125 soft routeressentially saturates gigabitN100 soft routergigabit without effortOld single-core devicesvisibly struggle with large rule setsActual capacity depends on rule-set size, connection count and protocol; treat this as a selection guide only

The order to optimise in when performance is tight:

  1. Trim the rule sets (drop streaming and ad lists you do not use)
  2. Switch rule sets to the binary mrs format
  3. Set log-level to warning, never debug
  4. Reduce health-check frequency
  5. TProxy uses fewer resources than TUN, so prefer TProxy

9. Diagnostics

# service status
systemctl status mihomo
journalctl -u mihomo -n 100 --no-pager

# what is listening
ss -tulnp | grep mihomo

# does the API respond
curl -H "Authorization: Bearer your-secret" http://127.0.0.1:9090/version

# test the proxy port from another device
curl -x http://192.168.1.2:7890 -I https://www.google.com

# hit counts on the iptables rules
iptables -t mangle -L MIHOMO -v -n

In short

  • Get "side router plus manual gateway" working first, and only move to TProxy once it is stable
  • secret is mandatory; 0.0.0.0:9090 is open to your entire LAN
  • Use a proxy-provider rather than a whole subscription, so the core updates itself and the main config never moves
  • Add -t to ExecStartPre so a broken config cannot displace a running service
  • Give important devices a direct SRC-IP-CIDR rule so the household is unaffected
  • Before touching TProxy, schedule an automatic cleanup to leave yourself a way back

Related: the control API and web panels and managing several subscriptions.


Related docs

Getting Docker and WSL2 onto the host's Clash proxy
Deployment Getting Docker and WSL2 onto the host's Clash proxy

127.0.0.1 inside a container is not the host. Configuration for all three Docker scenarios — daemon pulls, build time and run time — plus two approaches for WSL2's different networking modes.

2026-06-141330 words3 min read