LogoLogo Light

RouterOS主路由+纯Linux旁路由配置教程 OSS配置

~ RouterOS主路由+纯Linux旁路由配置教程2026-06-28 18:18#Tech #Network #RouterOS #Linux #OpenSUSE

三.OSS配置

OSS的命令与Debian几乎一模一样,只有下载安装时略有不同.
OSS有少数配置文件位置与Debian不同,需自行更换位置.
Linux网卡命名模式有几种,命令中的网卡名字请换成你自己机器的.

第一步 替换软件源

使用以下脚本换源,自行选择合适的源即可,我用的是清华源:

zypper install curl wget # debian使用apt install即可,后续不再提示.OSS标准自带curl和wget,若已安装跳过此行.
bash <(curl -sSL https://linuxmirrors.cn/main.sh)

第二步 设置网络

OSS默认使用的是NetworkManager或Wicked,这里使用NM进行配置,使用其他达到相同配置也可.

首先下载一些常用的包

zypper install sudo nano bind-utils

然后进行配置:

sudo nmcli connection modify eth0 ipv4.addresses 192.168.1.2/24 ipv4.gateway 192.168.1.1 ipv4.dns 223.5.5.5 ipv4.method manual ipv4.route-metric 100 ipv6.method auto ipv6.route-metric 100 ipv6.ip6-privacy 0 ipv6.addr-gen-mode eui64

sudo nmcli connection modify eth1 ipv4.addresses 192.168.10.2/24 ipv4.gateway 192.168.10.1 ipv4.dns 223.5.5.5 ipv4.method manual ipv4.route-metric 50 ipv6.method auto ipv6.ignore-auto-routes yes ipv6.never-default yes ipv6.route-metric 50 ipv6.ip6-privacy 0 ipv6.addr-gen-mode eui64

sudo nmcli connection up eth0 && sudo nmcli connection up eth1

查看eth1的ULA地址(fd00:192:168:10开头的地址)后:

sudo nmcli connection modify eth1 ipv6.routes "::/0 fd00:192:168:10::1 50 src=eth1的ULA地址"
sudo nmcli connection up eth1

有些地址会有残留,重启一下最方便清除.

切回ROS,添加路由:

/ipv6 route
add comment=BGP_FAKEIP disabled=no distance=1 dst-address=fd00:198:18::/48 gateway=eth1的ULA地址 \
    pref-src="" routing-table=bgp-proxy scope=30 target-scope=10

第三步 设置透明代理

OSS标准自带nftables,无需安装

给路由表命名:

mkdir -p /etc/iproute2
cat <<EOF >> /etc/iproute2/rt_tables
# Local tables
#
401 tproxy4
402 int4
403 ext4
601 tproxy6
602 int6
603 ext6
EOF

生成路由脚本:

nano /usr/local/bin/addrt.sh

复制下面的脚本粘贴进去,按ctrl+s保存,ctrl+x退出:

#!/bin/bash

# ==========================================
# 0. 环境与变量初始化
# ==========================================
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

# --- 防抖、锁定与异步重试逻辑 ---
LOCKFILE="/tmp/addrt.lock"
LASTRUN="/tmp/addrt.lastrun"
MIN_INTERVAL=10 # 最小执行间隔(秒)

# 检查当前是否是由脚本发起的“重试模式”
IS_RETRY=false
[ "$1" == "retry_trigger" ] && IS_RETRY=true

# 唯一实例锁定检测
if [ -f "$LOCKFILE" ]; then
    PID=$(cat "$LOCKFILE")
    if ps -p "$PID" > /dev/null 2>&1; then
        if [ "$IS_RETRY" = false ]; then
            echo "⚠️  脚本已在运行 (PID: $PID)。10秒后将自动重试..."
            (sleep 10 && "$0" "retry_trigger") &
        fi
        exit 1
    fi
fi

# 时间间隔防抖检测 (仅对非重试触发生效)
if [ "$IS_RETRY" = false ] && [ -f "$LASTRUN" ]; then
    LAST=$(cat "$LASTRUN")
    NOW=$(date +%s)
    if ((NOW - LAST < MIN_INTERVAL)); then
        echo "⏳ 触发过于频繁 (距离上次 $((NOW - LAST))s)。10秒后延迟执行..."
        (sleep 10 && "$0" "retry_trigger") &
        exit 0
    fi
fi

# 记录当前进程 ID 与运行时间
echo $$ > "$LOCKFILE"
date +%s > "$LASTRUN"
# 确保退出时清理锁文件
trap 'rm -f "$LOCKFILE"' EXIT

# --- 静态配置变量 ---
IF_EXT="eth1"
IF_INT="eth0"

IP_EXT_V4="192.168.10.2"
GW_EXT_V4="192.168.10.1"
LAN_EXT_V4="192.168.10.0/24"
IP_INT_V4="192.168.1.2"
GW_INT_V4="192.168.1.1"
LAN_INT_V4="192.168.1.0/24"

GW_EXT_V6="fd00:192:168:10::1"
LAN_EXT_V6="fd00:192:168:10::/64"

# 已移除硬编码的 IP_INT_V6,改为后续动态获取
GW_INT_V6="fd00:192:168:1::1"
LAN_INT_V6="fd00:192:168:1::/64"

T_TP_V4_NAME="tproxy4"
T_INT_V4_NAME="int4"
T_EXT_V4_NAME="ext4"
T_TP_V6_NAME="tproxy6"
T_INT_V6_NAME="int6"
T_EXT_V6_NAME="ext6"
MARK_TPROXY="0x1"
MARK_EXT="0x2"
MARK_INT="0x3"

# ==========================================
# 1. 核心功能函数
# ==========================================
del_rule() {
    local VER=$1
    local PRIO=$5
    while ip $VER rule show | grep -q "^$PRIO:"; do
        ip $VER rule del priority $PRIO > /dev/null 2>&1 || break
    done
}

safe_add_rule() {
    local VER=$1
    local TYPE=$2
    local VALUE=$3
    local TABLE=$4
    local PRIO=$5
    [ -z "$VALUE" ] && return
    local MATCH_VALUE=$(echo "$VALUE" | tr '[:upper:]' '[:lower:]')
    if ! ip $VER rule show 2> /dev/null | grep -E "^$PRIO:" | grep -qi "$TYPE $MATCH_VALUE lookup $TABLE"; then
        echo "  + Adding Rule: $TYPE $VALUE -> Table $TABLE (Prio: $PRIO)"
        ip $VER rule add $TYPE $VALUE lookup $TABLE priority $PRIO
    fi
}

safe_add_route() {
    local VER=$1
    local GW=$2
    local DEV=$3
    local TABLE=$4
    local LAN=$5
    local SRC=$6
    [ -z "$GW" ] && return
    if ! ip $VER route show table "$TABLE" 2> /dev/null | grep -q "default via $GW dev $DEV"; then
        echo "  * Updating Table $TABLE"
        ip $VER route flush table "$TABLE" > /dev/null 2>&1
        ip $VER route add default via "$GW" dev "$DEV" table "$TABLE" ${SRC:+src $SRC} 2> /dev/null ||
            ip $VER route add default via "$GW" dev "$DEV" table "$TABLE"
        [ -n "$LAN" ] && ip $VER route add "$LAN" dev "$DEV" scope link table "$TABLE"
    fi
}

safe_add_local() {
    local VER=$1
    local TABLE=$2
    if ! ip $VER route show table "$TABLE" 2> /dev/null | grep -q "local default dev lo"; then
        ip $VER route flush table "$TABLE" > /dev/null 2>&1
        [ "$VER" == "-4" ] && ip route add local 0.0.0.0/0 dev lo table "$TABLE" || ip -6 route add local ::/0 dev lo table "$TABLE"
    fi
}

# 获取所有有效的 IPv6 地址 (GUA 和 ULA)
get_all_valid_ipv6() {
    local iface=$1
    # 过滤过期的、检测中的以及 fe80(Link-Local)
    ip -6 addr show dev "$iface" scope global |
        grep "inet6" |
        grep -vE "deprecated|tentative" |
        awk '{print $2}' |
        grep -v "^fe80" |
        cut -d/ -f1
}

# ==========================================
# 2. 清理逻辑
# ==========================================
do_clean() {
    echo "--- 正在清理所有策略路由规则与表 ---"
    for p in 999 1000 1001 1002 1003 1004 1005; do
        del_rule "-4" "" "" "" "$p"
        del_rule "-6" "" "" "" "$p"
    done
    for tbl in "$T_TP_V4_NAME" "$T_INT_V4_NAME" "$T_EXT_V4_NAME" "$T_TP_V6_NAME" "$T_INT_V6_NAME" "$T_EXT_V6_NAME"; do
        ip route flush table "$tbl" > /dev/null 2>&1
        ip -6 route flush table "$tbl" > /dev/null 2>&1
    done
    ip route flush cache
    echo "✅ 清理完成。"
}

# ==========================================
# 3. 主程序逻辑
# ==========================================
if [ "$EUID" -ne 0 ]; then
    echo "Error: 请使用 root 权限"
    exit 1
fi
if [ "$1" == "clean" ]; then
    do_clean
    exit 0
fi

echo "--- 策略路由增量检测开始 $(date) ---"

# [Step 1] 优先动态等待并获取 eth1 所有有效的 IPv6 地址 (GUA + ULA)
echo "[Step 1] 正在获取 $IF_EXT 的有效 IPv6 地址..."
WAIT_COUNT=0
MAX_RETRIES=15
ALL_EXT_V6=""

while [ $WAIT_COUNT -lt $MAX_RETRIES ]; do
    ALL_EXT_V6=$(get_all_valid_ipv6 "$IF_EXT")
    # 因为出站主要依赖 ULA,所以这里以检测到 f[cd] 开头的 ULA 地址作为网络就绪的标志
    if echo "$ALL_EXT_V6" | grep -qiE "^f[cd]"; then
        echo "✅ 获取到以下有效 IPv6 地址:"
        echo "$ALL_EXT_V6" | sed 's/^/   - /'
        break
    fi
    echo "   ...等待有效 ULA 地址就绪 ($((WAIT_COUNT + 1))/$MAX_RETRIES)"
    sleep 1
    ((WAIT_COUNT++))
done

if [ -z "$ALL_EXT_V6" ] || ! echo "$ALL_EXT_V6" | grep -qiE "^f[cd]"; then
    echo "❌ 错误: 超时未获取到 $IF_EXT 的 ULA 地址。退出。"
    exit 1
fi

# [Step 1.5] 动态获取 eth0 的有效 ULA 地址
echo "[Step 1.5] 正在获取 $IF_INT 的有效 ULA 地址..."
WAIT_COUNT_INT=0
IP_INT_V6=""

while [ $WAIT_COUNT_INT -lt $MAX_RETRIES ]; do
    ALL_INT_V6=$(get_all_valid_ipv6 "$IF_INT")
    # 提取以 f[cd] 开头的 ULA
    IP_INT_V6=$(echo "$ALL_INT_V6" | grep -iE "^f[cd]" | head -n 1)
    if [ -n "$IP_INT_V6" ]; then
        echo "✅ 获取到 $IF_INT 的 ULA 地址: $IP_INT_V6"
        break
    fi
    echo "   ...等待 $IF_INT 的 ULA 地址就绪 ($((WAIT_COUNT_INT + 1))/$MAX_RETRIES)"
    sleep 1
    ((WAIT_COUNT_INT++))
done

if [ -z "$IP_INT_V6" ]; then
    echo "❌ 错误: 超时未获取到 $IF_INT 的 ULA 地址。退出。"
    exit 1
fi

# [Step 2] 获取成功后,清理 eth1 (IF_EXT) 上已经过时(deprecated)的旧地址
DEPRECATED_IPS=$(ip -6 addr show dev "$IF_EXT" scope global | grep "deprecated" | awk '{print $2}')
if [ -n "$DEPRECATED_IPS" ]; then
    echo "[Step 2] 正在清理 $IF_EXT 过时地址..."
    echo "$DEPRECATED_IPS" | xargs -r -I {} ip -6 addr del {} dev "$IF_EXT"
else
    echo "[Step 2] $IF_EXT 无过时地址,无需清理。"
fi

# 应用配置 (IPv4)
safe_add_local "-4" "$T_TP_V4_NAME"
safe_add_rule "-4" "fwmark" "$MARK_TPROXY" "$T_TP_V4_NAME" "999"

safe_add_route "-4" "$GW_INT_V4" "$IF_INT" "$T_INT_V4_NAME" "$LAN_INT_V4" "$IP_INT_V4"
safe_add_rule "-4" "from" "$IP_INT_V4" "$T_INT_V4_NAME" "1001"
safe_add_rule "-4" "fwmark" "$MARK_INT" "$T_INT_V4_NAME" "1003"
safe_add_rule "-4" "oif" "$IF_INT" "$T_INT_V4_NAME" "1004"

safe_add_route "-4" "$GW_EXT_V4" "$IF_EXT" "$T_EXT_V4_NAME" "$LAN_EXT_V4" "$IP_EXT_V4"
safe_add_rule "-4" "from" "$IP_EXT_V4" "$T_EXT_V4_NAME" "1000"
safe_add_rule "-4" "fwmark" "$MARK_EXT" "$T_EXT_V4_NAME" "1002"
safe_add_rule "-4" "oif" "$IF_EXT" "$T_EXT_V4_NAME" "1005"

# 应用配置 (IPv6)
safe_add_local "-6" "$T_TP_V6_NAME"
safe_add_rule "-6" "fwmark" "$MARK_TPROXY" "$T_TP_V6_NAME" "999"

safe_add_route "-6" "$GW_INT_V6" "$IF_INT" "$T_INT_V6_NAME" "$LAN_INT_V6" "$IP_INT_V6"
safe_add_rule "-6" "from" "$IP_INT_V6" "$T_INT_V6_NAME" "1001"
safe_add_rule "-6" "fwmark" "$MARK_INT" "$T_INT_V6_NAME" "1003"
safe_add_rule "-6" "oif" "$IF_INT" "$T_INT_V6_NAME" "1004"

# 提取 ULA (以 fd 或 fc 开头) 作为 EXT 表默认路由的主动出站源 IP
PRIMARY_ULA_V6=$(echo "$ALL_EXT_V6" | grep -iE "^f[cd]" | head -n 1)
safe_add_route "-6" "$GW_EXT_V6" "$IF_EXT" "$T_EXT_V6_NAME" "$LAN_EXT_V6" "$PRIMARY_ULA_V6"

# 为 eth1 上获取到的所有 IPv6 地址 (GUA 和 ULA) 都添加 from 规则
# 确保 Nginx 收到 GUA 的请求时,回复的数据包可以命中策略路由,原路从 eth1 出去
for ip6 in $ALL_EXT_V6; do
    safe_add_rule "-6" "from" "$ip6" "$T_EXT_V6_NAME" "1000"
done

safe_add_rule "-6" "fwmark" "$MARK_EXT" "$T_EXT_V6_NAME" "1002"
safe_add_rule "-6" "oif" "$IF_EXT" "$T_EXT_V6_NAME" "1005"

ip route flush cache
echo "✅ 增量配置完成。"

赋权并执行:

chmod +x /usr/local/bin/addrt.sh
addrt.sh

生成nft配置文件:

nano /usr/local/bin/nftables.conf

复制脚本粘贴进去:

#!/usr/sbin/nft -f

flush ruleset

define PROXY_PORT = 12345
define MARK_PROXY = 1
define MARK_EXT   = 2
define MARK_INT  = 3

table inet xray_mangle {

    map iface_to_mark {
        type ifname : mark
        elements = { "eth1" : $MARK_EXT, "eth0" : $MARK_INT }
    }

    set fake_ip_v4 { type ipv4_addr; flags interval; elements = { 198.18.0.0/15 } }
    set fake_ip_v6 { type ipv6_addr; flags interval; elements = { fd00:198:18::/48 } }

    set private_ip_v4 {
        type ipv4_addr; flags interval;
        elements = {
            0.0.0.0/8,          # 当前网络 (RFC 1122)
            10.0.0.0/8,         # 私有网络 (RFC 1918) -> 包含你的 FakeIP,依靠规则优先级处理
            100.64.0.0/10,      # 运营商级 NAT (RFC 6598)
            127.0.0.0/8,        # 本地回环 (RFC 1122)
            169.254.0.0/16,     # 链路本地地址 (RFC 3927)
            172.16.0.0/12,      # 私有网络 (RFC 1918)
            192.0.0.0/24,       # IETF 协议分配 (RFC 6890)
            192.0.2.0/24,       # 文档示例 (RFC 5737)
            192.88.99.0/24,     # 6to4 中继 (RFC 3068) - 虽然废弃但保留无害
            192.168.0.0/16,     # 私有网络 (RFC 1918)
            # 198.18.0.0/15,       网络基准测试 (RFC 2544)
            198.51.100.0/24,    # 文档示例 (RFC 5737)
            203.0.113.0/24,     # 文档示例 (RFC 5737)
            224.0.0.0/4,        # 组播地址 (RFC 5771)
            240.0.0.0/4        # 保留地址 (RFC 1112)
        }
    }
    
    set private_ip_v6 {
        type ipv6_addr; flags interval;
        elements = {
            ::/128,             # 未指定地址
            ::1/128,            # 本地回环
            ::ffff:0:0/96,      # IPv4 映射地址
            64:ff9b::/96,       # NAT64 前缀
            100::/64,           # 丢弃前缀 (RFC 6666)
            2001::/32,          # Teredo (RFC 4380)
            2001:20::/28,       # ORCHIDv2 (RFC 7343)
            2001:db8::/32,      # 文档示例 (RFC 3849)
            2002::/16,          # 6to4 (RFC 3056)
            fc00::/7,           # 唯一本地地址 (ULA)
            fe80::/10,          # 链路本地地址
            ff00::/8            # 组播地址
        }
    }
    
    chain prerouting {
        type filter hook prerouting priority mangle; policy accept;
        fib daddr type local accept
        meta mark set ct mark
        meta mark $MARK_PROXY jump handle_proxy
        ip daddr @fake_ip_v4 jump handle_proxy
        ip6 daddr @fake_ip_v6 jump handle_proxy
        ip daddr @private_ip_v4 accept
        ip6 daddr @private_ip_v6 accept
        iifname "eth1" jump handle_proxy
        accept
    }

    chain handle_proxy {
        meta l4proto { tcp, udp } tproxy to :$PROXY_PORT meta mark set $MARK_PROXY
        ct mark set $MARK_PROXY
        accept
    }

    chain output {
        type route hook output priority mangle; policy accept;
        ct mark $MARK_PROXY return
        meta mark set ct mark
    }
}

赋权并执行:

chmod +x /usr/local/bin/nftables.conf
/usr/local/bin/nftables.conf

检查是否生效:

# 检查nftables规则
nft list ruleset
# 检查路由
ip rule show
ip route show
ip -6 rule show
ip -6 route show 

设置开机自启:

cat <<EOF > /etc/systemd/system/addrt.service
[Unit]
Description=Custom Network Configuration (Nftables & Routes)
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
# 增加这一行:循环检查直到发现 global scope 的 IPv6 地址
ExecStartPre=/bin/bash -c 'until ip -6 addr show dev eth1 scope global | grep -q "inet6"; do sleep 1; done'
ExecStartPre=/usr/sbin/nft -f /usr/local/bin/nftables.conf
ExecStart=/bin/bash /usr/local/bin/addrt.sh

[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable addrt.service

现在,OSS端的透明代理和源进源出已设置完成,

第四步 安装docker及相关容器

也可以使用docker compose,我就按最简配置来了,本教程所有容器都用的是host模式.

zypper install docker

修改docker配置并重启docker:

cat <<EOF > /etc/docker/daemon.json
{
  "log-level": "warn",
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "5"
  },
  "selinux-enabled": true,
  "iptables": false,
  "ip6tables": false,
  "bridge": "none",
  "registry-mirrors": [
    "https://docker.1ms.run"
  ]
}
EOF
systemctl restart docker.service

安装PaoPaoDNS,用于DNS解析,去广告等:

docker run -d \
--name paopaodns \
--net=host \
--user root \
--privileged \
-v /usr/local/etc/paopaodns:/data \
--restart always \
sliamb/paopaodns

修改eth0和eth1的DNS:

nmcli connection modify eth0 ipv4.dns ""
nmcli connection modify eth0 ipv4.dns 192.168.1.2
nmcli connection up eth0
nmcli connection modify eth1 ipv4.dns ""
nmcli connection modify eth1 ipv4.dns 192.168.1.2
nmcli connection up eth1

进入ROS,修改相应的DNS:

/ip dns set servers=192.168.1.2,"eth0的ULA地址"
/ip dhcp-server network set [find] dns-server=192.168.1.2
/ip dhcp-server option set [find name=DNS] value="'192.168.1.2'"
/ipv6 nd set [find] dns="eth0的ULA地址"

安装Sub-Store,用于订阅转换:

docker run -it -d \
--restart=always \
--net=host \
--user root \
-e SUB_STORE_FRONTEND_BACKEND_PATH=/20位随机密码 \
-v /usr/local/etc/sub-store:/opt/app/data:z \
--name sub-store \
xream/sub-store:latest

打开192.168.1.2:3001,输入你的随机密码进入界面:

ss1.png

ss2.png

名称随便写,来源选远程,链接填你机场的订阅链接,然后保存

ss3.png

添加同步配置,名称随便写,来源选刚才的订阅,目标平台选singbox,或者你需要使用别的代理就换成别的

ss4.png

保存下来订阅链接,你们的应该是一个 http://192.168.1.2 开头的本地连接

第五步 安装代理

考虑到大家可能用机场的比较多,所以这里以Mihomo为例.Sing-Box/Xray只要使用透明代理配置即可.
具体代理配置并不是本文的重点,Mihomo在网上有很多很多现成的配置文件,我这里只做最简化配置,让代理能够正常运行和使用.
这里直接使用我找AI写的脚本安装:

创建脚本文件:

nano /usr/local/bin/install.sh

复制以下脚本粘贴并保存:

#!/bin/bash
set -e

# ==========================================
# 1. 参数解析与环境准备
# ==========================================
SUB_URL="$1"
VERSION_INPUT="$2"

GH_PROXY="https://ghfast.top/"
ARCH=$(uname -m)
case "$ARCH" in
    x86_64) SB_ARCH="amd64" ;;
    aarch64) SB_ARCH="arm64" ;;
    *)
        echo "❌ 不支持的架构"
        exit 1
        ;;
