Загрузка данных
#!/bin/sh
#
# Install mihomo as a low-memory, fail-open TUN gateway.
#
# Primary target: OpenWrt 22.03+ with fw4/nftables.
# Also supports systemd-based Linux routers.
#
# Routing is owned by mihomo itself:
# system TUN + auto-route + auto-redirect + GSO.
# OpenWrt places the TUN device in the LAN firewall zone, so fw4 reloads do not
# remove forwarding access. No custom full-tunnel route table is maintained.
#
# Usage:
# LAN_IFACE=br-lan sh install-mihomo-router.sh
#
# Optional environment variables:
# MIHOMO_VERSION=v1.19.29
# DOWNLOAD_URL=https://.../mihomo.gz
# MIHOMO_SHA256=<sha256>
# LAN_IFACE=br-lan
# WAN_IFACE=pppoe-wan
# TUN_IFACE=mihomo0
# GOGC_VAL=30
# GOMEMLIMIT=64MiB
# MEMWATCH=1
# BYPASS_MARK=0xc0de
# BYPASS_RULE_PRIORITY=50
#
set -eu
MIHOMO_VERSION="${MIHOMO_VERSION:-v1.19.29}"
DOWNLOAD_URL="${DOWNLOAD_URL:-}"
MIHOMO_SHA256="${MIHOMO_SHA256:-}"
LAN_IFACE="${LAN_IFACE:-br-lan}"
WAN_IFACE="${WAN_IFACE:-}"
TUN_IFACE="${TUN_IFACE:-mihomo0}"
GOGC_VAL="${GOGC_VAL:-30}"
GOMEMLIMIT="${GOMEMLIMIT:-}"
MEMWATCH="${MEMWATCH:-1}"
BYPASS_MARK="${BYPASS_MARK:-0xc0de}"
BYPASS_RULE_PRIORITY="${BYPASS_RULE_PRIORITY:-50}"
CONFIG_DIR="/etc/mihomo"
CONFIG_FILE="$CONFIG_DIR/config.yaml"
INSTALL_ENV="$CONFIG_DIR/install.env"
MEMWATCH_SCRIPT="$CONFIG_DIR/memwatch.sh"
MIHOMO_BIN="/usr/bin/mihomo"
CONFIG_HELPER="/usr/bin/mihomo-config"
SYSTEMD_UNIT="/etc/systemd/system/mihomo.service"
OPENWRT_INIT="/etc/init.d/mihomo"
# Reuse the paths from the reference installer so rerunning this script replaces
# that setup instead of installing a second, competing connmark chain.
OPENWRT_NFT_INCLUDE="/etc/nftables.d/30-vpn-bypass.nft"
OPENWRT_RULE_INCLUDE="/etc/firewall.vpn-bypass.sh"
# Files and state created by installer versions that used manual policy routing.
LEGACY_NET_HELPER="/usr/libexec/mihomo-router-net"
LEGACY_RUN_HELPER="/usr/libexec/mihomo-router-run"
LEGACY_FIREWALL_HOOK="/etc/hotplug.d/firewall/95-mihomo-router"
LEGACY_NFT_TABLE="mihomo_router"
LEGACY_ROUTE_MARK="0x100000"
LEGACY_ROUTE_TABLE="20220"
LEGACY_RULE_PRIORITY="10080"
TMP_DIR=""
PLATFORM=""
MEMWATCH_KB=""
MEMWATCH_MIN_AVAILABLE_KB=""
log() {
printf '%s\n' "[mihomo-installer] $*"
}
die() {
printf '%s\n' "[mihomo-installer] ERROR: $*" >&2
exit 1
}
cleanup_installer() {
if [ -n "$TMP_DIR" ] && [ -d "$TMP_DIR" ]; then
rm -rf "$TMP_DIR"
fi
}
trap cleanup_installer EXIT HUP INT TERM
require_root() {
[ "$(id -u)" -eq 0 ] || die "run this script as root"
}
validate_iface() {
iface_name="$1"
iface_label="$2"
case "$iface_name" in
*[!A-Za-z0-9_.:-]*|'') die "invalid $iface_label: $iface_name" ;;
esac
}
validate_mark() {
mark_value="$1"
case "$mark_value" in
0x*)
mark_digits="${mark_value#0x}"
case "$mark_digits" in
*[!0-9A-Fa-f]*|'') die "invalid BYPASS_MARK: $mark_value" ;;
esac
;;
*)
case "$mark_value" in
*[!0-9]*|'') die "invalid BYPASS_MARK: $mark_value" ;;
esac
;;
esac
}
validate_settings() {
validate_iface "$LAN_IFACE" "LAN_IFACE"
validate_iface "$TUN_IFACE" "TUN_IFACE"
if [ -n "$WAN_IFACE" ]; then
validate_iface "$WAN_IFACE" "WAN_IFACE"
fi
case "$GOGC_VAL" in
*[!0-9]*|'') die "GOGC_VAL must be numeric" ;;
esac
case "$MEMWATCH" in
0|1) ;;
*) die "MEMWATCH must be 0 or 1" ;;
esac
case "$BYPASS_RULE_PRIORITY" in
*[!0-9]*|'') die "BYPASS_RULE_PRIORITY must be numeric" ;;
esac
validate_mark "$BYPASS_MARK"
if [ -n "$GOMEMLIMIT" ]; then
case "$GOMEMLIMIT" in
*MiB)
limit_number="${GOMEMLIMIT%MiB}"
case "$limit_number" in
*[!0-9]*|'') die "GOMEMLIMIT must look like 64MiB" ;;
esac
;;
*) die "GOMEMLIMIT must look like 64MiB" ;;
esac
fi
}
detect_platform() {
if [ -f /etc/openwrt_release ] && [ -x /etc/rc.common ]; then
PLATFORM="openwrt"
elif command -v systemctl >/dev/null 2>&1; then
PLATFORM="systemd"
else
die "only OpenWrt/procd and systemd-based Linux are supported"
fi
}
detect_wan_interface() {
[ -z "$WAN_IFACE" ] || return 0
# Do not source /lib/functions/network.sh under `set -e`: on some OpenWrt
# builds network_get_device exits the entire ash process when ubus has no
# cached object. Reading the kernel route table is simpler and reliable.
WAN_IFACE="$(
ip -4 route show default 2>/dev/null |
awk '/^default / {
for (i=1; i<=NF; i++) {
if ($i=="dev") {
print $(i+1)
exit
}
}
}' || true
)"
if [ -z "$WAN_IFACE" ] &&
[ "$PLATFORM" = "openwrt" ] &&
command -v ubus >/dev/null 2>&1 &&
command -v jsonfilter >/dev/null 2>&1; then
WAN_IFACE="$(
ubus call network.interface.wan status 2>/dev/null |
jsonfilter -e '@.l3_device' 2>/dev/null || true
)"
fi
if [ -n "$WAN_IFACE" ]; then
validate_iface "$WAN_IFACE" "detected WAN_IFACE"
log "WAN interface: $WAN_IFACE"
else
log "WAN interface was not detected; port-forward bypass will be skipped"
fi
}
compute_memory_settings() {
if [ -z "$GOMEMLIMIT" ]; then
total_kb="$(awk '/^MemTotal:/ { print $2; exit }' /proc/meminfo)"
[ -n "$total_kb" ] || die "cannot read MemTotal from /proc/meminfo"
limit_mb=$((total_kb / 3 / 1024))
[ "$limit_mb" -ge 32 ] || limit_mb=32
GOMEMLIMIT="${limit_mb}MiB"
else
limit_mb="${GOMEMLIMIT%MiB}"
fi
MEMWATCH_KB=$((limit_mb * 3 / 2 * 1024))
total_kb="$(awk '/^MemTotal:/ { print $2; exit }' /proc/meminfo)"
MEMWATCH_MIN_AVAILABLE_KB=$((total_kb / 10))
[ "$MEMWATCH_MIN_AVAILABLE_KB" -ge 8192 ] ||
MEMWATCH_MIN_AVAILABLE_KB=8192
log "memory policy: GOGC=$GOGC_VAL, GOMEMLIMIT=$GOMEMLIMIT"
if [ "$MEMWATCH" = "1" ]; then
log "memory watchdog: RSS over $((MEMWATCH_KB / 1024)) MiB and low available RAM"
fi
}
install_dependencies_openwrt() {
missing=""
command -v nft >/dev/null 2>&1 || missing="$missing nftables"
opkg list-installed 2>/dev/null | grep -q '^ip-full - ' ||
missing="$missing ip-full"
command -v gzip >/dev/null 2>&1 || missing="$missing gzip"
if ! command -v curl >/dev/null 2>&1 &&
! command -v wget >/dev/null 2>&1; then
missing="$missing curl ca-bundle"
fi
[ -e /dev/net/tun ] || missing="$missing kmod-tun"
if [ -n "$missing" ]; then
command -v opkg >/dev/null 2>&1 ||
die "opkg is required to install:$missing"
log "installing OpenWrt dependencies:$missing"
opkg update
# shellcheck disable=SC2086
opkg install $missing
fi
}
install_dependencies_systemd() {
if command -v nft >/dev/null 2>&1 &&
command -v ip >/dev/null 2>&1 &&
command -v gzip >/dev/null 2>&1 &&
{ command -v curl >/dev/null 2>&1 ||
command -v wget >/dev/null 2>&1; }; then
return
fi
if command -v apt-get >/dev/null 2>&1; then
log "installing Debian/Ubuntu dependencies"
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y \
ca-certificates curl gzip iproute2 nftables
elif command -v dnf >/dev/null 2>&1; then
log "installing Fedora/RHEL dependencies"
dnf install -y ca-certificates curl gzip iproute nftables
elif command -v apk >/dev/null 2>&1; then
log "installing Alpine dependencies"
apk add --no-cache ca-certificates curl gzip iproute2 nftables
else
die "install curl/wget, gzip, iproute2 and nftables, then rerun"
fi
}
install_dependencies() {
if [ "$PLATFORM" = "openwrt" ]; then
install_dependencies_openwrt
else
install_dependencies_systemd
fi
command -v nft >/dev/null 2>&1 || die "nft is not installed"
command -v ip >/dev/null 2>&1 || die "iproute2 is not installed"
command -v gzip >/dev/null 2>&1 || die "gzip is not installed"
[ -e /dev/net/tun ] ||
die "/dev/net/tun is unavailable; load/install the tun module"
}
fetch() {
fetch_url="$1"
fetch_dest="$2"
rm -f "$fetch_dest"
if command -v curl >/dev/null 2>&1; then
curl --fail --location --retry 3 --connect-timeout 20 \
--output "$fetch_dest" "$fetch_url"
else
wget -O "$fetch_dest" "$fetch_url"
fi
}
architecture_candidates() {
case "$(uname -m)" in
x86_64|amd64)
printf '%s\n' "mihomo-linux-amd64-compatible mihomo-linux-amd64"
;;
i386|i486|i586|i686|x86)
printf '%s\n' "mihomo-linux-386"
;;
aarch64|arm64)
printf '%s\n' "mihomo-linux-arm64 mihomo-linux-arm64-v8"
;;
armv7l|armv7*) printf '%s\n' "mihomo-linux-armv7" ;;
armv6l|armv6*) printf '%s\n' "mihomo-linux-armv6" ;;
armv5l|armv5*) printf '%s\n' "mihomo-linux-armv5" ;;
mips64el|mips64le) printf '%s\n' "mihomo-linux-mips64le" ;;
mips64) printf '%s\n' "mihomo-linux-mips64" ;;
mipsel|mipsle)
printf '%s\n' "mihomo-linux-mipsle-softfloat mihomo-linux-mipsle"
;;
mips)
printf '%s\n' "mihomo-linux-mips-softfloat mihomo-linux-mips"
;;
riscv64) printf '%s\n' "mihomo-linux-riscv64" ;;
loongarch64|loong64)
printf '%s\n' "mihomo-linux-loong64-abi2 mihomo-linux-loong64-abi1"
;;
ppc64le) printf '%s\n' "mihomo-linux-ppc64le" ;;
s390x) printf '%s\n' "mihomo-linux-s390x" ;;
*)
die "unsupported architecture $(uname -m); set DOWNLOAD_URL manually"
;;
esac
}
download_mihomo() {
TMP_DIR="$(mktemp -d)"
archive="$TMP_DIR/mihomo.gz"
if [ -n "$DOWNLOAD_URL" ]; then
log "downloading mihomo from DOWNLOAD_URL"
fetch "$DOWNLOAD_URL" "$archive" || die "download failed"
else
candidates="$(architecture_candidates)"
for candidate in $candidates; do
url="https://github.com/MetaCubeX/mihomo/releases/download/${MIHOMO_VERSION}/${candidate}-${MIHOMO_VERSION}.gz"
log "trying $candidate for $(uname -m)"
if fetch "$url" "$archive"; then
DOWNLOAD_URL="$url"
break
fi
done
[ -s "$archive" ] ||
die "no matching release asset found; set DOWNLOAD_URL manually"
fi
if [ -n "$MIHOMO_SHA256" ]; then
if command -v sha256sum >/dev/null 2>&1; then
actual_hash="$(sha256sum "$archive" | awk '{print $1}')"
elif command -v openssl >/dev/null 2>&1; then
actual_hash="$(openssl dgst -sha256 "$archive" | awk '{print $NF}')"
else
die "MIHOMO_SHA256 was supplied, but no SHA-256 tool is installed"
fi
[ "$actual_hash" = "$MIHOMO_SHA256" ] ||
die "mihomo SHA-256 mismatch"
fi
gzip -dc "$archive" > "$TMP_DIR/mihomo"
chmod 0755 "$TMP_DIR/mihomo"
"$TMP_DIR/mihomo" -v >/dev/null 2>&1 ||
die "downloaded binary cannot run on this router"
}
delete_fw4_comment_rules() {
target_chain="$1"
target_comment="$2"
nft -a list chain inet fw4 "$target_chain" 2>/dev/null |
sed -n "/comment \"$target_comment\"/s/.*# handle \([0-9][0-9]*\)$/\1/p" |
while read -r handle; do
case "$handle" in
*[!0-9]*|'') continue ;;
esac
nft delete rule inet fw4 "$target_chain" handle "$handle" \
>/dev/null 2>&1 || true
done
}
stop_existing_service() {
if [ "$PLATFORM" = "openwrt" ]; then
if [ -x "$OPENWRT_INIT" ]; then
"$OPENWRT_INIT" stop >/dev/null 2>&1 || true
fi
else
systemctl stop mihomo.service >/dev/null 2>&1 || true
fi
if command -v pidof >/dev/null 2>&1; then
for pid in $(pidof mihomo 2>/dev/null || true); do
kill -TERM "$pid" >/dev/null 2>&1 || true
done
attempt=0
while pidof mihomo >/dev/null 2>&1 && [ "$attempt" -lt 5 ]; do
sleep 1
attempt=$((attempt + 1))
done
if pidof mihomo >/dev/null 2>&1; then
for pid in $(pidof mihomo 2>/dev/null || true); do
kill -KILL "$pid" >/dev/null 2>&1 || true
done
sleep 1
fi
fi
}
cleanup_legacy_network() {
if [ -x "$LEGACY_NET_HELPER" ]; then
"$LEGACY_NET_HELPER" down >/dev/null 2>&1 || true
fi
nft delete table inet "$LEGACY_NFT_TABLE" >/dev/null 2>&1 || true
delete_fw4_comment_rules forward "mihomo-router"
delete_fw4_comment_rules output "mihomo-router-system"
while ip rule del priority "$LEGACY_RULE_PRIORITY" \
fwmark "$LEGACY_ROUTE_MARK/$LEGACY_ROUTE_MARK" \
lookup "$LEGACY_ROUTE_TABLE" >/dev/null 2>&1; do
:
done
while ip -6 rule del priority "$LEGACY_RULE_PRIORITY" \
fwmark "$LEGACY_ROUTE_MARK/$LEGACY_ROUTE_MARK" \
lookup "$LEGACY_ROUTE_TABLE" >/dev/null 2>&1; do
:
done
ip route flush table "$LEGACY_ROUTE_TABLE" >/dev/null 2>&1 || true
ip -6 route flush table "$LEGACY_ROUTE_TABLE" >/dev/null 2>&1 || true
rm -f "$LEGACY_NET_HELPER" "$LEGACY_RUN_HELPER" \
"$LEGACY_FIREWALL_HOOK" "$CONFIG_DIR/router.env"
# Remove the optional QUIC blocker generated by the reference scripts.
# QUIC stays enabled by default; memory is bounded by system TUN and Go GC.
if [ -f /etc/nftables.d/30-quic-block.nft ] &&
grep -q 'chain quic_block' /etc/nftables.d/30-quic-block.nft; then
rm -f /etc/nftables.d/30-quic-block.nft
fi
}
install_binary() {
stop_existing_service
cleanup_legacy_network
mkdir -p "$(dirname "$MIHOMO_BIN")"
cp "$TMP_DIR/mihomo" "$MIHOMO_BIN"
chmod 0755 "$MIHOMO_BIN"
}
write_dns_block() {
cat <<'EOF'
dns:
enable: true
listen: 127.0.0.1:1053
ipv6: true
cache-algorithm: arc
enhanced-mode: redir-host
respect-rules: false
default-nameserver:
- 77.88.8.8
- 77.88.8.1
nameserver:
- https://1.1.1.1/dns-query#PROXY
- https://8.8.8.8/dns-query#PROXY
proxy-server-nameserver:
- udp://77.88.8.8#DIRECT
- udp://77.88.8.1#DIRECT
direct-nameserver:
- udp://77.88.8.8#DIRECT
- udp://77.88.8.1#DIRECT
direct-nameserver-follow-policy: false
EOF
}
normalize_tun_config() {
target_config="$1"
normalized_config="$(mktemp)"
awk -v tun_device="$TUN_IFACE" '
function managed(line) {
return line ~ /^[[:space:]]+(enable|stack|device|auto-route|auto-redirect|auto-detect-interface|strict-route|gso|gso-max-size):/
}
function emit_tun_settings() {
print " enable: true"
print " stack: system"
print " device: " tun_device
print " auto-route: true"
print " auto-redirect: true"
print " auto-detect-interface: true"
print " strict-route: false"
print " gso: true"
print " gso-max-size: 65536"
}
BEGIN {
in_tun = 0
found_tun = 0
}
/^tun:[[:space:]]*$/ {
if (in_tun) {
emit_tun_settings()
}
print
in_tun = 1
found_tun = 1
next
}
in_tun && /^[^[:space:]#][^:]*:/ {
emit_tun_settings()
in_tun = 0
}
in_tun && managed($0) {
next
}
{
print
}
END {
if (in_tun) {
emit_tun_settings()
}
if (!found_tun) {
print ""
print "tun:"
emit_tun_settings()
}
}
' "$target_config" > "$normalized_config"
cp "$normalized_config" "$target_config"
rm -f "$normalized_config"
}
add_low_memory_defaults() {
target_config="$1"
grep -Eq '^find-process-mode:' "$target_config" ||
printf '%s\n' 'find-process-mode: off' >> "$target_config"
grep -Eq '^geodata-loader:' "$target_config" ||
printf '%s\n' 'geodata-loader: memconservative' >> "$target_config"
grep -Eq '^keep-alive-idle:' "$target_config" ||
printf '%s\n' 'keep-alive-idle: 30' >> "$target_config"
grep -Eq '^keep-alive-interval:' "$target_config" ||
printf '%s\n' 'keep-alive-interval: 30' >> "$target_config"
}
write_configuration() {
mkdir -p "$CONFIG_DIR"
if [ -f "$CONFIG_FILE" ]; then
backup="$CONFIG_FILE.bak.$(date +%Y%m%d-%H%M%S)"
cp "$CONFIG_FILE" "$backup"
log "existing config backed up to $backup"
if ! grep -Eq '^dns:[[:space:]]*$' "$CONFIG_FILE"; then
new_config="$(mktemp)"
write_dns_block > "$new_config"
cat "$CONFIG_FILE" >> "$new_config"
cp "$new_config" "$CONFIG_FILE"
rm -f "$new_config"
log "DNS block added to existing configuration"
fi
else
write_dns_block > "$CONFIG_FILE"
cat >> "$CONFIG_FILE" <<EOF
mode: rule
log-level: warning
ipv6: true
find-process-mode: off
geodata-loader: memconservative
keep-alive-idle: 30
keep-alive-interval: 30
external-controller: 127.0.0.1:9090
tun:
enable: true
stack: system
device: ${TUN_IFACE}
auto-route: true
auto-redirect: true
auto-detect-interface: true
strict-route: false
gso: true
gso-max-size: 65536
mtu: 1500
proxy-groups:
- name: PROXY
type: select
proxies:
- DIRECT
rules:
- MATCH,DIRECT
EOF
fi
normalize_tun_config "$CONFIG_FILE"
add_low_memory_defaults "$CONFIG_FILE"
cat > "$INSTALL_ENV" <<EOF
TUN_IFACE='${TUN_IFACE}'
EOF
chmod 0600 "$CONFIG_FILE" "$INSTALL_ENV"
}
write_config_helper() {
cat > "$CONFIG_HELPER" <<'EOF'
#!/bin/sh
set -eu
[ "$(id -u)" -eq 0 ] || {
echo "run as root" >&2
exit 1
}
[ "$#" -eq 1 ] || {
echo "usage: mihomo-config <file-or-url>" >&2
exit 2
}
. /etc/mihomo/install.env
source_config="$1"
work_config="$(mktemp)"
final_config="$(mktemp)"
normalized_config="$(mktemp)"
trap 'rm -f "$work_config" "$final_config" "$normalized_config"' \
EXIT HUP INT TERM
case "$source_config" in
http://*|https://*)
if command -v curl >/dev/null 2>&1; then
curl --fail --location --output "$work_config" "$source_config"
else
wget -O "$work_config" "$source_config"
fi
;;
*)
[ -f "$source_config" ] || {
echo "configuration not found: $source_config" >&2
exit 1
}
cp "$source_config" "$work_config"
;;
esac
if grep -Eq '^dns:[[:space:]]*$' "$work_config"; then
cp "$work_config" "$final_config"
if ! grep -Eq '^[[:space:]]+listen:[[:space:]]*127\.0\.0\.1:1053[[:space:]]*$' \
"$work_config"; then
echo "warning: existing DNS block must listen on 127.0.0.1:1053" >&2
fi
else
cat > "$final_config" <<'EOF_DNS'
dns:
enable: true
listen: 127.0.0.1:1053
ipv6: true
cache-algorithm: arc
enhanced-mode: redir-host
respect-rules: false
default-nameserver:
- 77.88.8.8
- 77.88.8.1
nameserver:
- https://1.1.1.1/dns-query#PROXY
- https://8.8.8.8/dns-query#PROXY
proxy-server-nameserver:
- udp://77.88.8.8#DIRECT
- udp://77.88.8.1#DIRECT
direct-nameserver:
- udp://77.88.8.8#DIRECT
- udp://77.88.8.1#DIRECT
direct-nameserver-follow-policy: false
EOF_DNS
cat "$work_config" >> "$final_config"
fi
awk -v tun_device="$TUN_IFACE" '
function managed(line) {
return line ~ /^[[:space:]]+(enable|stack|device|auto-route|auto-redirect|auto-detect-interface|strict-route|gso|gso-max-size):/
}
function emit_tun_settings() {
print " enable: true"
print " stack: system"
print " device: " tun_device
print " auto-route: true"
print " auto-redirect: true"
print " auto-detect-interface: true"
print " strict-route: false"
print " gso: true"
print " gso-max-size: 65536"
}
BEGIN {
in_tun = 0
found_tun = 0
}
/^tun:[[:space:]]*$/ {
if (in_tun) {
emit_tun_settings()
}
print
in_tun = 1
found_tun = 1
next
}
in_tun && /^[^[:space:]#][^:]*:/ {
emit_tun_settings()
in_tun = 0
}
in_tun && managed($0) {
next
}
{
print
}
END {
if (in_tun) {
emit_tun_settings()
}
if (!found_tun) {
print ""
print "tun:"
emit_tun_settings()
}
}
' "$final_config" > "$normalized_config"
cp "$normalized_config" "$final_config"
grep -Eq '^find-process-mode:' "$final_config" ||
printf '%s\n' 'find-process-mode: off' >> "$final_config"
grep -Eq '^geodata-loader:' "$final_config" ||
printf '%s\n' 'geodata-loader: memconservative' >> "$final_config"
grep -Eq '^keep-alive-idle:' "$final_config" ||
printf '%s\n' 'keep-alive-idle: 30' >> "$final_config"
grep -Eq '^keep-alive-interval:' "$final_config" ||
printf '%s\n' 'keep-alive-interval: 30' >> "$final_config"
/usr/bin/mihomo -t -d /etc/mihomo -f "$final_config"
if [ -f /etc/mihomo/config.yaml ]; then
cp /etc/mihomo/config.yaml \
"/etc/mihomo/config.yaml.bak.$(date +%Y%m%d-%H%M%S)"
fi
cp "$final_config" /etc/mihomo/config.yaml
chmod 0600 /etc/mihomo/config.yaml
if [ -x /etc/init.d/mihomo ]; then
/etc/init.d/mihomo stop >/dev/null 2>&1 || true
/etc/init.d/mihomo start
/etc/init.d/dnsmasq restart
else
systemctl restart mihomo
fi
echo "installed: /etc/mihomo/config.yaml"
EOF
chmod 0755 "$CONFIG_HELPER"
}
configure_openwrt_dnsmasq() {
[ "$PLATFORM" = "openwrt" ] || return 0
if [ ! -f /etc/config/dhcp.before-mihomo ]; then
cp /etc/config/dhcp /etc/config/dhcp.before-mihomo
fi
upstreams="$(uci -q get dhcp.@dnsmasq[0].server || true)"
filtered_upstreams=""
for upstream in $upstreams; do
[ "$upstream" = "127.0.0.1#1053" ] && continue
filtered_upstreams="$filtered_upstreams $upstream"
done
if [ -z "$filtered_upstreams" ]; then
filtered_upstreams=" 77.88.8.8 77.88.8.1"
fi
uci -q delete dhcp.@dnsmasq[0].server || true
uci add_list dhcp.@dnsmasq[0].server='127.0.0.1#1053'
for upstream in $filtered_upstreams; do
uci add_list dhcp.@dnsmasq[0].server="$upstream"
done
uci set dhcp.@dnsmasq[0].noresolv='1'
uci set dhcp.@dnsmasq[0].strictorder='1'
uci set dhcp.@dnsmasq[0].cachesize='0'
uci set dhcp.@dnsmasq[0].rebind_protection='1'
uci commit dhcp
/etc/init.d/dnsmasq restart
}
configure_openwrt_firewall_zone() {
[ "$PLATFORM" = "openwrt" ] || return 0
lan_zone="$(uci show firewall |
sed -n "s/^firewall\.\(@zone\[[0-9]*\]\|[A-Za-z0-9_]*\)\.name='lan'/\1/p" |
head -n 1)"
[ -n "$lan_zone" ] ||
die "cannot find the OpenWrt firewall zone named lan"
if uci -q get "firewall.$lan_zone.device" 2>/dev/null |
tr ' ' '\n' | grep -Fxq "$TUN_IFACE"; then
log "$TUN_IFACE is already assigned to the LAN firewall zone"
else
uci add_list "firewall.$lan_zone.device=$TUN_IFACE"
uci commit firewall
log "assigned $TUN_IFACE to the LAN firewall zone"
fi
}
register_firewall_script_include() {
include_path="$1"
if uci -q show firewall | grep -Fq ".path='$include_path'"; then
return 0
fi
uci add firewall include >/dev/null
uci set firewall.@include[-1].type='script'
uci set "firewall.@include[-1].path=$include_path"
uci set firewall.@include[-1].fw4_compatible='1'
uci commit firewall
}
configure_openwrt_port_forward_bypass() {
[ "$PLATFORM" = "openwrt" ] || return 0
[ -n "$WAN_IFACE" ] || return 0
mkdir -p "$(dirname "$OPENWRT_NFT_INCLUDE")"
cat > "$OPENWRT_NFT_INCLUDE" <<EOF
# Preserve replies for connections initiated from WAN. The first WAN packet
# marks conntrack state; reply packets inherit the mark and use the main table
# before mihomo auto-route rules.
chain mihomo_port_forward_bypass {
type filter hook prerouting priority mangle; policy accept;
iifname "$WAN_IFACE" ct state new ct mark set $BYPASS_MARK counter
ct mark $BYPASS_MARK meta mark set $BYPASS_MARK counter
}
EOF
cat > "$OPENWRT_RULE_INCLUDE" <<EOF
#!/bin/sh
while ip rule del priority $BYPASS_RULE_PRIORITY \
fwmark $BYPASS_MARK lookup main >/dev/null 2>&1; do
:
done
ip rule add priority $BYPASS_RULE_PRIORITY \
fwmark $BYPASS_MARK lookup main
if ip -6 route show default 2>/dev/null | grep -q .; then
while ip -6 rule del priority $BYPASS_RULE_PRIORITY \
fwmark $BYPASS_MARK lookup main >/dev/null 2>&1; do
:
done
ip -6 rule add priority $BYPASS_RULE_PRIORITY \
fwmark $BYPASS_MARK lookup main
fi
EOF
chmod 0755 "$OPENWRT_RULE_INCLUDE"
register_firewall_script_include "$OPENWRT_RULE_INCLUDE"
log "configured conntrack bypass for inbound WAN connections"
}
reload_openwrt_firewall() {
[ "$PLATFORM" = "openwrt" ] || return 0
if ! fw4 check >/dev/null 2>&1; then
die "fw4 validation failed; inspect $OPENWRT_NFT_INCLUDE"
fi
fw4 reload
}
write_openwrt_service() {
cat > "$OPENWRT_INIT" <<EOF
#!/bin/sh /etc/rc.common
USE_PROCD=1
START=95
STOP=10
start_service() {
procd_open_instance
procd_set_param command /usr/bin/mihomo -d /etc/mihomo -f /etc/mihomo/config.yaml
procd_set_param env GOGC=$GOGC_VAL GOMEMLIMIT=$GOMEMLIMIT
procd_set_param respawn 3600 5 5
procd_set_param stdout 1
procd_set_param stderr 1
procd_set_param limits nofile="1048576 1048576"
procd_close_instance
}
EOF
chmod 0755 "$OPENWRT_INIT"
}
write_systemd_service() {
cat > "$SYSTEMD_UNIT" <<EOF
[Unit]
Description=Mihomo low-memory router proxy
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
Environment=GOGC=$GOGC_VAL
Environment=GOMEMLIMIT=$GOMEMLIMIT
ExecStart=/usr/bin/mihomo -d /etc/mihomo -f /etc/mihomo/config.yaml
Restart=on-failure
RestartSec=3
KillMode=mixed
TimeoutStopSec=20
LimitNOFILE=1048576
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
}
configure_memory_watchdog() {
[ "$PLATFORM" = "openwrt" ] || return 0
if [ "$MEMWATCH" = "1" ]; then
cat > "$MEMWATCH_SCRIPT" <<EOF
#!/bin/sh
LIMIT_KB=$MEMWATCH_KB
MIN_AVAILABLE_KB=$MEMWATCH_MIN_AVAILABLE_KB
PID="\$(pidof mihomo 2>/dev/null | awk '{print \$1}')"
[ -n "\$PID" ] || exit 0
RSS="\$(awk '/^VmRSS:/ { print \$2; exit }' /proc/\$PID/status 2>/dev/null)"
[ -n "\$RSS" ] || exit 0
AVAILABLE="\$(awk '/^MemAvailable:/ { print \$2; exit }' /proc/meminfo)"
[ -n "\$AVAILABLE" ] || AVAILABLE=0
if [ "\$RSS" -gt "\$LIMIT_KB" ] &&
[ "\$AVAILABLE" -lt "\$MIN_AVAILABLE_KB" ]; then
logger -t mihomo-memwatch \
"RSS \${RSS}kB, available \${AVAILABLE}kB; restarting mihomo"
/etc/init.d/mihomo stop >/dev/null 2>&1 || true
sleep 1
/etc/init.d/mihomo start
fi
EOF
chmod 0755 "$MEMWATCH_SCRIPT"
if ! crontab -l 2>/dev/null | grep -Fq "$MEMWATCH_SCRIPT"; then
(
crontab -l 2>/dev/null || true
printf '%s\n' "*/5 * * * * $MEMWATCH_SCRIPT"
) | crontab -
fi
/etc/init.d/cron enable >/dev/null 2>&1 || true
/etc/init.d/cron restart >/dev/null 2>&1 ||
/etc/init.d/cron start >/dev/null 2>&1 || true
else
if crontab -l 2>/dev/null | grep -Fq "$MEMWATCH_SCRIPT"; then
crontab -l 2>/dev/null |
grep -Fv "$MEMWATCH_SCRIPT" | crontab -
fi
rm -f "$MEMWATCH_SCRIPT"
fi
}
enable_ip_forwarding() {
current="$(sysctl -n net.ipv4.ip_forward 2>/dev/null || printf '0')"
if [ "$current" != "1" ]; then
log "enabling IPv4 forwarding"
sysctl -w net.ipv4.ip_forward=1 >/dev/null
if [ "$PLATFORM" = "systemd" ]; then
mkdir -p /etc/sysctl.d
cat > /etc/sysctl.d/90-mihomo-router.conf <<'EOF'
net.ipv4.ip_forward = 1
EOF
fi
fi
}
validate_installation() {
"$MIHOMO_BIN" -t -d "$CONFIG_DIR" -f "$CONFIG_FILE"
sh -n "$CONFIG_HELPER"
if [ "$PLATFORM" = "openwrt" ]; then
sh -n "$OPENWRT_INIT"
if [ -f "$OPENWRT_RULE_INCLUDE" ]; then
sh -n "$OPENWRT_RULE_INCLUDE"
fi
if [ "$MEMWATCH" = "1" ]; then
sh -n "$MEMWATCH_SCRIPT"
fi
fi
}
start_service() {
if [ "$PLATFORM" = "openwrt" ]; then
"$OPENWRT_INIT" enable
"$OPENWRT_INIT" start
sleep 3
if ! "$OPENWRT_INIT" running; then
logread -e mihomo | tail -n 50 >&2 || true
die "mihomo did not stay running; see the log above"
fi
else
systemctl enable --now mihomo.service
sleep 3
systemctl is-active --quiet mihomo.service ||
die "mihomo did not stay running; inspect with: journalctl -u mihomo"
fi
ip link show dev "$TUN_IFACE" >/dev/null 2>&1 ||
die "mihomo is running but did not create $TUN_IFACE"
}
main() {
require_root
validate_settings
detect_platform
install_dependencies
detect_wan_interface
compute_memory_settings
ip link show dev "$LAN_IFACE" >/dev/null 2>&1 ||
die "LAN interface $LAN_IFACE does not exist; rerun with LAN_IFACE=<name>"
download_mihomo
install_binary
write_configuration
write_config_helper
if [ "$PLATFORM" = "openwrt" ]; then
write_openwrt_service
configure_openwrt_firewall_zone
configure_openwrt_port_forward_bypass
configure_memory_watchdog
else
write_systemd_service
fi
enable_ip_forwarding
validate_installation
reload_openwrt_firewall
start_service
configure_openwrt_dnsmasq
log "installation complete"
log "platform: $PLATFORM"
log "mihomo: $MIHOMO_VERSION"
log "TUN: system + auto-route + auto-redirect + GSO"
log "memory: GOGC=$GOGC_VAL, GOMEMLIMIT=$GOMEMLIMIT"
log "LAN interface: $LAN_IFACE"
log "configuration: $CONFIG_FILE"
log "replace config: mihomo-config /path/to/config.yaml"
if [ "$PLATFORM" = "openwrt" ]; then
log "disable proxy: $OPENWRT_INIT stop"
log "enable proxy: $OPENWRT_INIT start"
else
log "disable proxy: systemctl stop mihomo"
log "enable proxy: systemctl start mihomo"
fi
}
main "$@"