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.
523 lines
25 KiB
Bash
523 lines
25 KiB
Bash
#!/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
|