esac

BIN_DIR="/usr/local/bin"
CONF_DIR="/etc/sing-box"
LOG_DIR="/var/log/sing-box"

# 自动安装 jq
if ! command -v jq &> /dev/null; then
    echo "📦 安装必备工具 jq..."
    if command -v zypper &> /dev/null; then
        sudo zypper install -y jq
    elif command -v apt-get &> /dev/null; then sudo apt-get update && sudo apt-get install -y jq; fi
fi

# ==========================================
# 2. 版本获取与日志权限加固 (777)
# ==========================================
echo "⏳ 获取版本号..."
if [ -n "$VERSION_INPUT" ]; then
    VERSION="$VERSION_INPUT"
    [[ $VERSION != v* ]] && VERSION="v$VERSION"
else
    VERSION=$(curl -s https://api.github.com/repos/SagerNet/sing-box/releases/latest | jq -r .tag_name)
    [ "$VERSION" == "null" ] || [ -z "$VERSION" ] && VERSION="v1.11.0"
fi
echo "✅ 目标版本: $VERSION"

echo "📂 正在创建日志目录并强制加权 777..."
sudo mkdir -p "$LOG_DIR"
sudo touch "$LOG_DIR/sing-box.log"
sudo chmod -R 777 "$LOG_DIR"

# ==========================================
# 3. 通过镜像源下载并安装
# ==========================================
cd /tmp
TAR_NAME="sing-box-${VERSION#v}-linux-${SB_ARCH}"
TAR_FILE="${TAR_NAME}.tar.gz"
echo "⬇️ 正在下载 sing-box $VERSION..."
wget -q --show-progress -O "$TAR_FILE" "${GH_PROXY}https://github.com/SagerNet/sing-box/releases/download/${VERSION}/${TAR_FILE}"
tar -xzf "$TAR_FILE"
sudo cp "${TAR_NAME}/sing-box" "$BIN_DIR/"
sudo chmod +x "$BIN_DIR/sing-box"
rm -rf "$TAR_FILE" "${TAR_NAME}"

# ==========================================
# 4. 配置文件处理 (保证严格的紧凑格式)
# ==========================================
sudo mkdir -p "$CONF_DIR"

cat > /tmp/sb_template.json << 'EOF'
{
  "log": {"disabled":false,"level":"warn","output":"/var/log/sing-box/sing-box.log","timestamp":true},
  "dns": {"servers":[{"type":"udp","tag":"forcerealip","server":"223.5.5.5"},{"type":"fakeip","tag":"fakedns","inet4_range":"198.18.0.0/15","inet6_range":"fd00:198:18::/48"}],"rules":[{"inbound":["dnsin"],"server":"fakedns"}]},
  "inbounds": [
    {"type":"tproxy","tag":"tproxyin","listen":"::","listen_port":12345,"tcp_fast_open":true},
    {"type":"socks","tag":"socksin","listen":"192.168.1.2","listen_port":3426,"tcp_fast_open":true},
    {"type":"direct","tag":"dnsin","listen":"192.168.1.2","listen_port":53535,"tcp_fast_open":true}
  ],
  "outbounds": [
    {"type":"selector","tag":"🎯 Main ꜱᴇʟᴇᴄᴛᴏʀ","outbounds":["⚡ Main ᴀᴜᴛᴏ"__TAGS_PLACEHOLDER__],"default":"⚡ Main ᴀᴜᴛᴏ"},
    {"type":"urltest","tag":"⚡ Main ᴀᴜᴛᴏ","outbounds":[__TAGS_PLACEHOLDER_NO_COMMA__],"url":"https://www.gstatic.com/generate_204","tolerance":10}__NODES_PLACEHOLDER__
  ],
  "route": {
    "rules": [
      {"inbound":["socksin","tproxyin"],"action":"sniff"},
      {"inbound":["dnsin"],"action":"hijack-dns"},
      {"inbound":["socksin"],"action":"route","outbound":"🎯 Main ꜱᴇʟᴇᴄᴛᴏʀ"},
      {"inbound":["tproxyin"],"action":"route","outbound":"🎯 Main ꜱᴇʟᴇᴄᴛᴏʀ"}
    ],
    "final": "🎯 Main ꜱᴇʟᴇᴄᴛᴏʀ",
    "default_domain_resolver": "forcerealip"
  },
  "experimental": {"cache_file":{"enabled":true,"store_fakeip":true},"clash_api":{"external_controller":"0.0.0.0:9898","external_ui":"ui","external_ui_download_url":"https://github.com/MetaCubeX/MetaCubeXD/archive/refs/heads/gh-pages.zip","external_ui_download_detour":"🎯 Main ꜱᴇʟᴇᴄᴛᴏʀ","secret":"passwd","default_mode":"rule","access_control_allow_origin":["*"],"access_control_allow_private_network":true}}
}
EOF

export TAGS_COMMA=""
export TAGS_NO_COMMA=""
export NODES_STR=""

if [ -n "$SUB_URL" ]; then
    echo "🔗 正在拉取订阅并注入节点..."
    if curl -s -f -L "$SUB_URL" -o /tmp/sub.json; then
        TAGS_STR=$(jq -c '(if type == "array" then . else .outbounds end) | map(select(type == "object" and .type != "selector" and .type != "urltest" and .type != "direct" and .type != "block" and .type != "dns")) | map(.tag)' /tmp/sub.json)
        
        if [ "$TAGS_STR" != "[]" ] && [ -n "$TAGS_STR" ]; then
            TAGS_INNER=$(echo "$TAGS_STR" | sed 's/^\[//;s/\]$//')
            export TAGS_COMMA=",${TAGS_INNER}"
            export TAGS_NO_COMMA="${TAGS_INNER}"
            export NODES_STR=$(jq -r '(if type == "array" then . else .outbounds end) | map(select(type == "object" and .type != "selector" and .type != "urltest" and .type != "direct" and .type != "block" and .type != "dns")) | map(",\n    " + tostring) | join("")' /tmp/sub.json)
        fi
    else
        echo "⚠️ 订阅拉取失败,将使用默认无节点模板。"
    fi
fi

awk '
BEGIN {
    tags_comma = ENVIRON["TAGS_COMMA"]
    tags_no_comma = ENVIRON["TAGS_NO_COMMA"]
    nodes = ENVIRON["NODES_STR"]
}
{
    while (index($0, "__TAGS_PLACEHOLDER__") > 0) {
        idx = index($0, "__TAGS_PLACEHOLDER__")
        $0 = substr($0, 1, idx - 1) tags_comma substr($0, idx + 20)
    }
    while (index($0, "__TAGS_PLACEHOLDER_NO_COMMA__") > 0) {
        idx = index($0, "__TAGS_PLACEHOLDER_NO_COMMA__")
        $0 = substr($0, 1, idx - 1) tags_no_comma substr($0, idx + 29)
    }
    while (index($0, "__NODES_PLACEHOLDER__") > 0) {
        idx = index($0, "__NODES_PLACEHOLDER__")
        $0 = substr($0, 1, idx - 1) nodes substr($0, idx + 21)
    }
    print
}' /tmp/sb_template.json > /tmp/final_config.json

sudo mv /tmp/final_config.json "$CONF_DIR/config.json"
rm -f /tmp/sb_template.json /tmp/sub.json

# ==========================================
# 5. 创建 Systemd 服务 (新增步骤)
# ==========================================
echo "⚙️ 正在配置 systemd 服务..."
sudo cat > /etc/systemd/system/sing-box.service << 'EOF'
[Unit]
Description=sing-box service
Documentation=https://sing-box.sagernet.org
After=network.target nss-lookup.target network-online.target

[Service]
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
ExecStart=/usr/local/bin/sing-box run -c /etc/sing-box/config.json
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=10s
LimitNOFILE=infinity

[Install]
WantedBy=multi-user.target
EOF

# ==========================================
# 6. 服务启动
# ==========================================
echo "🚀 正在启动服务..."
sudo systemctl daemon-reload
sudo systemctl enable sing-box
sudo systemctl restart sing-box

echo "=========================================="
echo "🎉 sing-box $VERSION 安装完成!"
echo "📂 日志目录: $LOG_DIR (权限 777)"
echo "🚀 配置文件: $CONF_DIR/config.json"
echo "=========================================="

加权并执行:

chmod +x /usr/local/bin/install.sh
/usr/local/bin/install.sh 刚才的sub-store生成的订阅链接

这时候应该可以访问外网了,先用socks5测试一下:

curl -x socks5h://192.168.1.2:3426 www.google.com

第六步 设置PaoPaoDNS

先关闭PPDNS:

docker stop paopaodns

修改配置文件:

touch /usr/local/etc/paopaodns/adlist.txt

cat <<EOF > /usr/local/etc/paopaodns/custom_env.ini
# Variables configured here
# override the ENV at docker startup.
# MosDNS reload if Modifying this file.
# Format: key="value"

#ADDINFO="yes"
#SHUFFLE="yes"
#IPV6="no"
#MORE_ENV="VALUE"...
#test_ip="1.1.1.1 2.2.2.2 2000::1 2002::2"
SOCKS5="192.168.1.2:3426"
CUSTOM_FORWARD="192.168.1.2:53535"
CNAUTO="yes"
DNSPORT="53"
IPV6="yes_yes"
UPDATE="daily"
SERVER_IP="192.168.1.2"
AUTO_FORWARD="yes"
CNFALL="yes"
DNS_SERVERNAME="PPDNS"
RULES_TTL="600"
EOF

truncate -s 0

nano /usr/local/etc/paopaodns/mosdns.yaml

将以下内容复制粘贴进去并保存:

log:
    file: ""
    level: error

plugins:

##zones_dns_start##
##zones_dns_end##

##zones_seq_start##
##zones_seq_end##

#usehosts-yes  - tag: "usehosts"
#usehosts-yes    type: "hosts"
#usehosts-yes    args:
#usehosts-yes#serverip-enable      entries:
#usehosts-yes#serverip-enable        - "paopao.dns {SERVER_IP}"
#usehosts-yes#usehosts-enable      files:
#usehosts-yes#usehosts-enable        - "/tmp/hosts.txt" 

  - tag: "adlist"
    type: "domain_set"
    args:
      files:
        - "/data/adlist.txt"

  - tag: "force_recurse_list"
    type: "domain_set"
    args:
      files:
        - "/tmp/force_recurse_list.txt"

  - tag: "force_dnscrypt_list"
    type: "domain_set"
    args:
      files:
        - "/tmp/force_dnscrypt_list.txt"
#cntracker-yes        - "/tmp/cn_tracker_list.txt"

#ttl_rule_ok  - tag: "force_ttl_rules"
#ttl_rule_ok    type: "domain_set"
#ttl_rule_ok    args:
#ttl_rule_ok      files:
#ttl_rule_ok        - "/tmp/force_ttl_rules.txt"

#global_mark_yes  - tag: "global_mark"
#global_mark_yes    type: "domain_set"
#global_mark_yes    args:
#global_mark_yes      files:
#global_mark_yes        - "/tmp/global_mark.dat"

#global_mark_yes  - tag: "cn_mark"
#global_mark_yes    type: "domain_set"
#global_mark_yes    args:
#global_mark_yes      files:
#global_mark_yes        - "/tmp/cn_mark.dat"
#global_mark_yes        - "/tmp/custom_cn_mark.txt"

#global_mark_yes  - tag: "global_mark_cn"
#global_mark_yes    type: "domain_set"
#global_mark_yes    args:
#global_mark_yes      files:
#global_mark_yes        - "/tmp/global_mark_cn.dat"
#global_mark_yes        - "/tmp/custom_cn_mark.txt"

#customforward-seted  - tag: "force_forward_list"
#customforward-seted    type: "domain_set"
#customforward-seted    args:
#customforward-seted      files:
#customforward-seted        - "/tmp/force_forward_list.txt"

#cnfall  - tag: cn_test
#cnfall    type: forward
#cnfall    args:
#cnfall      concurrent: 3
#cnfall      upstreams:
#cnfall        - addr: "udp://127.0.0.1:5301"
#cnfall        - addr: "udp://223.5.5.5"
#cnfall        - addr: "udp://119.29.29.29"

  - tag: local_unbound
    type: forward
    args:
      allowcode: 23
      upstreams:
        - addr: "udp://127.0.0.1:5301"

#cnfall  - tag: local_unbound_fall
#cnfall    type: forward
#cnfall    args:
#cnfall      qtime: 3
#flushd_un_yes#cnfall      flush: 1
#cnfall      upstreams:
#cnfall        - addr: "udp://127.0.0.1:5301"

#customforward-seted  - tag: force_forward
#customforward-seted    type: forward
#customforward-seted    args:
#customforward-seted      allowcode: 23
#customforward-seted      upstreams:
#customforward-seted        - addr: "udp://{CUSTOM_FORWARD}"

  - tag: forward_unbound
    type: forward
    args:
      allowcode: 23
      concurrent: 2
      upstreams:
        - addr: "udp://127.0.0.1:5304"
#nosocks        - addr: "udp://127.0.0.1:5302"
#socksok        - addr: "udp://127.0.0.1:5303"

  - tag: forward_dnscrypt
    type: forward
    args:
      upstreams:
        - addr: "udp://127.0.0.1:5302"

  - tag: cnip
    type: mmdb
    args:
      file: "/tmp/Country.mmdb"

  - tag: accept
    type: sequence
    args:
#liteshuffle        - exec: shuffle 2
#trncshuffle        - exec: shuffle 4
##swaps_match_start##
##swaps_match_end##
        - exec: ok

  - tag: not_a_aaaa
    type: sequence
    args:
        - exec: $local_unbound
#addinfo        - exec: addinfo not_a_aaaa -> local_unbound
        - matches: has_wanted_ans
          exec: goto accept
        - exec: drop_resp
        - exec: $forward_unbound
#addinfo        - exec: addinfo not_a_aaaa -> forward_unbound
        - matches: has_wanted_ans
          exec: goto accept
        - exec: drop_resp

#customforward-seted  - tag: f_force_forward_list
#customforward-seted    type: sequence
#customforward-seted    args:
#customforward-seted        - exec: drop_resp
#customforward-seted        - exec: $force_forward
#addinfo#customforward-seted        - exec: addinfo forward -> {CUSTOM_FORWARD_SERVER}@{CUSTOM_FORWARD_PORT}
#CUSTOM_FORWARD_TTL#customforward-seted        - exec: ttl {CUSTOM_FORWARD_TTL}-0
#customforward-seted        - exec: goto accept

  - tag: f_force_dnscrypt_list
    type: sequence
    args:
        - exec: drop_resp
#ipv6cn_only6        - matches: 
#ipv6cn_only6          - qtype 28
#ipv6cn_only6          exec: prefer_ipv4
        - exec: $forward_unbound

#addinfo        - exec: addinfo nocn forward_unbound
        - matches: has_wanted_ans
          exec: goto accept
        - exec: $forward_dnscrypt
#addinfo        - exec: addinfo nocn forward_dnscrypt
        - matches: has_wanted_ans
          exec: goto accept
        - exec: pongerr END by f_force_dnscrypt_list.

  - tag: f_local_unbound
    type: sequence
    args:
        - exec: $local_unbound
#addinfo        - exec: addinfo local_unbound
        - matches: has_wanted_ans
          exec: goto accept
        - exec: pongerr END by f_local_unbound.
        
#ttl_rule_ok  - tag: f_force_ttl_rules
#ttl_rule_ok    type: sequence
#ttl_rule_ok    args:
#ttl_rule_ok        - exec: $forward_dnscrypt
#ttl_rule_ok        - exec: ttl 0-{RULES_TTL}
#addinfo#ttl_rule_ok        - exec: addinfo force_ttl_rules
#ttl_rule_ok        - exec: goto accept

#cnfall  - tag: try_cn_fall
#cnfall    type: sequence
#cnfall    args:
#cnfall        - exec: drop_resp
#cnfall        - exec: $cn_test
#cnfall#addinfo        - exec: addinfo try_cn_fall

#autoforward-yes#autoforward-check  - tag: try_auto_check
#autoforward-yes#autoforward-check    type: sequence
#autoforward-yes#autoforward-check    args:
#autoforward-yes#autoforward-check        - exec: drop_resp
#autoforward-yes#autoforward-check        - exec: $forward_unbound
#addinfo#autoforward-yes#autoforward-check        - exec: addinfo AUTO_FORWARD_CHECK forward_unbound PRIVATE
#autoforward-yes#autoforward-check        - matches: resp_ip_mmdb $cnip PRIVATE
#autoforward-yes#autoforward-check          exec: goto accept
#autoforward-yes#autoforward-check        - matches: "has_wanted_ans"
#autoforward-yes#autoforward-check          exec: goto f_force_forward_list
#autoforward-yes#autoforward-check        - exec: pongerr END by try_auto_check.

#global_mark_yes  - tag: f_global_mark
#global_mark_yes    type: sequence
#global_mark_yes    args:
#global_mark_yes        - matches: qname $global_mark_cn
#global_mark_yes          exec: return

#global_mark_yes#ipv6yes        - matches: 
#global_mark_yes#ipv6yes          - qtype 28
#global_mark_yes#ipv6yes          exec: pong END by IPv6=yes, MARK_DATA block aaaa.
#global_mark_yes#ipv6cn_only6        - matches: 
#global_mark_yes#ipv6cn_only6          - qtype 28
#global_mark_yes#ipv6cn_only6          exec: prefer_ipv4
#global_mark_yes#autoforward-yes#autoforward-check        - exec: goto try_auto_check
#global_mark_yes#autoforward-yes#autoforward-nocheck        - exec: $force_forward
#global_mark_yes#autoforward-no        - exec: goto f_force_dnscrypt_list


#usehosts-yes  - tag: accept_hosts
#usehosts-yes    type: sequence
#usehosts-yes    args:
#usehosts-yes        - exec: $usehosts
#usehosts-yes        - matches: has_wanted_ans
#usehosts-yes          exec: goto accept

  - tag: main_sequence
    type: sequence
    args:

##zones_qname_top_start##
##zones_qname_top_end##

#usehosts-yes        - exec: jump accept_hosts

        - matches: qname $adlist
          exec: reject 3

        - matches: "qtype 64 65"
          exec: pong END by block qtype 64/65.

#ipv6no        - matches: 
#ipv6no          - qtype 28
#ipv6no          exec: pong END by IPv6=no.

#ipv6yes        - matches: 
#ipv6yes          - qname $force_dnscrypt_list
#ipv6yes          - qtype 28
#ipv6yes          exec: pong END by IPv6=yes, force_dnscrypt_list block aaaa.

#ipv6only6        - exec: prefer_ipv4

##zones_qname_top6_start##
##zones_qname_top6_end##

        - matches: 
          - "!qtype 1 28"
          exec: jump not_a_aaaa
#customforward-seted        - matches: qname $force_forward_list
#customforward-seted          exec: goto f_force_forward_list
        - matches: qname $force_dnscrypt_list
          exec: goto f_force_dnscrypt_list
        - matches: qname $force_recurse_list
          exec: goto f_local_unbound
 
#ttl_rule_ok        - matches: qname $force_ttl_rules
#ttl_rule_ok          exec: goto f_force_ttl_rules

##zones_qname_list_start##
##zones_qname_list_end##

#global_mark_yes        - matches: qname $global_mark
#global_mark_yes          exec: jump f_global_mark

#nofall        - exec: $local_unbound
#cnfall        - exec: $local_unbound_fall
#cnfall        - matches: "!rcode 0"
#cnfall          exec: jump try_cn_fall
#global_mark_yes#addinfo        - exec: addinfo cn_mark
#global_mark_yes        - matches: qname $cn_mark
#global_mark_yes          exec: goto accept
#addinfo        - exec: addinfo mmdb CN IP
        - matches: resp_ip_mmdb $cnip CN
          exec: goto accept

#ipv6yes        - matches: 
#ipv6yes          - qtype 28
#ipv6yes          exec: pong END by IPv6=yes, NO CN IP block aaaa.

#ipv6cn_only6        - matches: 
#ipv6cn_only6          - qtype 28
#ipv6cn_only6          exec: prefer_ipv4

#autoforward-yes#autoforward-check        - matches: 
#autoforward-yes#autoforward-check          - "!resp_ip_mmdb $cnip PRIVATE"
#autoforward-yes#autoforward-check          - "has_wanted_ans"
#autoforward-yes#autoforward-check          exec: goto f_force_forward_list
#autoforward-yes#autoforward-check        - exec: goto try_auto_check
#autoforward-yes#autoforward-nocheck        - exec: goto f_force_forward_list

#autoforward-no        - exec: goto f_force_dnscrypt_list


  - tag: respond
    type: sequence
    args:
#shuffle        - exec: shuffle
#liteshuffle        - exec: shuffle 3
#trncshuffle        - exec: shuffle 3
#addinfo        - exec: addinfo respond mosdns cache
        - exec: ok
  - tag: check_cache
    type: sequence
    args:
        - exec: cache {MSCACHE}
        - matches: has_wanted_ans
          exec: goto respond
        - exec: jump main_sequence

  - tag: udp_server
    type: udp_server
    args:
      entry: check_cache
      listen: :53
  - tag: "tcp_server"
    type: "tcp_server"
    args:
      entry: check_cache
      listen: :53
      idle_timeout: 5
#flushd_un_yes  - tag: "flushd_server"
#flushd_un_yes    type: "flushd_server"
#http_file_yes  - tag: "httpd_server"
#http_file_yes    type: "httpd_server"

启动PPDNS:

docker start paopaodns

检测外网地址是否返回fakeip,返回结果应该为198.18.0.0/15段的地址:

nslookup www.google.com

如想自行设置PPDNS,可参考PaoPaoDNS

再次测试外网连通性,此时应该完全设置完毕,所有设备均可正常访问外网(注意之前/routing/rule设置中需要添加需要代理设备的IP,若全局代理则填写对应网段):

# OSS测试 
curl www.google.com

# ROS测试
/tool fetch url="https://www.gstatic.com/generate_204" keep-result=no

第七步 设置bird及定时更新脚本

首先下载相关文件:
v2dat:提取码3426,下载后放到/usr/local/bin

安装相关包:

chmod +x /usr/local/bin/v2dat

ROS添加脚本:

/system script
add dont-require-permissions=no name="Update Geodata" owner=admin policy=\
    ftp,reboot,read,write,policy,test,password,sniff,sensitive,romon source="# ================= \E5\8F\98\E9\87\8F\E5\AE\9A\E4\B9\
    \89 =================\
    \n# 1. \E4\B8\8B\E8\BD\BD\E9\93\BE\E6\8E\A5\E5\88\97\E8\A1\A8\
    \n:local links {\
    \n    \"https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geoip.dat\";\
    \n    \"https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geosite.dat\";\
    \n    \"https://gaoyifan.github.io/china-operator-ip/chinanet.txt\";\
    \n    \"https://ispip.clang.cn/chinatelecom.txt\";\
    \n    \"https://ispip.clang.cn/chinatelecom_apnic.txt\";\
    \n    \"https://gaoyifan.github.io/china-operator-ip/unicom.txt\";\
    \n    \"https://ispip.clang.cn/unicom_cnc.txt\";\
    \n    \"https://ispip.clang.cn/unicom_cnc_apnic.txt\";\
    \n    \"https://gaoyifan.github.io/china-operator-ip/chinanet6.txt\";\
    \n    \"https://ispip.clang.cn/chinatelecom_ipv6.txt\";\
    \n    \"https://ispip.clang.cn/chinatelecom_ipv6_apnic.txt\";\
    \n    \"https://gaoyifan.github.io/china-operator-ip/unicom6.txt\";\
    \n    \"https://ispip.clang.cn/unicom_cnc_ipv6.txt\";\
    \n    \"https://ispip.clang.cn/unicom_cnc_ipv6_apnic.txt\";\
    \n    \"https://gaoyifan.github.io/china-operator-ip/cmcc.txt\";\
    \n    \"https://ispip.clang.cn/cmcc.txt\";\
    \n    \"https://ispip.clang.cn/cmcc_apnic.txt\";\
    \n    \"https://gaoyifan.github.io/china-operator-ip/cmcc6.txt\";\
    \n    \"https://ispip.clang.cn/cmcc_ipv6.txt\";\
    \n    \"https://ispip.clang.cn/cmcc_ipv6_apnic.txt\";\
    \n    \"https://anti-ad.net/domains.txt\";\
    \n}\
    \n\
    \n# 2. \E6\9C\AC\E5\9C\B0\E4\BF\9D\E5\AD\98\E7\9A\84\E6\96\87\E4\BB\B6\E5\90\8D\
    \n:local saveNames {\
    \n    \"geoip.dat\"; \
    \n    \"geosite.dat\";\
    \n    \"ct_gao_v4.txt\"; \
    \n    \"ct_clang_v4.txt\"; \
    \n    \"ct_apnic_v4.txt\";\
    \n    \"cu_gao_v4.txt\"; \
    \n    \"cu_clang_v4.txt\"; \
    \n    \"cu_apnic_v4.txt\";\
    \n    \"ct_gao_v6.txt\"; \
    \n    \"ct_clang_v6.txt\"; \
    \n    \"ct_apnic_v6.txt\";\
    \n    \"cu_gao_v6.txt\"; \
    \n    \"cu_clang_v6.txt\"; \
    \n    \"cu_apnic_v6.txt\";\
    \n    \"cm_gao_v4.txt\"; \
    \n    \"cm_clang_v4.txt\"; \
    \n    \"cm_apnic_v4.txt\";\
    \n    \"cm_gao_v6.txt\"; \
    \n    \"cm_clang_v6.txt\"; \
    \n    \"cm_apnic_v6.txt\";\
    \n    \"anti-ad-domains.txt\";\
    \n}\
    \n\
    \n# ================= \E9\80\BB\E8\BE\91\E5\BC\80\E5\A7\8B =================\
    \n:log warning \"Script: Starting Update Task...\"\
    \n\
    \n# --- \E6\AD\A5\E9\AA\A4 1: \E5\87\86\E5\A4\87 Geodata \E7\9B\AE\E5\BD\95 ---\
    \n:if ([:len [/file find where name=\"geodata\"]] = 0) do={\
    \n    :log warning \"Creating geodata directory...\"\
    \n    # \E5\B0\9D\E8\AF\95\E8\A7\A6\E5\8F\91\E7\9B\AE\E5\BD\95\E5\88\9B\E5\BB\BA\
    \n    :do {\
    \n        /tool fetch mode=https url=\"https://www.baidu.com/favicon.ico\" dst-path=\"geodata/init.tmp\" keep-result=no check-c\
    ertificate=no;\
    \n    } on-error={ :log warning \"Dir init check failed, proceeding anyway...\"; }\
    \n}\
    \n\
    \n# --- \E6\AD\A5\E9\AA\A4 2: \E4\B8\8B\E8\BD\BD Geodata \E6\96\87\E4\BB\B6 ---\
    \n:local totalCount [:len \$links]\
    \n:local saveNameCount [:len \$saveNames]\
    \n\
    \n:if (\$totalCount != \$saveNameCount) do={\
    \n    :log error (\"Script Error: Array lengths mismatch! Links: \" . \$totalCount . \", Names: \" . \$saveNameCount);\
    \n} else={\
    \n    :for i from=0 to=(\$totalCount-1) do={\
    \n        :local url [:pick \$links \$i];\
    \n        :local name [:pick \$saveNames \$i];\
    \n        :local localPath (\"geodata/\" . \$name);\
    \n        \
    \n        :log warning (\"Downloading: \" . \$name);\
    \n        \
    \n        # Geodata \E4\B8\8B\E8\BD\BD\E9\80\BB\E8\BE\91\EF\BC\9A\E5\85\88\E5\88\A0\E5\90\8E\E4\B8\8B\EF\BC\8C\E9\98\B2\E6\AD\
    \A2\E6\96\87\E4\BB\B6\E6\8D\9F\E5\9D\8F\E6\AE\8B\E7\95\99\
    \n        /file remove [find name=\$localPath];\
    \n        \
    \n        :do {\
    \n            /tool fetch mode=https \\\
    \n                url=\$url \\\
    \n                dst-path=\$localPath \\\
    \n                check-certificate=no \\\
    \n                keep-result=yes \\\
    \n                http-header-field=\"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)\"\
    \n        } on-error={ \
    \n            :log error (\"Download Failed: \" . \$name); \
    \n        }\
    \n        # \E7\AE\80\E5\8D\95\E5\BB\B6\E6\97\B6\
    \n        :delay 0.5s;\
    \n    }\
    \n}\
    \n\
    \n# --- \E6\AD\A5\E9\AA\A4 3: \E5\88\9B\E5\BB\BA\E6\9C\AC\E5\9C\B0\E7\B3\BB\E7\BB\9F\E5\A4\87\E4\BB\BD (\E8\87\AA\E5\8A\A8\E8\
    \A6\86\E7\9B\96) ---\
    \n:log warning \"Script: Creating System Backups...\"\
    \n\
    \n# \E5\88\9B\E5\BB\BA\E4\BA\8C\E8\BF\9B\E5\88\B6\E5\85\A8\E9\87\8F\E5\A4\87\E4\BB\BD (ROS\E4\BC\9A\E8\87\AA\E5\8A\A8\E8\A6\86\
    \E7\9B\96\E5\90\8C\E5\90\8D\E6\96\87\E4\BB\B6)\
    \n:do {\
    \n    /system backup save name=\"autobackup\"\
    \n    :log warning \"System Backup (.backup) created/overwritten.\"\
    \n} on-error={ :log error \"Failed to create binary backup.\" }\
    \n\
    \n# \E5\88\9B\E5\BB\BA\E6\96\87\E6\9C\AC\E9\85\8D\E7\BD\AE\E5\AF\BC\E5\87\BA (ROS\E4\BC\9A\E8\87\AA\E5\8A\A8\E8\A6\86\E7\9B\96\
    \E5\90\8C\E5\90\8D\E6\96\87\E4\BB\B6)\
    \n:do {\
    \n    /export file=\"autobackup\" show-sensitive\
    \n    :log warning \"System Export (.rsc) created/overwritten.\"\
    \n} on-error={ :log error \"Failed to create config export.\" }\
    \n\
    \n# \E7\AD\89\E5\BE\85\E6\96\87\E4\BB\B6\E5\86\99\E5\85\A5\E5\AE\8C\E6\88\90\EF\BC\8C\E9\98\B2\E6\AD\A2 SSH \E6\8B\89\E5\8F\96\
    \E5\88\B0\E4\B8\8D\E5\AE\8C\E6\95\B4\E7\9A\84\E6\96\87\E4\BB\B6\
    \n:delay 5s\
    \n\
    \n# --- \E6\AD\A5\E9\AA\A4 4: \E8\A7\A6\E5\8F\91\E8\BF\9C\E7\A8\8B\E6\8B\89\E5\8F\96 (Linux) ---\
    \n:log warning \"Script: Notifying OpenSUSE(192.168.1.2) to pull files...\"\
    \n:do {\
    \n    /system ssh-exec address=\"192.168.1.2\" user=\"root\" password=\"OSS_PASS\" \\\
    \n        command=\"/usr/local/bin/pull_backup.sh\";\
    \n    :log warning \"Notification Sent.\";\
    \n} on-error={ \
    \n    :log error \"Notification Failed: Could not connect to 192.168.1.2 via SSH.\"; \
    \n}\
    \n\
    \n:log warning \"Script: All Tasks Finished.\""

先在OSS端由ssh连接以下ROS,输入密码后quit退出即可:

ssh admin@192.168.1.1

ROS运行Update Geodata脚本:

:execute "Update Geodata"

创建定时更新脚本:

nano /usr/local/bin/update

复制以下内容并粘贴进去保存:

#!/bin/bash

# =========================================================
# 0. 脚本初始化与环境设置
# =========================================================
set -o pipefail

# --- 路径定义 ---
readonly DIR_GEODATA="/srv/backup/Geodata"
readonly DIR_RULES="/usr/local/etc/rules"
readonly DIR_CACHE="/usr/local/etc/rules/cache"
readonly DIR_PAOPAO="/usr/local/etc/paopaodns"
readonly DIR_BACKUP="/srv/backup/Geodata"
readonly DIR_COMPOSE="/usr/local/etc/docker-compose"

readonly TMP_DIR="/tmp/geodata_update"
readonly LOCK_FILE="/var/lock/update_rules.lock"
readonly LOG_FILE="/var/log/update.log"

# --- [BIRD 配置路径] ---
readonly BIRD_CONF_V4="/etc/bird/routes_static_v4.conf"
readonly BIRD_CONF_V6="/etc/bird/routes_static_v6.conf"

# --- 认证与设备信息 ---
readonly ROS_IP="192.168.1.1"
readonly ROS_USER="admin"
readonly ROS_PASS="ROS_PASS"

# --- [下载源定义] ---
declare -A DOWNLOAD_TASKS
DOWNLOAD_TASKS["geoip.dat"]="https://fastly.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geoip.dat"
DOWNLOAD_TASKS["geosite.dat"]="https://fastly.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geosite.dat"
DOWNLOAD_TASKS["ct_gao_v4.txt"]="https://gaoyifan.github.io/china-operator-ip/chinanet.txt"
DOWNLOAD_TASKS["ct_gao_v6.txt"]="https://gaoyifan.github.io/china-operator-ip/chinanet6.txt"
DOWNLOAD_TASKS["ct_clang_v4.txt"]="https://ispip.clang.cn/chinatelecom.txt"
DOWNLOAD_TASKS["ct_clang_v6.txt"]="https://ispip.clang.cn/chinatelecom_ipv6.txt"
DOWNLOAD_TASKS["ct_apnic_v4.txt"]="https://ispip.clang.cn/chinatelecom_apnic.txt"
DOWNLOAD_TASKS["ct_apnic_v6.txt"]="https://ispip.clang.cn/chinatelecom_ipv6_apnic.txt"
DOWNLOAD_TASKS["cu_gao_v4.txt"]="https://gaoyifan.github.io/china-operator-ip/unicom.txt"
DOWNLOAD_TASKS["cu_gao_v6.txt"]="https://gaoyifan.github.io/china-operator-ip/unicom6.txt"
DOWNLOAD_TASKS["cu_clang_v4.txt"]="https://ispip.clang.cn/unicom_cnc.txt"
DOWNLOAD_TASKS["cu_clang_v6.txt"]="https://ispip.clang.cn/unicom_cnc_ipv6.txt"
DOWNLOAD_TASKS["cu_apnic_v4.txt"]="https://ispip.clang.cn/unicom_cnc_apnic.txt"
DOWNLOAD_TASKS["cu_apnic_v6.txt"]="https://ispip.clang.cn/unicom_cnc_ipv6_apnic.txt"
DOWNLOAD_TASKS["cm_gao_v4.txt"]="https://gaoyifan.github.io/china-operator-ip/cmcc.txt"
DOWNLOAD_TASKS["cm_gao_v6.txt"]="https://gaoyifan.github.io/china-operator-ip/cmcc6.txt"
DOWNLOAD_TASKS["cm_clang_v4.txt"]="https://ispip.clang.cn/cmcc.txt"
DOWNLOAD_TASKS["cm_clang_v6.txt"]="https://ispip.clang.cn/cmcc_ipv6.txt"
DOWNLOAD_TASKS["cm_apnic_v4.txt"]="https://ispip.clang.cn/cmcc_apnic.txt"
DOWNLOAD_TASKS["cm_apnic_v6.txt"]="https://ispip.clang.cn/cmcc_ipv6_apnic.txt"
DOWNLOAD_TASKS["anti-ad-domains.txt"]="https://anti-ad.net/domains.txt"

readonly AD_FILES=("anti-ad-domains.txt")
readonly CURL_CMD="curl -fsSL -A 'Mozilla/5.0' --connect-timeout 20 --max-time 600 --retry 3 --retry-delay 2"

# =========================================================
# 1. UI、日志与控制变量
# =========================================================
NEED_RESTART_SINGBOX=0
FORCE_UPDATE=0
FORCE_PAOPAO=0
SKIP_SINGBOX_UPDATE=0
USE_LOCAL_BACKUP=0
ENABLED_OPERATORS="cm,ct,cu"
TARGET_GEOSITES=("openai" "youtube" "google" "google-gemini" "netflix" "telegram")
TARGET_GEOIPS=("telegram")

readonly TOTAL_STEPS=7
CURRENT_STEP=0

readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;36m'
readonly PURPLE='\033[0;35m'
readonly CYAN='\033[0;96m'
readonly BOLD='\033[1m'
readonly NC='\033[0m'

get_file_desc() {
    local fname="$1"
    case "$fname" in
        "geoip.dat") echo "GeoIP 核心数据库" ;;
        "geosite.dat") echo "GeoSite 域名数据库" ;;
        "ct_gao_v4.txt") echo "电信IPv4列表 (Gao)" ;;
        "ct_gao_v6.txt") echo "电信IPv6列表 (Gao)" ;;
        "ct_clang_v4.txt") echo "电信IPv4列表 (Clang)" ;;
        "ct_clang_v6.txt") echo "电信IPv6列表 (Clang)" ;;
        "ct_apnic_v4.txt") echo "电信IPv4列表 (APNIC)" ;;
        "ct_apnic_v6.txt") echo "电信IPv6列表 (APNIC)" ;;
        "cu_gao_v4.txt") echo "联通IPv4列表 (Gao)" ;;
        "cu_gao_v6.txt") echo "联通IPv6列表 (Gao)" ;;
        "cu_clang_v4.txt") echo "联通IPv4列表 (Clang)" ;;
        "cu_clang_v6.txt") echo "联通IPv6列表 (Clang)" ;;
        "cu_apnic_v4.txt") echo "联通IPv4列表 (APNIC)" ;;
        "cu_apnic_v6.txt") echo "联通IPv6列表 (APNIC)" ;;
        "cm_gao_v4.txt") echo "移动IPv4列表 (Gao)" ;;
        "cm_gao_v6.txt") echo "移动IPv6列表 (Gao)" ;;
        "cm_clang_v4.txt") echo "移动IPv4列表 (Clang)" ;;
        "cm_clang_v6.txt") echo "移动IPv6列表 (Clang)" ;;
        "cm_apnic_v4.txt") echo "移动IPv4列表 (APNIC)" ;;
        "cm_apnic_v6.txt") echo "移动IPv6列表 (APNIC)" ;;
        "anti-ad-domains.txt") echo "Anti-AD 广告列表" ;;
        "adlist.txt") echo "聚合广告规则文件" ;;
        *) echo "$fname" ;;
    esac
}

