Загрузка данных


#!/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 "$@"