chore: baseline snapshot before moderation-strictness feature

Repo .git was damaged (refs/ missing, index truncated, object store near-empty).
Repaired via git init (rebuilt refs), stale index preserved as .git/index.corrupt-20260910.bak,
then full worktree committed as a rollback baseline.
This commit is contained in:
tamakyi
2026-09-10 10:56:27 +08:00
commit e3e59e7c69
296 changed files with 67458 additions and 0 deletions
+522
View File
@@ -0,0 +1,522 @@
#!/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 <PORT> -> ACCEPT (cloudflared Tunnel 本机回连)
# -i eth0 -p tcp --dport <PORT> -> 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 <PORT> 跳转的流量,加 -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<i;j++) if($j~/^src=/){split($j,b,"="); print b[2]; break} break }
} } }' /proc/net/nf_conntrack >>"$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<NF; i++) if ($(i+1) == ">") { 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