draw_global_bar() {
    local pct=$((CURRENT_STEP * 100 / TOTAL_STEPS))
    [ $pct -gt 100 ] && pct=100
    local width=40
    local num=$((pct * width / 100))
    local bar=""
    local empty=""

    [ "$num" -gt 0 ] && bar=$(printf "%0.s█" $(seq 1 $num))
    [ $((width - num)) -gt 0 ] && empty=$(printf "%0.s░" $(seq 1 $((width - num))))

    printf "\r${CYAN}╭─ [${NC}%s${NC}%s${CYAN}] ${PURPLE}%3d%%${NC}" "$bar" "$empty" "$pct"
}

progress_inc() {
    ((CURRENT_STEP++))
    draw_global_bar
}

log_wrapper() {
    local color="$1"
    local icon="$2"
    local prefix="$3"
    local msg="$4"

    printf "\r\033[K"
    if [ -n "$prefix" ]; then
        printf "${color}%s [%s]${NC} %s\n" "$icon" "$prefix" "$msg"
    else
        printf "   ${CYAN}├─${NC} %s\n" "$msg"
    fi
    draw_global_bar

    local clean_msg
    clean_msg=$(echo "$msg" | sed -r "s/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[mGK]//g")
    local log_time
    log_time=$(date "+%Y-%m-%d %H:%M:%S")

    if [ -n "$prefix" ]; then
        echo "${log_time} [${prefix}] ${clean_msg}" >> "$LOG_FILE"
    else
        echo "${log_time} [SUB]  ${clean_msg}" >> "$LOG_FILE"
    fi
}

