Files
tamabox/scripts/edgeone-origin-whitelist.sh
tamakyi e3e59e7c69 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.
2026-09-10 10:56:27 +08:00

523 lines
25 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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