三.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,输入你的随机密码进入界面:


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

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

保存下来订阅链接,你们的应该是一个 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 端的配置已全部完成.