log_info() { log_wrapper "$GREEN" "✨" "信息" "$1"; }
log_step() { log_wrapper "$BLUE" "🚀" "阶段" "$1"; }
log_warn() { log_wrapper "$YELLOW" "⚠️ " "注意" "$1"; }
log_err() { log_wrapper "$RED" "❌" "错误" "$1"; }
log_sub() { log_wrapper "" "" "" "$1"; }

cleanup() {
    rm -rf "$TMP_DIR"
    flock -u 9 2> /dev/null
    rm -f "$LOCK_FILE"
}

# =========================================================
# 2. 文件管理与上传逻辑
# =========================================================

# =========================================================
# 3. 核心功能模块
# =========================================================

update_cores() {
    log_step "[1/7] 代理核心版本检查 (Beta 版通道)"

    # --- 1. 更新 sing-box ---
    if [ "$SKIP_SINGBOX_UPDATE" -eq 1 ]; then
        log_warn "用户指定跳过 sing-box 检查"
    else
        # 精准抓取绝对最新版本(无论稳定还是Beta)
        local sb_remote_ver
        sb_remote_ver=$(curl -sL -A "Mozilla/5.0" --connect-timeout 15 --retry 3 "https://api.github.com/repos/SagerNet/sing-box/releases" | python3 -c '
import sys, json
try:
    data = json.load(sys.stdin)
    if data and isinstance(data, list) and len(data) > 0:
        print(data[0].get("tag_name", "").lstrip("v"))
except Exception:
    pass
')

        if [ -z "$sb_remote_ver" ]; then
            log_warn "获取 sing-box 最新版本号失败"
        else
            local sb_local_ver="none"
            if [ -x "/usr/local/bin/sing-box" ]; then
                sb_local_ver=$(/usr/local/bin/sing-box version | head -n1 | awk '{print $3}')
            fi

            if [[ "$sb_local_ver" != "$sb_remote_ver" ]]; then
                log_warn "发现新版本 sing-box: v$sb_remote_ver (当前: v$sb_local_ver)"

                local sb_install_args=""
                if [[ "$sb_remote_ver" == *"-"* ]]; then
                    sb_install_args="--beta"
                fi

                local sb_arch=""
                case $(uname -m) in
                    x86_64) sb_arch="amd64" ;;
                    aarch64 | arm64) sb_arch="arm64" ;;
                    *) log_err "不支持的架构: $(uname -m)" ;;
                esac

                if [ -n "$sb_arch" ]; then
                    log_sub "正在直接下载 sing-box v${sb_remote_ver} (${sb_arch})..."
                    local dl_url="https://github.com/SagerNet/sing-box/releases/download/v${sb_remote_ver}/sing-box-${sb_remote_ver}-linux-${sb_arch}.tar.gz"
                    if curl -fsSL --connect-timeout 20 -o "/tmp/sing-box.tar.gz" "$dl_url"; then
                        tar -xzf "/tmp/sing-box.tar.gz" -C "/tmp/"
                        # 强行部署至指定路径
                        mv -f /tmp/sing-box-${sb_remote_ver}-linux-${sb_arch}/sing-box /usr/local/bin/sing-box
                        chmod +x /usr/local/bin/sing-box
                        rm -rf /tmp/sing-box*

                        # 顺手修复你的 Systemd 路径(如果有)
                        if [ -f /etc/systemd/system/sing-box.service ]; then
                            sed -i 's#/usr/bin/sing-box#/usr/local/bin/sing-box#g' /etc/systemd/system/sing-box.service 2> /dev/null || true
                            systemctl daemon-reload 2> /dev/null || true
                        fi

                        local sb_new_ver="none"
                        if [ -x "/usr/local/bin/sing-box" ]; then
                            sb_new_ver=$(/usr/local/bin/sing-box version | head -n1 | awk '{print $3}')
                        fi

                        if [[ "$sb_new_ver" == "$sb_remote_ver" ]]; then
                            NEED_RESTART_SINGBOX=1
                            log_sub "sing-box 部署至 /usr/local/bin 成功 (v$sb_new_ver)"
                        else
                            log_err "sing-box 更新后版本号校验失败,详情请查阅 $LOG_FILE"
                        fi
                    else
                        log_err "sing-box 下载失败: $dl_url"
                    fi
                fi
            else
                log_sub "sing-box 已是最新版 (v$sb_local_ver)"
            fi
        fi
    fi

    progress_inc
}

