#!/usr/bin/env bash # ============================================================================= # edgeone-origin-whitelist.sh # EdgeOne 回源端口白名单自动维护脚本(Linux / iptables + ipset) # # 背景 # TamaBox 源站对公网开放了一个回源端口给腾讯 EdgeOne 回源使用。 # 若不做任何限制,任何人都可直连"服务器 IP + 端口"绕过 CDN 直打源站。 # 本脚本把该端口收敛为白名单:仅放行 EdgeOne 回源网段(+ 本机回环, # 供 cloudflared Tunnel 本地回连),其余一律拒绝(tcp-reset)。 # # 设计要点 # 1. 白名单存放于 ipset 集合 EO_ORIGIN(hash:net / IPv4)。 # 更新采用「临时集合填充 -> ipset swap -> 销毁临时集合」原子替换, # iptables 规则全程引用同名集合,更新过程零规则窗口、失败不影响现网。 # 2. iptables 自建链 EO_IN 只建立一次(幂等,内容恒定): # -m set EO_ORIGIN src -> ACCEPT (EdgeOne 回源) # 其余 (-p tcp) -> REJECT tcp-reset # (注:iptables-nft 要求 --reject-with tcp-reset 必须带 -p tcp,否则 EINVAL) # INPUT 跳转默认【只对公网网卡 eth0 生效】(见 EO_IFACE 配置): # -i lo -p tcp --dport -> ACCEPT (cloudflared Tunnel 本机回连) # -i eth0 -p tcp --dport -> EO_IN (仅判定 eth0 入站流量) # EO_IFACE 留空 = 不限网卡(EO_IN 内另有 127.0.0.0/8 -> ACCEPT 兜回环, # 即脚本早期行为)。之后白名单增删只改 ipset 集合,EO_IN 与 INPUT 规则永不动。 # 3. 清单来源(可同时启用,取并集): # - 本地文件 CIDR_FILE:每行一个 IPv4 CIDR,# 开头为注释(裸 IP 视作 /32 亦可) # - 远程 URL CIDR_URL:返回纯文本、每行一个 IPv4 CIDR # 官方来源说明:EdgeOne「源站防护(Origin ACL)」是唯一官方回源网段 # 来源,但仅付费套餐支持(免费版不提供,见腾讯文档 1552/76086)。 # 🔴 免费档方案定稿(2026-09-03 实测):EO 回源出口是海量轮换节点池 # (一次 23s tcpdump 见 13 出口、散布多个 /16),逐 IP 采种白名单=打地鼠, # 必然间歇漏放 → 境内 521。请直接放行腾讯云 EO 大段(脚本原生支持 CIDR): # 实测生效清单 = 43.146/43.152/43.168/43.174/43.175 五个 /16(521 消失)。 # learn/auto 降级为「新段预警」:月度跑 auto,采到清单外 IP 时 whois 归段 # (属腾讯云 EO 则整段补入 cidr.txt 再 sync)。升级付费档后 API 结果写清单即可。 # 4. fail-safe:清单为空 / 全部非法 / 拉取失败 -> 直接退出(非 0), # 绝不触碰现网规则(宁可保持旧白名单,不可误删导致裸奔)。 # 5. IPv6 说明:脚本只管理 IPv4(iptables / ipset family inet),IPv6 流量 # 不受本脚本任何规则影响。TamaBox 部署为 listen=0.0.0.0(仅 IPv4 监听、 # 无 AAAA),IPv6 上根本不存在该回源端口 —— IPv6 用户经 CDN 访问时由 # CDN 边缘走 IPv4 回源到本机,一切正常,无需任何 ip6tables 规则。 # 若日后改为监听 [::]:PORT 并向 EO 开放 IPv6 回源,才需另行扩展 # ip6tables + family inet6 的 ipset(等价镜像本脚本逻辑)。 # # 用法 # 默认只对公网网卡 eth0 生效;网卡不同请改脚本头 EO_IFACE,或运行时覆盖: # sudo EO_IFACE=ens3 ./edgeone-origin-whitelist.sh sync # sudo ./edgeone-origin-whitelist.sh sync # 更新白名单(供 cron/systemd 周期调用;段级清单推荐每 3 天) # sudo ./edgeone-origin-whitelist.sh sync --dry-run# 演练:只打印将执行的动作,不触碰系统 # sudo ./edgeone-origin-whitelist.sh auto # 新段预警:采集回源 IP → 合并清单 → 有新增即 sync # sudo ./edgeone-origin-whitelist.sh learn # 即时观测:当前连接回源端口 18080 的来源 IP(不必用于日常维护) # sudo ./edgeone-origin-whitelist.sh show # 查看当前白名单与规则状态 # sudo ./edgeone-origin-whitelist.sh flush # 移除本脚本管理的全部规则与集合(回滚) # # 依赖: bash, ipset, iptables, (可选) curl 或 wget # Debian/Ubuntu: apt-get install -y ipset iptables curl # 说明: 本机为低配服务器,可先用 `sudo apt-get install -y ipset` 补齐。 # # 2026-09-03 TamaBox 项目 # ============================================================================= set -u # ---- 可配置区(按需修改,或用环境变量覆盖)---------------------------------- # 目标回源端口(务必改成你 EO 回源配置里填写的实际端口!) : "${EO_PORT:=5877}" # 入站网卡限定:只对【该网卡】入站的回源端口流量执行白名单。默认 eth0。 # ⚠️ 公网网卡不叫 eth0 时务必改(`ip route get 1.1.1.1` 可确认网卡名), # 否则跳转规则永不命中,该端口等于对全世界裸奔! # 留空 = 不限网卡(所有网卡入站都判定,即脚本早期行为)。 : "${EO_IFACE:=eth0}" # 本脚本管理的 ipset 集合名与 iptables 链名(一般无需改动) : "${EO_SET:=EO_ORIGIN}" : "${EO_CHAIN:=EO_IN}" # 本地清单文件:每行一个 IPv4 CIDR,# 开头为注释(不存在则忽略) : "${EO_CIDR_FILE:=/etc/edgeone-origin/cidr.txt}" # 远程清单 URL(可选):返回纯文本、每行一个 IPv4 CIDR。留空则仅用本地文件 : "${EO_CIDR_URL:=}" # 日志文件 : "${EO_LOG:=/var/log/eo-origin-whitelist.log}" # 放行本机回环到回源端口(cloudflared Tunnel 本机回连需要,保持 true) : "${EO_ALLOW_LOOPBACK:=true}" # auto 子命令的 tcpdump 采样窗口(秒)。0=不抓包(默认,仅 ss+conntrack 瞬时采样)。 # 回源是短连接、瞬时采样易错过 → 若 auto 常采不到 IP,设如 60~300:每次 auto # 先抓一段窗口内的回源 SYN 再合并(需已装 tcpdump;期间 auto 会阻塞该秒数)。 : "${EO_SAMPLE_SECONDS:=0}" # ---- 可配置区结束 ----------------------------------------------------------- CMD="${1:-sync}" shift 2>/dev/null || true DRY=0 for a in "$@"; do case "$a" in --dry-run) DRY=1 ;; *) echo "未知参数: $a" >&2; exit 2 ;; esac done log() { echo "[$(date '+%F %T')] $*" | tee -a "$EO_LOG" >&2; } need_root() { if [ "$(id -u)" -ne 0 ]; then echo "错误:需要 root 权限运行(iptables/ipset 需要)。请使用 sudo。" >&2 exit 3 fi } # 校验是否为合法 IPv4 地址/CIDR;裸 IP(无前缀)按 /32 视为合法 # (learn 子命令输出的就是裸 IP,必须能被清单接受) valid_v4_cidr() { local c="$1" ip pfx o1 o2 o3 o4 o case "$c" in ''|*[!0-9./]*|*.*.*.*.*) return 1 ;; esac ip="${c%/*}" pfx="${c#*/}" if [ "$ip" = "$c" ]; then pfx=32 # 无 / 前缀 -> 单 IP,等价 /32 fi case "$pfx" in ''|*[!0-9]*) return 1 ;; esac [ "$pfx" -ge 0 ] 2>/dev/null && [ "$pfx" -le 32 ] 2>/dev/null || return 1 IFS=. read -r o1 o2 o3 o4 <<<"$ip" for o in "$o1" "$o2" "$o3" "$o4"; do case "$o" in ''|*[!0-9]*) return 1 ;; esac [ "$o" -ge 0 ] 2>/dev/null && [ "$o" -le 255 ] 2>/dev/null || return 1 done return 0 } # 收集全部合法 CIDR:本地文件 + 远程 URL 的并集(去重) collect_cidrs() { local tmp tmp="$(mktemp)" local ok=1 if [ -f "$EO_CIDR_FILE" ]; then grep -vE '^[[:space:]]*(#|$)' "$EO_CIDR_FILE" 2>/dev/null | sed 's/[[:space:]]//g' >>"$tmp" || true fi if [ -n "$EO_CIDR_URL" ]; then if command -v curl >/dev/null 2>&1; then curl -fsSL --max-time 20 "$EO_CIDR_URL" 2>/dev/null >>"$tmp" || ok=0 elif command -v wget >/dev/null 2>&1; then wget -qO- --timeout=20 "$EO_CIDR_URL" 2>/dev/null >>"$tmp" || ok=0 else echo "警告:未安装 curl/wget,无法拉取远程清单 $EO_CIDR_URL" >&2 ok=0 fi fi # 过滤合法 CIDR 并去重 local out out="$(mktemp)" while IFS= read -r line || [ -n "$line" ]; do if valid_v4_cidr "$line"; then echo "$line" >>"$out"; fi done <"$tmp" rm -f "$tmp" sort -u -t. -k1,1n -k2,2n -k3,3n -k4,4n "$out" rm -f "$out" return $ok } # 一次性建立 iptables 骨架(幂等;已存在则跳过,永不在 sync 中重建,保证零窗口) setup_iptables() { # 0. 先确保 ipset 集合存在 —— 🔴 iptables-nft(nf_tables) 硬性要求: # 引用集合的规则只能在集合已存在时添加;顺序反了会报 # "Set XXX doesn't exist" / "RULE_APPEND failed (Invalid argument)", # 且失败残留会让后续 sync 无法自愈(曾实测踩坑,2026-09-03)。 # 集合创建失败则中止,绝不带着残缺骨架继续。 if ! ipset list "$EO_SET" >/dev/null 2>&1; then ipset create "$EO_SET" hash:net family inet hashsize 1024 maxelem 65536 || { log "错误:创建 ipset $EO_SET 失败"; return 1; } log "已创建 ipset 集合 $EO_SET(当前为空,等待 sync 填充)" fi local tmp tmp="$(iptables -L "$EO_CHAIN" -n >/dev/null 2>&1; echo $?)" if [ "$tmp" -ne 0 ]; then log "建立自建链 $EO_CHAIN ..." iptables -N "$EO_CHAIN" || { log "错误:创建链 $EO_CHAIN 失败"; return 1; } fi # 自建链内容(恒定,仅首次填充;若因故为空则重建) # 127.0.0.0/8 回环行:仅「不限网卡模式」经 EO_IN 命中;限定网卡模式(默认) # 下回环改由 INPUT 的 -i lo ACCEPT 负责,本行保留但不会被命中(冗余无害)。 if ! iptables -C "$EO_CHAIN" -s 127.0.0.0/8 -j ACCEPT >/dev/null 2>&1; then iptables -A "$EO_CHAIN" -s 127.0.0.0/8 -j ACCEPT fi if ! iptables -C "$EO_CHAIN" -m set --match-set "$EO_SET" src -j ACCEPT >/dev/null 2>&1; then iptables -A "$EO_CHAIN" -m set --match-set "$EO_SET" src -j ACCEPT fi # 🔴 兜底拒绝必须带 -p tcp:iptables-nft(nf_tables) 下 --reject-with tcp-reset # 要求规则已匹配 TCP 协议,否则内核校验失败报 # "RULE_APPEND failed (Invalid argument)"(实测踩坑,2026-09-03)。 # 链尾缺此行 = 非白名单流量落回 INPUT 默认策略,白名单形同虚设! # 本链仅接收 INPUT 中 -p tcp --dport 跳转的流量,加 -p tcp 语义不变。 if ! iptables -C "$EO_CHAIN" -p tcp -j REJECT --reject-with tcp-reset >/dev/null 2>&1; then iptables -A "$EO_CHAIN" -p tcp -j REJECT --reject-with tcp-reset fi # INPUT 跳转规则(幂等)。默认「仅公网网卡 eth0」:-i lo 单独放行(cloudflared # 回环回连,与网卡限定解耦),-i eth0 的入站流量才进 EO_IN 判定。 if [ -n "$EO_IFACE" ]; then # 升级清理:若曾按「不限网卡」模式插过旧跳转,先移除(不存在则忽略) iptables -D INPUT -p tcp --dport "$EO_PORT" -j "$EO_CHAIN" >/dev/null 2>&1 if [ "$EO_ALLOW_LOOPBACK" = "true" ]; then if ! iptables -C INPUT -p tcp --dport "$EO_PORT" -i lo -j ACCEPT >/dev/null 2>&1; then iptables -I INPUT 1 -p tcp --dport "$EO_PORT" -i lo -j ACCEPT log "INPUT 已插入(回环放行): -p tcp --dport $EO_PORT -i lo -j ACCEPT" fi fi if ! iptables -C INPUT -p tcp --dport "$EO_PORT" -i "$EO_IFACE" -j "$EO_CHAIN" >/dev/null 2>&1; then iptables -I INPUT 1 -p tcp --dport "$EO_PORT" -i "$EO_IFACE" -j "$EO_CHAIN" log "INPUT 已插入: -p tcp --dport $EO_PORT -i $EO_IFACE -j $EO_CHAIN" fi else # 不限网卡模式(早期行为):回环由 EO_IN 首行 127.0.0.0/8 -> ACCEPT 负责 if ! iptables -C INPUT -p tcp --dport "$EO_PORT" -j "$EO_CHAIN" >/dev/null 2>&1; then iptables -I INPUT 1 -p tcp --dport "$EO_PORT" -j "$EO_CHAIN" log "INPUT 已插入: -p tcp --dport $EO_PORT -j $EO_CHAIN" fi fi return 0 } # 原子替换集合内容(临时集合 -> swap -> 销毁;iptables 引用同名集合不受影响) sync_set() { local tmp_set list n=0 tmp_set="${EO_SET}_tmp_$$" list="$(mktemp)" cat >"$list" # 从 stdin 读入 CIDR n="$(wc -l <"$list")" if [ "$n" -eq 0 ]; then log "错误:清单为空,拒绝更新(保持现有白名单不变)" rm -f "$list"; return 1 fi if [ "$DRY" -eq 1 ]; then log "[dry-run] 将加载 $n 个 CIDR 到 $EO_SET" sed 's/^/ + /' "$list" rm -f "$list"; return 0 fi ipset create "$tmp_set" hash:net family inet hashsize 1024 maxelem 65536 2>/dev/null || { log "错误:创建临时集合 $tmp_set 失败"; rm -f "$list"; return 1; } local line rc=0 while IFS= read -r line || [ -n "$line" ]; do ipset add "$tmp_set" "$line" 2>/dev/null || rc=1 done <"$list" rm -f "$list" if [ "$rc" -ne 0 ]; then log "警告:个别 CIDR 添加失败(可能已存在或语法异常),继续执行" fi ipset swap "$EO_SET" "$tmp_set" || { log "错误:ipset swap 失败"; ipset destroy "$tmp_set" 2>/dev/null; return 1; } ipset destroy "$tmp_set" 2>/dev/null return 0 } # 检测 ufw 等可能存在的"全局放行该端口"的规则,提醒用户收敛 warn_open_rules() { if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -qE "^$EO_PORT\b|:$EO_PORT\b"; then log "⚠️ 检测到 ufw 正在放行端口 $EO_PORT(ANYWHERE)。请执行 ufw delete allow $EO_PORT 撤掉全局放行," log " 否则白名单形同虚设(虽然本脚本的 REJECT 兜底仍会先拦截非白名单,但规则冗余易误判)。" fi if iptables -S INPUT | grep -E -- "--dport $EO_PORT([ /]|$)" | grep -vE "jump $EO_CHAIN|set| -i lo" | grep -qE "ACCEPT|ALLOW"; then log "⚠️ INPUT 中存在非本脚本管理的放行规则匹配端口 $EO_PORT,请人工核对是否应删除。" fi } cmd_sync() { need_root # 1. 校验依赖 for b in ipset iptables; do command -v "$b" >/dev/null 2>&1 || { echo "错误:缺少依赖 $b。Debian/Ubuntu 执行: apt-get install -y $b" >&2; exit 4; } done # 2. 收集清单(本地 + 远程)。collect_cidrs 返回 0 = 出现过来源失败,1 = 来源均正常 local cidrs ok cidrs="$(mktemp)" collect_cidrs >"$cidrs" ok=$? local n n="$(wc -l <"$cidrs")" # 真·来源全部失败:配置了 URL 且拉取失败,同时本地文件无任何条目 if [ "$ok" -eq 0 ] && [ "$n" -eq 0 ]; then log "错误:清单来源全部失败(EO_CIDR_URL=${EO_CIDR_URL:-未配置} 拉取失败,且本地文件 $EO_CIDR_FILE 无有效条目)" rm -f "$cidrs"; exit 1 fi if [ "$n" -eq 0 ]; then local src_msg if [ -f "$EO_CIDR_FILE" ]; then src_msg="文件 $EO_CIDR_FILE 存在但无有效 IPv4/CIDR 条目" else src_msg="文件 $EO_CIDR_FILE 不存在" fi log "错误:清单为空($src_msg;EO_CIDR_URL=${EO_CIDR_URL:-未配置})——保持现网白名单不变(fail-safe)" log "提示:空清单拒绝更新是保护机制(绝不拿空白名单上线)。若尚未生成清单:" log " 段级定稿方案(2026-09-03 实测,勿再逐 IP 采种):cidr.txt 直接放行" log " EO 所属腾讯云大段,实测生效:43.146/43.152/43.168/43.174/43.175 五个 /16," log " 逐行写入 $EO_CIDR_FILE 后重试本命令(脚本原生支持 CIDR;详细见脚本头注释 3)" rm -f "$cidrs"; exit 1 fi log "收集到 $n 个合法 IPv4 CIDR(来源: $EO_CIDR_FILE${EO_CIDR_URL:+ + $EO_CIDR_URL})" # 3. 建立骨架(幂等) setup_iptables || { rm -f "$cidrs"; exit 1; } # 4. 原子更新集合 if [ "$DRY" -eq 0 ]; then sync_set <"$cidrs" || { rm -f "$cidrs"; exit 1; } log "白名单已更新(swap 完成),当前生效 $(ipset list "$EO_SET" -t 2>/dev/null | awk '/Number of entries/{print $NF}') 个条目" else sync_set <"$cidrs" fi rm -f "$cidrs" warn_open_rules log "sync 完成" } cmd_learn() { need_root echo "# EdgeOne 回源端口 $EO_PORT 当前活跃连接来源 IP($(date '+%F %T'))" >&2 echo "# 说明:以下 IP 是此刻正在回源到本机的 EO 边缘出口 IP。可多时段多次执行" >&2 echo "# 并去重合并后作为种子清单写入 $EO_CIDR_FILE;人工审阅后启用白名单。" >&2 echo "# 仅输出 IPv4(脚本白名单只管理 IPv4,见头注释 5)。" >&2 local out out="$(ss -Htn state established "( sport = :$EO_PORT )" 2>/dev/null \ | awk '{print $5}' \ | sed -E 's/^\[([0-9a-fA-F:]+)\]:.*/\1/; s/:[0-9]+$//' \ | grep -vE '^127\.|^::1$' \ | grep -v ':' \ | sort -u)" if [ -z "$out" ]; then echo "(0 条活跃连接:当前没有正在回源的 EO 连接)" >&2 echo "排查提示:" >&2 echo " ① 端口核对:TamaBox 实际监听端口是否 = EO_PORT=$EO_PORT?" >&2 echo " 执行: ss -tlnp | grep LISTEN(应看到 0.0.0.0:$EO_PORT 的监听行)" >&2 echo " ② 制造流量:需【境内网络】访问站点才会走 EO 直连回源(境外访问走 CF Tunnel," >&2 echo " 对端是 127.0.0.1 会被过滤)。访问后立刻重跑本命令;回源是短连接,错过即无。" >&2 echo " ③ 看含 TIME-WAIT 的痕迹(访问后 60 秒内):ss -Htan | grep :$EO_PORT" >&2 echo " ④ 最稳办法:tcpdump -ni <网卡> tcp port $EO_PORT and 'tcp[tcpflags] & tcp-syn != 0'" >&2 echo " 然后触发一次境内访问,出现的源 IP 即 EO 回源 IP。" >&2 else echo "$out" fi } # 采集「正在/近期」回源到本机 $EO_PORT 的出口 IP(并集,尽力而为,允许部分来源缺失): # ① ss established —— 此刻活跃连接(与 learn 同源) # ② conntrack —— 近期连接(窗口比 ss 宽:TIME_WAIT 等保留态;工具缺失时读 /proc/net/nf_conntrack) # ③ tcpdump 采样窗口 —— EO_SAMPLE_SECONDS>0 时抓窗口内回源 SYN(回源短连接错过即无,抓包最稳) # 仅输出 IPv4 裸 IP(去重);IPv6/回环/本机地址一律剔除。 collect_live_ips() { local tmp tmp="$(mktemp)" # ① ss(此刻活跃) ss -Htn state established "( sport = :$EO_PORT )" 2>/dev/null \ | awk '{print $5}' \ | sed -E 's/^\[([0-9a-fA-F:]+)\].*/\1/; s/:[0-9]+$//' >>"$tmp" # ② conntrack(近期;优先工具,缺失回退 /proc) if command -v conntrack >/dev/null 2>&1; then conntrack -L -p tcp --dport "$EO_PORT" 2>/dev/null \ | awk '{for(i=1;i<=NF;i++) if($i~/^src=/){split($i,a,"="); print a[2]; break}}' >>"$tmp" elif [ -r /proc/net/nf_conntrack ]; then awk -v p="$EO_PORT" ' { for(i=1;i<=NF;i++){ if($i ~ /^dport=/){ split($i,a,"=") if(a[2]==p){ for(j=1;j>"$tmp" fi # ③ tcpdump 窗口(可选):EO_SAMPLE_SECONDS 必须为正整数秒 if [ -n "${EO_SAMPLE_SECONDS:-}" ]; then case "$EO_SAMPLE_SECONDS" in ''|*[!0-9]*) log "提示:EO_SAMPLE_SECONDS 必须为正整数秒(当前值: ${EO_SAMPLE_SECONDS}),已跳过 tcpdump 窗口采样" ;; *) if [ "$EO_SAMPLE_SECONDS" -gt 0 ] 2>/dev/null; then if command -v tcpdump >/dev/null 2>&1; then log "tcpdump 采样 ${EO_SAMPLE_SECONDS}s(网卡 ${EO_IFACE:-any}),请稍候 ..." # 🔴 坑(实测,2026-09-03):filter 必须不带引号!手动执行时单引号由 shell # 吃掉;脚本里写在双引号内会变成【字面单引号】传给 tcpdump,pcap 编译器 # 不认识引号 → syntax error 立即退出 → 表现为「没等窗口就结束、采到 0 个」。 # 双引号内写裸 filter 即可(无空格歧义,变量已展开)。 timeout "$EO_SAMPLE_SECONDS" tcpdump -ni "${EO_IFACE:-any}" \ "tcp port $EO_PORT and tcp[tcpflags] & tcp-syn != 0" 2>/dev/null \ | awk '{ for (i=1; i") { print $i; break } }' \ | sed -E 's/\.[0-9]+$//' >>"$tmp" log "tcpdump 采样结束" else log "提示:EO_SAMPLE_SECONDS=$EO_SAMPLE_SECONDS 但未安装 tcpdump,跳过窗口采样(apt-get install -y tcpdump)" fi fi ;; esac fi # 统一清洗:IPv6(含冒号) / 回环 / 非 IP 剔除,去重 grep -vE '^127\.|^::1$' "$tmp" | grep -v ':' | sort -u rm -f "$tmp" } # 把采样到的 IP 增量合并进清单文件(只增不删)。返回码: # 0 = 有新增(文件已更新,调用方应触发 sync);1 = 无新增;2 = 采到为空且清单无条目;3 = 写入失败 merge_live_ips() { local ips="$1" old new n_old n_new old="$(mktemp)"; new="$(mktemp)" if [ -f "$EO_CIDR_FILE" ]; then grep -vE '^[[:space:]]*(#|$)' "$EO_CIDR_FILE" | sed 's/[[:space:]]//g' | sort -u >"$old" fi # 新集合 = 旧条目 ∪ 本次采样(逐条过合法性校验) { cat "$old"; printf '%s\n' "$ips"; } \ | while IFS= read -r l || [ -n "$l" ]; do if [ -n "$l" ] && valid_v4_cidr "$l"; then echo "$l"; fi done | sort -u >"$new" n_new="$(wc -l <"$new")" if [ "$n_new" -eq 0 ]; then rm -f "$old" "$new"; return 2 fi if [ "$DRY" -eq 1 ]; then local added added="$(comm -13 "$old" "$new")" if [ -n "$added" ]; then echo "将新增到 $EO_CIDR_FILE:" echo "$added" | sed 's/^/ + /' else echo "无新增(清单与采样一致)" fi rm -f "$old" "$new"; return 0 fi n_old="$(wc -l <"$old")" # 无变化:不写文件(保留原时间戳/注释),返回 1 if [ "$n_old" -eq "$n_new" ] && ! comm -3 "$old" "$new" | grep -q .; then rm -f "$old" "$new"; return 1 fi # 写回:原 # 注释行(若有)置于文件头,其后为排序后的条目;原子替换 mkdir -p "$(dirname "$EO_CIDR_FILE")" || { rm -f "$old" "$new"; return 3; } { grep -E '^[[:space:]]*#' "$EO_CIDR_FILE" 2>/dev/null; cat "$new"; } \ >"${EO_CIDR_FILE}.tmp" || { rm -f "$old" "$new"; return 3; } mv "${EO_CIDR_FILE}.tmp" "$EO_CIDR_FILE" || { rm -f "$old" "$new"; return 3; } comm -13 "$old" "$new" | sed 's/^/ + /' rm -f "$old" "$new" return 0 } # 自动收集并应用(定时入口,推荐 cron/systemd 每天调用): # 采集回源 IP -> 增量合并进清单(只增不删)-> 有新增则 sync 应用。 # 说明:失效/旧节点条目不在此自动移除(地址属 EO、无安全风险,见 guide §4.4); # 人工删掉清单条目后,本命令内的 sync 会自动完成剔除落地。 cmd_auto() { need_root log "auto 开始:采集 EO 回源 IP 并合并白名单 ..." local ips n rc ips="$(collect_live_ips)" n=0 [ -n "$ips" ] && n="$(printf '%s\n' "$ips" | wc -l)" log "采样到 $n 个回源 IP(ss${EO_SAMPLE_SECONDS:+ + tcpdump $EO_SAMPLE_SECONDS s},conntrack 视可用性)" merge_live_ips "$ips" rc=$? case "$rc" in 0) log "清单已更新(只增不删,不影响既有条目);调用 sync 应用 ..." cmd_sync ;; 1) log "无新增回源 IP,清单与白名单保持现状" ;; 2) log "未采到任何回源 IP(清单也为空)——不生成空清单(fail-safe),保持现状" echo " 排查提示:" >&2 echo " ① auto 依赖真实 EO 回源流量:需【境内网络】访问站点触发 EO 直连回源" >&2 echo " (境外访问走 CF Tunnel,对端 127.0.0.1 被过滤,永远采不到);" >&2 echo " ② 端口核对:TamaBox 监听端口是否 = EO_PORT=$EO_PORT(ss -tlnp | grep LISTEN);" >&2 echo " ③ 提高命中率:设 EO_SAMPLE_SECONDS=60~300 让每次 auto 附带抓包窗口" >&2 echo " (需 apt-get install -y tcpdump),或把定时改到境内访问高峰时段。" >&2 ;; 3) log "错误:清单文件写入失败" exit 1 ;; esac log "auto 完成" } cmd_show() { need_root echo "== ipset 集合 $EO_SET ==" if ipset list "$EO_SET" >/dev/null 2>&1; then ipset list "$EO_SET" -t echo "-- 条目 --" ipset list "$EO_SET" | sed -n '/Members:/,$p' | grep -v '^Members:' | grep -vE '^$' | head -40 else echo "(集合不存在,尚未初始化)" fi echo echo "== iptables INPUT 相关规则 ==" iptables -S INPUT | grep -E -- "--dport $EO_PORT|jump $EO_CHAIN|^-A INPUT -i lo" || echo "(无)" echo echo "== 自建链 $EO_CHAIN ==" iptables -S "$EO_CHAIN" 2>/dev/null || echo "(链不存在)" } cmd_flush() { need_root echo "将移除:INPUT 跳转规则、自建链 $EO_CHAIN、ipset 集合 $EO_SET(仅本脚本管理部分)" if [ "$DRY" -eq 1 ]; then echo "[dry-run] 未执行任何操作"; return 0; fi # 删除 INPUT 中所有指向本链的规则(含历史多端口残留) iptables-save | grep -E -- "^-A INPUT .*-j $EO_CHAIN" | while IFS= read -r rule; do args="${rule#-A INPUT }" iptables -D INPUT $args 2>/dev/null && echo "已删除: -A INPUT $args" done # 删除限定网卡模式插入的回环放行规则(不存在则忽略) iptables -D INPUT -p tcp --dport "$EO_PORT" -i lo -j ACCEPT 2>/dev/null && echo "已删除: 回环放行(-i lo)" iptables -F "$EO_CHAIN" 2>/dev/null iptables -X "$EO_CHAIN" 2>/dev/null && echo "已删除链 $EO_CHAIN" ipset destroy "$EO_SET" 2>/dev/null && echo "已销毁集合 $EO_SET" echo "回滚完成。原"开放+端口"策略若由 ufw 管理,请确认 ufw allow $EO_PORT 规则仍存在(本脚本不触碰 ufw)。" } case "$CMD" in sync) cmd_sync ;; learn) cmd_learn ;; auto) cmd_auto ;; show) cmd_show ;; flush) cmd_flush ;; -h|--help|help) sed -n '2,60p' "$0" | sed 's/^# \{0,1\}//' ;; *) echo "用法: $0 {sync|learn|auto|show|flush} [--dry-run]" >&2; exit 2 ;; esac