download_files() {
    log_step "[2/7] 资源文件下载"

    mkdir -p "$TMP_DIR"
    mkdir -p "$DIR_BACKUP"

    if [ "$USE_LOCAL_BACKUP" -eq 1 ]; then
        log_warn "参数 B 触发: 强制使用本地备份,跳过网络下载"
        for fname in "${!DOWNLOAD_TASKS[@]}"; do
            local file_path="$TMP_DIR/$fname"
            local backup_path="$DIR_BACKUP/$fname"
            local file_desc
            file_desc=$(get_file_desc "$fname")

            if [[ -s "$backup_path" ]]; then
                cp -f "$backup_path" "$file_path"
                log_sub "恢复本地备份: $file_desc"
            else
                log_err "本地备份不存在: $file_desc"
            fi
        done
    else
        for fname in "${!DOWNLOAD_TASKS[@]}"; do
            $CURL_CMD "${DOWNLOAD_TASKS[$fname]}" -o "$TMP_DIR/$fname" &
        done

        wait

        log_sub "正在校验文件并更新备份..."
        for fname in "${!DOWNLOAD_TASKS[@]}"; do
            local file_path="$TMP_DIR/$fname"
            local backup_path="$DIR_BACKUP/$fname"
            local file_desc
            file_desc=$(get_file_desc "$fname")

            if [[ -s "$file_path" ]]; then
                if [[ "$FORCE_UPDATE" -eq 1 ]] || ! cmp -s "$file_path" "$backup_path"; then
                    cp -f "$file_path" "$backup_path"
                    if [[ "$FORCE_UPDATE" -eq 1 ]]; then
                        log_sub "强制更新下载备份 (参数 F): $file_desc"
                    else
                        log_sub "下载并更新备份: $file_desc"
                    fi
                else
                    log_sub "下载成功 (备份无变更): $file_desc"
                fi
            else
                if [[ -s "$backup_path" ]]; then
                    cp -f "$backup_path" "$file_path"
                    log_warn "下载失败,使用本地备份: $file_desc"
                else
                    log_err "下载失败且无备份: $file_desc"
                fi
            fi
        done
    fi

    log_sub "正在合并运营商 IP 数据..."
    if ls "$TMP_DIR"/ct_*.txt 1> /dev/null 2>&1; then
        cat "$TMP_DIR"/ct_*.txt > "$TMP_DIR/raw_ct.txt"
        echo "" >> "$TMP_DIR/raw_ct.txt"
    fi
    if ls "$TMP_DIR"/cu_*.txt 1> /dev/null 2>&1; then
        cat "$TMP_DIR"/cu_*.txt > "$TMP_DIR/raw_cu.txt"
        echo "" >> "$TMP_DIR/raw_cu.txt"
    fi
    if ls "$TMP_DIR"/cm_*.txt 1> /dev/null 2>&1; then
        cat "$TMP_DIR"/cm_*.txt > "$TMP_DIR/raw_cm.txt"
        echo "" >> "$TMP_DIR/raw_cm.txt"
    fi

    progress_inc
}

process_data() {
    log_step "[3/7] 广告规则与数据解包 (Geosite/GeoIP)"

    log_sub "处理广告拦截规则..."
    : > "$DIR_RULES/adlist.txt"

    if [ -f "$TMP_DIR/geosite.dat" ] && command -v v2dat &> /dev/null; then
        log_sub "解析 Geosite (category-ads-all)..."
        v2dat unpack geosite -o "$TMP_DIR" -f category-ads-all "$TMP_DIR/geosite.dat" > /dev/null 2>&1

        if [ -f "$TMP_DIR/geosite_category-ads-all.txt" ]; then
            cat "$TMP_DIR/geosite_category-ads-all.txt" >> "$DIR_RULES/adlist.txt"
        else
            log_warn "Geosite 规则解析失败或为空"
        fi
    fi

    for f in "${AD_FILES[@]}"; do
        if [ -s "$TMP_DIR/$f" ]; then
            cat "$TMP_DIR/$f" >> "$DIR_RULES/adlist.txt"
        fi
    done

    # [关键修复] 剔除所有的注释行(以 #、! 或 [ 开头)以及空行,防止 MosDNS 崩溃
    sed -i -E '/^([[:space:]]*#|!|\[)/d; /^[[:space:]]*$/d' "$DIR_RULES/adlist.txt"

    sort -u "$DIR_RULES/adlist.txt" -o "$DIR_RULES/adlist.txt"

    local whitelist_file="$DIR_RULES/whitelist.txt"
    if [ -s "$whitelist_file" ]; then
        local count_before
        count_before=$(wc -l < "$DIR_RULES/adlist.txt")

        grep -vFf "$whitelist_file" "$DIR_RULES/adlist.txt" > "$DIR_RULES/adlist.tmp"
        mv -f "$DIR_RULES/adlist.tmp" "$DIR_RULES/adlist.txt"

        local count_after
        count_after=$(wc -l < "$DIR_RULES/adlist.txt")
        log_sub "白名单过滤: 移除 $((count_before - count_after)) 条 (剩余: ${count_after})"
    else
        log_sub "白名单为空或不存在,跳过过滤"
    fi

    mkdir -p "$DIR_PAOPAO"
    cp -f "$DIR_RULES/adlist.txt" "$DIR_PAOPAO/adlist.txt"
    log_sub "更新 PaoPaoDNS 规则: adlist.txt"

    log_sub "解包 Geosite 并编译为 SRS 规则集..."
    local target_sites=("${TARGET_GEOSITES[@]}")

    if [ -f "$TMP_DIR/geosite.dat" ] && command -v v2dat &> /dev/null && [ -x "/usr/local/bin/sing-box" ]; then
        if [[ "$FORCE_UPDATE" -eq 0 ]] && cmp -s "$TMP_DIR/geosite.dat" "$DIR_GEODATA/geosite.dat"; then
            log_sub "geosite.dat 数据无变更,跳过解包与编译"
        else
            for site in "${target_sites[@]}"; do
                (
                    log_sub "处理 Geosite: $site ..."
                    v2dat unpack geosite -o "$TMP_DIR" -f "$site" "$TMP_DIR/geosite.dat" > /dev/null 2>&1

                    local txt_file="$TMP_DIR/geosite_${site}.txt"
                    local json_file="$TMP_DIR/geosite_${site}.json"
                    local srs_file="$DIR_RULES/geosite_${site}.srs"

                    if [ -s "$txt_file" ]; then
                        if [[ "$FORCE_UPDATE" -eq 0 ]] && cmp -s "$txt_file" "$DIR_RULES/geosite_${site}.txt"; then
                            log_sub "规则 $site 无变更,跳过编译"
                        else
                            cp -f "$txt_file" "$DIR_RULES/geosite_${site}.txt"
                            python3 -c "
import sys, json

txt = sys.argv[1]
out = sys.argv[2]
rules = {'domain_suffix': [], 'domain': [], 'domain_keyword': [], 'domain_regex': []}

try:
    with open(txt, 'r', encoding='utf-8') as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith('#') or '@' in line: continue
            
            if line.startswith('full:'): rules['domain'].append(line[5:])
            elif line.startswith('keyword:'): rules['domain_keyword'].append(line[8:])
            elif line.startswith('regex:') or line.startswith('regexp:'): rules['domain_regex'].append(line.split(':', 1)[1])
            elif line.startswith('domain:'): rules['domain_suffix'].append(line[7:])
            else: rules['domain_suffix'].append(line)
            
    rule_obj = {k: v for k, v in rules.items() if v}
     
    with open(out, 'w', encoding='utf-8') as f:
        json.dump({'version': 1, 'rules': [rule_obj]}, f)
except Exception as e:
    sys.exit(1)
" "$txt_file" "$json_file"

                            if [ -s "$json_file" ]; then
                                /usr/local/bin/sing-box rule-set compile "$json_file" -o "$srs_file" > /dev/null 2>&1
                                if [ -f "$srs_file" ]; then
                                    touch "$TMP_DIR/srs_updated"
                                    log_sub "成功生成 SRS 规则: geosite_${site}.srs"
                                else
                                    log_warn "编译 SRS 失败: $site"
                                fi
                            else
                                log_warn "TXT 转 JSON 失败: $site"
                            fi
                        fi
                    else
                        log_warn "解包 $site 为空或执行失败"
                    fi
                ) &
            done
            wait
            [ -f "$TMP_DIR/srs_updated" ] && NEED_RESTART_SINGBOX=1
        fi
    else
        log_warn "环境缺少 geosite.dat、v2dat 或 sing-box 命令,跳过 SRS 转换"
    fi

    local paopao_files=("force_dnscrypt_list.txt" "force_recurse_list.txt" "force_forward_list.txt" "force_ttl_rules.txt")

    for p_file in "${paopao_files[@]}"; do
        if [ ! -f "$DIR_PAOPAO/$p_file" ]; then
            touch "$DIR_PAOPAO/$p_file"
            log_sub "补全配置文件: $p_file"
        fi
    done

    log_sub "解包 Geosite 并生成 PaoPaoDNS 强制转发列表..."
    if [ -f "$TMP_DIR/geosite.dat" ] && command -v v2dat &> /dev/null; then
        local p_forward_bak="$DIR_PAOPAO/force_forward_list.txt.bak"
        local p_forward_out="$DIR_PAOPAO/force_forward_list.txt"
        local p_forward_tmp="$TMP_DIR/force_forward.tmp"

        : > "$p_forward_tmp"

        for p_site in "microsoft" "win-update"; do
            v2dat unpack geosite -o "$TMP_DIR" -f "$p_site" "$TMP_DIR/geosite.dat" > /dev/null 2>&1
            if [ -s "$TMP_DIR/geosite_${p_site}.txt" ]; then
                grep -vE "^(#|@|keyword:|regexp?:)" "$TMP_DIR/geosite_${p_site}.txt" |
                    sed -E 's/^(full|domain)://' >> "$p_forward_tmp"
            fi
        done

        if [ -f "$p_forward_bak" ]; then
            cat "$p_forward_bak" >> "$p_forward_tmp"
        fi

        if [ -s "$p_forward_tmp" ]; then
            sed -i '/^$/d' "$p_forward_tmp"
            sort -u "$p_forward_tmp" -o "$p_forward_tmp"
            if [ -f "$p_forward_out" ] && cmp -s "$p_forward_tmp" "$p_forward_out"; then
                log_sub "force_forward_list.txt 无变更,跳过更新"
            else
                cp -f "$p_forward_tmp" "$p_forward_out"
                log_sub "成功更新 force_forward_list.txt"
            fi
        else
            log_warn "未提取到任何 forward 域名"
        fi
    else
        log_warn "环境缺少 geosite.dat 或 v2dat 命令,跳过生成 force_forward_list.txt"
    fi

    log_sub "解析 Telegram IP 库并编译为 SRS..."
    if [ -f "$TMP_DIR/geoip.dat" ] && command -v v2dat &> /dev/null && [ -x "/usr/local/bin/sing-box" ]; then
        if [[ "$FORCE_UPDATE" -eq 0 ]] && cmp -s "$TMP_DIR/geoip.dat" "$DIR_GEODATA/geoip.dat"; then
            log_sub "geoip.dat 数据无变更,跳过 Telegram IP 更新"
            [ -f "$DIR_RULES/geoip_telegram.txt" ] && cp -f "$DIR_RULES/geoip_telegram.txt" "$TMP_DIR/geoip_telegram.txt"
        else
            v2dat unpack geoip -o "$TMP_DIR" -f "${TARGET_GEOIPS[0]}" "$TMP_DIR/geoip.dat" > /dev/null 2>&1
            if [ -s "$TMP_DIR/geoip_telegram.txt" ]; then
                if [[ "$FORCE_UPDATE" -eq 0 ]] && cmp -s "$TMP_DIR/geoip_telegram.txt" "$DIR_RULES/geoip_telegram.txt"; then
                    log_sub "Telegram IP 规则无变更,跳过编译"
                else
                    cp -f "$TMP_DIR/geoip_telegram.txt" "$DIR_RULES/geoip_telegram.txt"
                    python3 -c "
import sys, json
try:
    with open(sys.argv[1], 'r', encoding='utf-8') as f:
        ips = [line.strip() for line in f if line.strip() and not line.startswith('#')]
    with open(sys.argv[2], 'w', encoding='utf-8') as f:
        json.dump({'version': 1, 'rules': [{'ip_cidr': ips}]}, f)
except Exception:
    sys.exit(1)
" "$TMP_DIR/geoip_telegram.txt" "$TMP_DIR/geoip_telegram.json"
                    if [ -s "$TMP_DIR/geoip_telegram.json" ]; then
                        /usr/local/bin/sing-box rule-set compile "$TMP_DIR/geoip_telegram.json" -o "$DIR_RULES/geoip_telegram.srs" > /dev/null 2>&1
                        [ -f "$DIR_RULES/geoip_telegram.srs" ] && {
                            NEED_RESTART_SINGBOX=1
                            log_sub "成功生成 SRS 规则: geoip_telegram.srs"
                        } || log_warn "编译 SRS 失败: telegram"
                    fi
                fi
            fi
        fi
    fi
    progress_inc

    log_step "[4/7] 路由计算与 BIRD 配置生成"
    mkdir -p "$DIR_CACHE"
    # 确保 TG IP 存在,若因跳过更新而丢失,则从缓存恢复或重新解包
    [ ! -f "$TMP_DIR/geoip_telegram.txt" ] && [ -f "$DIR_CACHE/geoip_telegram.txt" ] && cp -f "$DIR_CACHE/geoip_telegram.txt" "$TMP_DIR/geoip_telegram.txt"
    [ ! -f "$TMP_DIR/geoip_telegram.txt" ] && [ -f "$DIR_GEODATA/geoip.dat" ] && command -v v2dat &> /dev/null && v2dat unpack geoip -o "$TMP_DIR" -f "${TARGET_GEOIPS[0]}" "$DIR_GEODATA/geoip.dat" > /dev/null 2>&1

    ROUTE_CHANGED=0
    [[ "$FORCE_UPDATE" -eq 1 ]] && ROUTE_CHANGED=1
    cmp -s "$TMP_DIR/raw_ct.txt" "$DIR_CACHE/raw_ct.txt" || ROUTE_CHANGED=1
    cmp -s "$TMP_DIR/raw_cu.txt" "$DIR_CACHE/raw_cu.txt" || ROUTE_CHANGED=1
    cmp -s "$TMP_DIR/raw_cm.txt" "$DIR_CACHE/raw_cm.txt" || ROUTE_CHANGED=1
    cmp -s "$TMP_DIR/geoip_telegram.txt" "$DIR_CACHE/geoip_telegram.txt" || ROUTE_CHANGED=1

    if [[ "$ROUTE_CHANGED" -eq 0 ]]; then
        log_sub "路由源数据无变更,跳过 CIDR 聚合优化"
    elif [ -s "$TMP_DIR/raw_ct.txt" ]; then
        cp -f "$TMP_DIR/raw_ct.txt" "$DIR_CACHE/raw_ct.txt" 2> /dev/null
        cp -f "$TMP_DIR/raw_cu.txt" "$DIR_CACHE/raw_cu.txt" 2> /dev/null
        cp -f "$TMP_DIR/raw_cm.txt" "$DIR_CACHE/raw_cm.txt" 2> /dev/null
        cp -f "$TMP_DIR/geoip_telegram.txt" "$DIR_CACHE/geoip_telegram.txt" 2> /dev/null

        log_sub "计算网段并检查 BIRD 配置变更..."

        export FILE_CT="$TMP_DIR/raw_ct.txt"
        export FILE_CU="$TMP_DIR/raw_cu.txt"
        export FILE_CM="$TMP_DIR/raw_cm.txt"
        export FILE_TG="$TMP_DIR/geoip_telegram.txt"
        export BIRD_TMP_V4="$TMP_DIR/routes_static_v4.conf.tmp"
        export BIRD_TMP_V6="$TMP_DIR/routes_static_v6.conf.tmp"

        python3 -u -c "
import ipaddress, sys, os

f_ct = os.environ.get('FILE_CT')
f_cu = os.environ.get('FILE_CU')
f_cm = os.environ.get('FILE_CM')
f_tg = os.environ.get('FILE_TG')
bird_v4 = os.environ.get('BIRD_TMP_V4')
bird_v6 = os.environ.get('BIRD_TMP_V6')

def load_nets(filepath):
    v4, v6 = set(), set()
    if not filepath or not os.path.exists(filepath): return v4, v6
    try:
        with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
            for line in f:
                line = line.strip()
                if not line or line.startswith('#'): continue
                try:
                    net = ipaddress.ip_network(line, strict=False)
                    if net.version == 4: v4.add(net)
                    else: v6.add(net)
                except ValueError: continue
    except Exception as e:
        print(f'Warning: Error reading {filepath}: {e}')
    return v4, v6

print('[读取] 正在解析运营商与 Telegram 数据...')
ct_v4_raw, ct_v6_raw = load_nets(f_ct)
cu_v4_raw, cu_v6_raw = load_nets(f_cu)
cm_v4_raw, cm_v6_raw = load_nets(f_cm)
tg_v4_raw, tg_v6_raw = load_nets(f_tg)

def agg(s): 
    return list(ipaddress.collapse_addresses(s))

print('[计算] 执行 CIDR 网段聚合优化...')
ct_v4, ct_v6 = agg(ct_v4_raw), agg(ct_v6_raw)
cu_v4, cu_v6 = agg(cu_v4_raw), agg(cu_v6_raw)
cm_v4, cm_v6 = agg(cm_v4_raw), agg(cm_v6_raw)
tg_v4, tg_v6 = agg(tg_v4_raw), agg(tg_v6_raw)

print(f'[统计] 聚合后: 电信v4[{len(ct_v4)}] 电信v6[{len(ct_v6)}] 联通v4[{len(cu_v4)}] 联通v6[{len(cu_v6)}] 移动v4[{len(cm_v4)}] 移动v6[{len(cm_v6)}] TGv4[{len(tg_v4)}] TGv6[{len(tg_v6)}]')

print('[生成] 正在进行跨运营商网段去重与优先级分配...')
# 优先级字典: Prefix -> Community
# 相同网段冲突时,后写入的优先级更高 (覆盖机制)
# 当前优先级顺序: 移动(CM) < 联通(CU) < 电信(CT) < TG代理(TG)
routes_v4 = {}
routes_v6 = {}

for net in cm_v4: routes_v4[net] = '(65000, 1001)'
for net in cm_v6: routes_v6[net] = '(65000, 1001)'

for net in cu_v4: routes_v4[net] = '(65000, 3001)'
for net in cu_v6: routes_v6[net] = '(65000, 3001)'

for net in ct_v4: routes_v4[net] = '(65000, 2001)'
for net in ct_v6: routes_v6[net] = '(65000, 2001)'

for net in tg_v4: routes_v4[net] = '(65000, 4002)'
for net in tg_v6: routes_v6[net] = '(65000, 4002)'

print('[生成] 正在写入临时 BIRD 配置文件...')
try:
    with open(bird_v4, 'w', encoding='utf-8') as f4, open(bird_v6, 'w', encoding='utf-8') as f6:
        f4.write('# Auto-generated IPv4 routes by update script\\n')
        f6.write('# Auto-generated IPv6 routes by update script\\n')
        
        for net, community in routes_v4.items():
            f4.write(f'route {net} blackhole {{ bgp_community.add({community}); }};\\n')
            
        for net, community in routes_v6.items():
            f6.write(f'route {net} blackhole {{ bgp_community.add({community}); }};\\n')
    print('[完成] 临时文件生成完毕。')
except Exception as e:
    print(f'Error writing config: {e}')
    sys.exit(1)
" | while read -r line; do log_sub "$line"; done

        local need_reload=0
        mkdir -p "$(dirname "$BIRD_CONF_V4")"

        if [ ! -f "$BIRD_CONF_V4" ] || ! cmp -s "$BIRD_TMP_V4" "$BIRD_CONF_V4" || [[ "$FORCE_UPDATE" -eq 1 ]]; then
            cp -f "$BIRD_TMP_V4" "$BIRD_CONF_V4"
            need_reload=1
            if [[ "$FORCE_UPDATE" -eq 1 ]]; then
                log_sub "IPv4 路由表已强制更新 (参数 F)"
            else
                log_sub "IPv4 路由表已更新"
            fi
        else
            log_sub "IPv4 路由表无变更"
        fi

        if [ ! -f "$BIRD_CONF_V6" ] || ! cmp -s "$BIRD_TMP_V6" "$BIRD_CONF_V6" || [[ "$FORCE_UPDATE" -eq 1 ]]; then
            cp -f "$BIRD_TMP_V6" "$BIRD_CONF_V6"
            need_reload=1
            if [[ "$FORCE_UPDATE" -eq 1 ]]; then
                log_sub "IPv6 路由表已强制更新 (参数 F)"
            else
                log_sub "IPv6 路由表已更新"
            fi
        else
            log_sub "IPv6 路由表无变更"
        fi
    else
        log_sub "未发现运营商 IP 数据,跳过路由计算"
    fi

    # --- 独立执行的 BIRD 动态配置逻辑 (已与路由源数据更新解绑) ---
    local need_reload_bird=0
    if [ -f "/etc/bird.conf" ]; then
        local bcast_str="empty"
        local bcast_list=""
        [[ "$ENABLED_OPERATORS" == *"cm"* ]] && bcast_list+=" (65000, 1001), (65000, 1002),"
        [[ "$ENABLED_OPERATORS" == *"ct"* ]] && bcast_list+=" (65000, 2001), (65000, 2002),"
        [[ "$ENABLED_OPERATORS" == *"cu"* ]] && bcast_list+=" (65000, 3001), (65000, 3002),"
        bcast_list="${bcast_list%,}"
        [ -n "$bcast_list" ] && bcast_str="[$bcast_list ]"

        local current_bcast=$(grep "^define BCAST_SET =" /etc/bird.conf | sed 's/^define BCAST_SET = //; s/;//')
        if [ "$current_bcast" != "$bcast_str" ]; then
            sed -i -E "s/^define BCAST_SET = .*/define BCAST_SET = $bcast_str;/" /etc/bird.conf
            log_sub "动态路由宣告列表已变动: $bcast_str"
            need_reload_bird=1
        else
            log_sub "动态路由宣告列表无变动,当前为: $bcast_str"
        fi
    fi

    if [ "$need_reload" -eq 1 ] || [ "$need_reload_bird" -eq 1 ]; then
        log_sub "检测到路由或配置变更,触发 BIRD 重载..."
        if command -v birdc &> /dev/null; then
            if birdc configure > /dev/null 2>&1; then
                log_sub "BIRD 重载成功"
            else
                log_err "BIRD 重载失败,请检查配置"
            fi
        fi
    fi
    progress_inc
}

restart_services() {
    log_step "[5/7] 服务状态维护"

    if [ "$NEED_RESTART_SINGBOX" -eq 1 ] || [ "$FORCE_UPDATE" -eq 1 ]; then
        log_warn "正在重启 sing-box 服务..."
        systemctl restart sing-box.service
    else
        log_sub "sing-box 配置无变动,无需重启"
    fi

    if [ "$FORCE_PAOPAO" -eq 1 ]; then
        log_warn "参数 P 触发: 强制重启 PaoPaoDNS 容器..."
        if command -v docker &> /dev/null && docker ps -a --format '{{.Names}}' | grep -q 'paopaodns'; then
            docker restart paopaodns > /dev/null 2>&1
            log_sub "PaoPaoDNS 容器已成功重启"
        else
            log_sub "未检测到运行中的 paopaodns 容器"
        fi
    fi

    progress_inc
}

rotate_large_logs() {
    log_sub "执行日志轮转:扫描 /var/log 下大于 10MB 的 .log 文件..."
    local found=0
    while IFS= read -r logfile; do
        if [ -n "$logfile" ]; then
            found=1
            local fsize
            fsize=$(du -mh "$logfile" | cut -f1)
            log_warn "清理大日志文件: $logfile ($fsize)"
            truncate -s 0 "$logfile"
        fi
    done < <(find /var/log -type f -name "*.log" -size +10M 2> /dev/null)

    if [ "$found" -eq 0 ]; then
        log_sub "未发现需要清理的日志文件。"
    fi
}

clear_log() {
    log_step "[7/7] 清理工作"

    mkdir -p "$DIR_GEODATA"
    if [ -f "$TMP_DIR/geoip.dat" ]; then cp -f "$TMP_DIR/geoip.dat" "$DIR_GEODATA/"; fi
    if [ -f "$TMP_DIR/geosite.dat" ]; then cp -f "$TMP_DIR/geosite.dat" "$DIR_GEODATA/"; fi

    rotate_large_logs

    CURRENT_STEP=$TOTAL_STEPS
    printf "\r\033[K"
    log_info "🎉 所有任务执行完毕!"
    printf "\n"
}

check_env() {
    log_step "[0/7] 环境与依赖检查"
    local pkgs=("curl" "python3" "sshpass" "bird")
    for pkg in "${pkgs[@]}"; do
        if ! command -v "$pkg" &> /dev/null; then
            log_warn "缺少依赖 $pkg,尝试自动安装..."
            if command -v zypper &> /dev/null; then
                zypper install -y "$pkg" > /dev/null 2>&1
            elif command -v apt-get &> /dev/null; then
                apt-get install -y "$pkg" > /dev/null 2>&1
            fi
        fi
    done

    # 备份原配置
    if [ ! -f "/etc/bird.conf.bak" ] && [ -f "/etc/bird.conf" ]; then
        cp -f /etc/bird.conf /etc/bird.conf.bak
        log_sub "已备份原 bird.conf 为 bird.conf.bak"
    fi

    # 动态获取系统 ULA 地址 (192.168.1.x / 192.168.10.x 网段)
    TG_IPV6_GW=$(ip -6 addr show | grep -E 'inet6 fd00:192:168:10:[0-9a-f:]+' | awk '{print $2}' | awk -F/ '{print $1}' | head -n1)
    [ -z "$TG_IPV6_GW" ] && TG_IPV6_GW="fd00:192:168:10::1"

    LOCAL_IPV6=$(ip -6 addr show | grep -E 'inet6 fd00:192:168:1:[0-9a-f:]+' | awk '{print $2}' | awk -F/ '{print $1}' | head -n1)
    [ -z "$LOCAL_IPV6" ] && LOCAL_IPV6="fd00:192:168:1::2"

    # 写入唯一的 BIRD 模板,确保 EOF 独占一行且闭合正确
    cat << EOF > /etc/bird.conf
log syslog all;
debug protocols all;
router id 192.168.1.2;

protocol static static_v4 {
    ipv4;
    include "/etc/bird/routes_static_v4.conf";
}
protocol static static_v6 {
    ipv6;
    include "/etc/bird/routes_static_v6.conf";
}
protocol device { scan time 10; }

define BGP_CM = [(65000, 1001), (65000, 1002)];
define BGP_CT = [(65000, 2001), (65000, 2002)];
define BGP_CU = [(65000, 3001), (65000, 3002)];
define BGP_TG = [(65000, 4002)];
define BCAST_SET = [ (65000, 1001), (65000, 1002), (65000, 2001), (65000, 2002), (65000, 3001), (65000, 3002) ];

filter export_to_else {
    if bgp_community ~ BCAST_SET then {
        if net.type = NET_IP4 then {
            if bgp_community ~ BGP_CM then bgp_next_hop = 10.255.255.1;
            if bgp_community ~ BGP_CT then bgp_next_hop = 10.255.255.2;
            if bgp_community ~ BGP_CU then bgp_next_hop = 10.255.255.3;
        }
        if net.type = NET_IP6 then {
            if bgp_community ~ BGP_CM then bgp_next_hop = fdff::1;
            if bgp_community ~ BGP_CT then bgp_next_hop = fdff::2;
            if bgp_community ~ BGP_CU then bgp_next_hop = fdff::3;
        }
        accept;
    }
    reject;
}

filter export_to_proxy {
    if bgp_community ~ BCAST_SET || bgp_community ~ BGP_TG then {
        if net.type = NET_IP4 then {
            if bgp_community ~ BGP_CM then bgp_next_hop = 10.255.255.1;
            if bgp_community ~ BGP_CT then bgp_next_hop = 10.255.255.2;
            if bgp_community ~ BGP_CU then bgp_next_hop = 10.255.255.3;
            if bgp_community ~ BGP_TG then bgp_next_hop = 192.168.10.2;
        }
        if net.type = NET_IP6 then {
            if bgp_community ~ BGP_CM then bgp_next_hop = fdff::1;
            if bgp_community ~ BGP_CT then bgp_next_hop = fdff::2;
            if bgp_community ~ BGP_CU then bgp_next_hop = fdff::3;
            if bgp_community ~ BGP_TG then bgp_next_hop = ${TG_IPV6_GW};
        }
        accept;
    }
    reject;
}

protocol bgp bgp_else {
    local 192.168.1.2 as 65001;
    neighbor 192.168.1.1 as 65002;
    ipv4 { import none; export filter export_to_else; };
    ipv6 { import none; export filter export_to_else; };
}

protocol bgp bgp_proxy {
    local ${LOCAL_IPV6} as 65001;
    neighbor fd00:192:168:1::1 as 65002;
    ipv4 { import none; export filter export_to_proxy; extended next hop on; };
    ipv6 { import none; export filter export_to_proxy; };
}
EOF
    log_sub "已成功注入 BIRD 配置模板"
    mkdir -p "$DIR_GEODATA" "$DIR_RULES" "$DIR_CACHE" "$DIR_PAOPAO" "$DIR_BACKUP" "/var/log"
}

show_help() {
    echo -e "${BLUE}=================================================${NC}"
    echo -e "${BOLD}  旁路由分流与规则自动化维护脚本${NC}"
    echo -e "${BLUE}=================================================${NC}"
    echo -e "用法: bash update [选项]"
    echo -e ""
    echo -e "${YELLOW}命令行快捷选项:${NC}"
    echo -e "  -H, --help     显示此操作指南"
    echo -e "  -F, --force    强制触发所有规则更新与核心重启"
    echo -e "  -P, --paopao   强制重启 PaoPaoDNS / MosDNS 容器"
    echo -e "  -B, --backup   断网灾备模式:跳过网络下载使用本地备份"
    echo -e ""
    echo -e "${YELLOW}脚本内置需配置参数 (请直接编辑 update 脚本头部变量):${NC}"
    echo -e "  ENABLED_OPERATORS="cm,ct,cu"    # 启用的国内运营商网段宣告,用逗号分隔"
    echo -e "  TARGET_GEOSITES=("openai"...)   # 需解包并编译为代理规则的 Geosite 标签阵列"
    echo -e "  TARGET_GEOIPS=("telegram")      # 需解包并编译为代理规则的 GeoIP 标签阵列"
    echo -e "${BLUE}=================================================${NC}"
    exit 0
}

main() {
    [ ! -d "$(dirname "$LOG_FILE")" ] && mkdir -p "$(dirname "$LOG_FILE")"
    [ ! -d "$DIR_BACKUP" ] && mkdir -p "$DIR_BACKUP"

    # 参数解析调整以支持传值
    while [[ $# -gt 0 ]]; do
        case "$1" in
            -H | --help)
                show_help
                ;;
            -F | --force)
                FORCE_UPDATE=1
                shift
                ;;
            -P | --paopao)
                FORCE_PAOPAO=1
                shift
                ;;
            -B | --backup)
                USE_LOCAL_BACKUP=1
                shift
                ;;
            *) shift ;;
        esac
    done

    exec 9<> "$LOCK_FILE"
    if ! flock -n 9; then
        log_err "脚本正在运行中 (无法获取锁)"
        exit 1
    fi

    trap cleanup EXIT SIGINT SIGTERM

    check_env
    draw_global_bar
    update_cores
    download_files
    process_data
    restart_services
    clear_log
}

main "$@"

编写拉取ros端geodata备份的脚本(ros端上传有时候会有bug,由oss端拉是最稳的):

nano /usr/local/bin/pull_backup.sh

复制以下内容并粘贴保存:

#!/bin/bash
# 路径: /usr/local/bin/pull_backup.sh
# 权限: chmod +x /usr/local/bin/pull_backup.sh
# 依赖: sudo zypper install sshpass (OpenSUSE)

# ================= 配置信息 =================
ROS_IP="192.168.1.1"
ROS_USER="admin"
ROS_PASS="ROS_PASS"

# 1. Geodata 存放路径 (覆盖存放)
LOCAL_GEO_DIR="/srv/backup/Geodata"

# 2. RouterOS 备份存放路径 (增量存放+时间戳)
LOCAL_BACKUP_DIR="/srv/backup/RouterOS"

# 获取当前时间戳 (例如: 20231027_120000)
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")

# ================= 目录准备 =================
mkdir -p "$LOCAL_GEO_DIR"
mkdir -p "$LOCAL_BACKUP_DIR"

# ================= 任务 1: 拉取 Geodata (覆盖) =================
echo "$(date): Starting Geodata pull..."
# scp -r 递归拉取 /geodata/ 下所有文件
sshpass -p "$ROS_PASS" scp -P 22 -o StrictHostKeyChecking=no -r $ROS_USER@$ROS_IP:/geodata/* "$LOCAL_GEO_DIR"/

if [ $? -eq 0 ]; then
    echo "$(date): Geodata pull successful."
else
    echo "$(date): Geodata pull failed."
fi

# ================= 任务 2: 拉取 ROS 备份 (重命名+归档) =================
echo "$(date): Starting RouterOS backup pull..."

# 拉取 .backup 文件
sshpass -p "$ROS_PASS" scp -P 22 -o StrictHostKeyChecking=no \
    $ROS_USER@$ROS_IP:autobackup.backup \
    "$LOCAL_BACKUP_DIR/autobackup_$TIMESTAMP.backup"

# 拉取 .rsc 文件
sshpass -p "$ROS_PASS" scp -P 22 -o StrictHostKeyChecking=no \
    $ROS_USER@$ROS_IP:autobackup.rsc \
    "$LOCAL_BACKUP_DIR/autobackup_$TIMESTAMP.rsc"

if [ $? -eq 0 ]; then
    echo "$(date): RouterOS backup pull successful."
else
    echo "$(date): RouterOS backup pull failed."
fi

# ================= 任务 3: 清理 30 天前的旧备份 =================
echo "$(date): Cleaning up backups older than 30 days..."
find "$LOCAL_BACKUP_DIR" -type f -name "autobackup_*" -mtime +30 -exec rm {} \;
echo "$(date): Cleanup finished."

加权:

chmod +x /usr/local/bin/pull_backup.sh /usr/local/bin/update

# 使用以下命令查看需要修改的参数
update -H

# 修改完后执行
update

脚本会自动下载需要的包并创建模板配置文件.

第八步 设置Nginx反代v

下载Nginx:

zypper install nginx

安装acme,以CF为例,假设已经在CF设置好了解析为eth1的任意GUA地址(一般设置为*),注意不要勾选代理,这里假设你的域名为123.xyz:

#安装acme:
curl https://get.acme.sh | sh

#添加软链接:
ln -s  /root/.acme.sh/acme.sh /usr/local/bin/acme.sh

#切换CA机构:
acme.sh --set-default-ca --server letsencrypt

#cloudflare API
export CF_Key=登录https://dash.cloudflare.com/profile/api-tokens 查看Global API Key
export CF_Email=CF的邮箱

# 申请证书
mkdir -p /usr/local/etc/ssl

acme.sh --issue --dns dns_cf -d *.123.xyz

acme.sh --install-cert -d *.123.xyz --ecc \
--key-file /usr/local/etc/ssl/fullchain.key  \
--fullchain-file /usr/local/etc/ssl/fullchain.cer

配置Nginx:

nano /etc/nginx/conf.d/https.conf

复制以下内容并粘贴保存,以反代ROS网页为例,要反代什么直接复制这个模块,粘贴在下面修改即可:

server {
    listen [::]:443 ssl;
    server_name ros.123.xyz;
    
    ssl_certificate     "/usr/local/etc/ssl/fullchain.cer";
    ssl_certificate_key "/usr/local/etc/ssl/fullchain.key";
    ssl_session_timeout 5m;
    ssl_ciphers TLS13_AES_128_GCM_SHA256:TLS13_AES_256_GCM_SHA384:TLS13_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305;
    ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    
    location / {
        proxy_pass http://192.168.1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        
        # 针对 ROS WebFig 可能需要的额外 WebSocket/头部支持 (可选)
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

检查配置并重载:

nginx -t
nginx -s reload

要真正意义上解决内网回环在现在的环境下比较难(可能是我漏想了哪里,知道的佬可以评论区告知ROS中怎么设置),这里选择曲线救国,在PPDNS中将反代域名解析的IP强制修改为内网IP,这样可以直接在内网访问域名不受限:

cat <<EOF >> /usr/local/etc/paopaodns/force_ttl_rules.txt
123.xyz@@你OSSeth1的ULA地址
EOF

访问 https://ros.123.xyz 查看是否反代成功

设置定时执行脚本:

crontab -r
crontab -e

复制以下内容并粘贴保存:

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
4 4 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/null
5 5 * * * zypper ref
6 6 * * * /usr/local/bin/update

至此 OSS 端的配置已全部完成.