一份可执行运维手册:任何一台公网出口用 frp 转发、且 frps 有落盘日志的机器,都能照着 Step 0 → Step 4 独立解决同类问题。
结构:先给 30 秒快速路径(给复读这个文档的我),再给完整分析,最后给可复制的命令序列。
记录日期:2026-09-28(示例环境:frps + Ubuntu,家庭服务器经 frp 隧道暴露 SSH/邮件)。
零、30 秒快速路径(复读本手册时先看这里)
症状:自建隧道(SSH/邮件)经公网端口转发,出现"明明隧道通、却连不上、连接反复超时"。
证据:frp 服务端日志(/var/log/frps.log 或 frps.toml 的 log_file 指定路径)出现大量:
[ssh] get a user connection [某公网IP:随机端口]
解题顺序(细节见各 Step):
- Step 1 定量:统计日志里源 IP 连接数排行 + 单 IP 节奏,画出"攻击频率"和"自己使用频率"。
- Step 2 定红线:红线 = 卡在"攻击者高频"与"我们极限"之间的空档。
maxretry = 红线(次/分钟) × findtime(分钟)。 - Step 3 检查单 → 落地:先过"防误封自己"检查单(重点:确认封的不是自己/NAT 同池),再写 filter、配 jail、reload。
- Step 4 验证回滚:
fail2ban-client status看封禁、iptables -L验证、unbanip解封、ufw 固化手动规则。
红线公式(本手册核心):
红线次/分钟 = 通常取"攻击者高频频率"以下、"我们使用上限"以上的中间值 maxretry = 红线次/分钟 × findtime(分钟) 示例:findtime=1 分钟、红线 72 次/分钟 → maxretry=72
最优先的一条防翻车铁律:
封 IP 之前,先确认那个 IP 不是你自己的出口 IP。验证法:从受保护机子上
curl外部、或查 nginx/SSH 日志取合法源,与"想封的 IP"比对。宁可漏封,不可错封。
一、起因(参考案例:这问题长什么样)
自建家庭邮件服务器:
- 家里跑 sshd + frpc,服务经 frp 隧道暴露到公网一台服务器;
- 公网服务器用 frps 监听一个对外代理端口,把连接原样转发回家(连北京某个对端口 = 进家里 sshd);
- 平时管理服务器、收发邮件全走这条隧道。
浮出水面的问题:连着连着就断,SSH 连接反复失败(连续重试 12 次全部 connection failed)。一度怀疑隧道、frp、家里 sshd 配置,排查无果——最后盯住公网侧日志,真凶是该端口被外部 IP 高频敲门:
2026-09-28 11:25:07 [ssh] get a user connection [45.148.10.66:59106] 2026-09-28 11:25:17 [ssh] get a user connection [45.148.10.66:44746] 2026-09-28 11:25:26 [ssh] get a user connection [45.148.10.66:49328]
攻击现场数据(本案例 frps.log 统计)
| 攻击者 | 频率 | 间距 |
|---|---|---|
| 高频洪水 bot | 约 84 次/分钟 | 每 ~0.7 秒一条 |
| 低频持续 bot | 约 6 次/分钟 | 每 ~10 秒一条 |
| 当日该端口连接总量 | 单日 5438 条 | — |
高频洪水打爆 frps 入站队列 → 挤占家里 sshd 的 pre-auth 等待队列 → 表现成"隧道是通的、人却连不进"。
二、分析:难点在哪
1. frp 纯转发,天生无"认证防线"
frps 只做:对外端口收到连接 → 原样打包转发回家。不校验用户、不区分是否自己人,公网任何人都能连,"进来的一视同仁"。
2. 我们和低频 bot 在日志里"行为重叠"
fail2ban 靠行为频率区分人机。实测:
| 来源 | 频率 |
|---|---|
| 低频 bot | 约 6 次/分钟 |
| 我们正常 SSH 操作 | 约 3–12 次/分钟 |
两个都在每分钟几次的量级,不可用频率区分。任何"按频率封"要么误伤自己、要么漏掉低频 bot。
3. "IP 白名单"方案是死路(重点教训)
一度想用 fail2ban 的 ignoreip(豁免名单)加我们出口 IP,发现不可行:
- 家宽/公司出口 IP 几乎都是动态的(PPPoE 重启就换、NAT 池轮换);
- 白名单今天有效明天失效,反而把真自己关门外;
ignoreip只能当"纵深保险",不能当防线依赖。
4. 差点误伤自己的"假 bot"乌龙(案例)
日志里一个高频"bot" 103.91.179.102,看着极像扫描器。对账才知是自己:
- tcpdump 抓包:我们直连该出口端口的时刻,103.91.179.102 正好在完成完整 SSH 握手并传输上百 KB;
- 本机 curl 该服务器,nginx access.log 记下它:
103.91.179.102 - - [28/Sep/2026:12:06:25 +0800] "GET / HTTP/1.1" 200 62164 "-" "curl/8.13.0"
"高频 bot"就是我们自己。教训:确认源 IP 真相前动手封禁,第一批误伤往往是自己人。排查链:frps 日志对不上 → tcpdump 抓包 → 应用日志(nginx access.log)实锤出口 IP。
5. fail2ban 工作原理
攻击者每连一次对外端口 → frps 写一行日志 → fail2ban filter(正则)抠出源 IP(<HOST>) → 该 IP 犯规 +1 → 判定:findtime 窗口内达到 maxretry → 执行 iptables DROP → 关押 bantime 秒;到期自动解封;解封后还高频就再封
fail2ban 不看人,只看"一个源 IP 的行为频率"。①加豁免名单只管自己身份的 GAP;②阈值就是核心决策——详见 Step 2。
三、解法:四步操作法
Step 0 · 环境清点
逐项确认(打勾):
- [ ] 哪台机器是 frps(frp 服务端)?有没有 sudo?
- [ ] frps 日志在哪?读配置:sudo grep -E 'log_file' /etc/frp/frps.toml(或 frps.ini;默认 /var/log/frps.log)。
- [ ] 日志里"连接行"是什么样?先跑:sudo tail -n 20 <frps日志>,找 get a user connection 这类行。
- [ ] 有没有一台"合法管理机"(你日常操作用的机器),并知道它的出口 IP?(不知道怎么查→Step 1-C)
- [ ] fail2ban 装了吗:fail2ban-client version(没装→Step 3 安装)。
后续所有
<frps日志>都表示 Step 0 命中的实际路径;所有<port>表示你的对外代理端口。
Step 1 · 定量:测两组频率(别猜)
1-A 看谁在打(源 IP 连接数排行榜)
sudo tail -n 2000 <frps日志> | grep 'get a user connection' | sed -E 's/.*[([0-9.]+):[0-9]+].*/1/' | sort | uniq -c | sort -rn | head -15
输出形如 115 45.148.10.66 —— 前几名就是疑似攻击源。
1-B 看单个 IP 的节奏(这是高频洪水还是慢性骚扰)
sudo grep 'get a user connection' <frps日志> | grep '45.148.10.66' | tail -20
- 间隔稳定 ~0.5–2 秒 → 高频洪水(会打瘫服务,重点封)
- 间隔稳定 ~10 秒及以上 → 低频骚扰(无害,放养)
1-C 测"自己"的使用频率(防误封的基准)
① 先拿到你的出口 IP:从受保护机器(frps 那台)访问外部 echo 并用应用日志记录,例如:
# 受保护机器上起一个临时记录,或直接查 nginx access.log 最近一条 GET # 然后从你的管理机 curl 受保护机器,回看它记的源 IP 是不是"你" curl -s http://<受保护机器>/ && ssh <受保护机器> "sudo tail -3 /var/log/nginx/access.log"
② 统计自己正常使用频率:从管理机连续执行固定次数连接并观察记录密度:
# 管理机上: for i in $(seq 1 10); do ssh -p <port> 用户@主机 "true"; done # 受保护机器上,看这 10 次在日志里落在多宽的窗口、隔了多久 sudo grep 'get a user connection' <frps日志> | tail -20
估算出:正常操作大概每分钟几条(本案例 3–12 条)。
另注意环境差异:如果你用自动化循环跑批量命令,请把那也测一次,取上限计入"我们使用上限"。
1-D 判别口诀:连接规律(间距稳定、全天不断、次数进入 Top)且 不是你自己的出口 IP → 视为攻击源。
Step 2 · 定红线 → 换算 params
红线选择:压在「攻击者高频频率」之下、与「我们使用上限」之间。本案例:攻击高频 84 次/分、我们上限 ~60 次/分、正常 3–12 次/分,红线取 72 次/分钟(比我们上限高 1.2 倍、比洪水低 15%)。原则:偏离任何一边过近都危险——离自己近误封,离洪水近漏防。
换算公式(唯一要记住的):
findtime(分钟) 选窗口长度(fail2ban 里以秒写,60s=1 分钟) maxretry = 红线(次/分钟) × findtime(分钟)
本环境示例:findtime=60s(1分钟) → maxretry = 72×1 = 72。
如果你想要更宽窗口防"换 IP 慢速炮":findtime=600s(10分钟) → maxretry = 72×10 = 720(语义:10 分钟内超过 720 条才封)。
判定效果(本案例红线下):
| 来源 | 频率 | 判定 |
|---|---|---|
| 高频洪水 bot | 84 次/分钟 | 封(越线) |
| 低频 bot | 6 次/分钟 | 放养(差 12 倍,无害) |
| 我们正常操作 | 3–12 次/分钟 | 放养(差 6–24 倍) |
| 我们批量循环 | ~60 次/分钟 | 仍低于红线 |
策略一句话:低频骚扰不封(无危害),高频爆发必封;且零依赖 IP。
Step 3 · 动手前检查单 + 落地配置
动手前检查单(每条必过,防自锁):
- [ ] ① 被封目标是攻击者(确认过 1-A/1-B,且不是你的出口 IP—见 1-C)。
- [ ] ② 不会误封 NAT 同池:若你公司与攻击源同运营商/同出口池,慎封(宁可改用窄 action)。
- [ ] ③ 你不是正通过"目标机器 + 该 IP"在远程操作(否则先保证自己有一条逃生通道,或只封目标端口不封全端口)。
- [ ] ④ 动作面先窄后宽:action 用
iptables(只封指定端口)比iptables-allports(封全端口)更稳。 - [ ] ⑤ 先备份:
sudo cp /etc/fail2ban/jail.local /etc/fail2ban/jail.local.bak。
3-1 安装
sudo apt install -y fail2ban tcpdump # tcpdump 用于 Step1-C/验证
3-2 写 filter("日志探查→写正则"通用套路)
套路四步:
1. sudo tail -n 20 <frps日志> 看真实行长啥样;
2. 找到不变的特征片段(如 [ssh] get a user connection);
3. 写出把 IP 放到 <HOST> 的正则:^S+s+S+s+[ssh] get a user connection [<HOST>:[0-9]+](或宽松版 ^.*[ssh] get a user connection [<HOST>:[0-9]+]);
4. 立刻验证匹配率:sudo fail2ban-regex <frps日志> '<你的正则>',结果应"大量匹配且命中 <HOST> 能解出源 IP"。
写好文件 /etc/fail2ban/filter.d/frps-ssh.conf:
[Definition] failregex = ^.*[ssh] get a user connection [<HOST>:[0-9]+]
⚠️
<HOST>前不要加反斜杠转义,否则抓不到 IP。
⚠️ 如果你们 frps 版本/日志格式不同(关键是连行里有没有源 IP),按上面套路 1–4 重写。若日志完全走 journald 而无落盘文件,则 jail 改用backend = systemd(并相应指定 journal 匹配),本手册默认落盘文件场景。
验证:
sudo fail2ban-regex <frps日志> /etc/fail2ban/filter.d/frps-ssh.conf # 期待:失败行大量匹配;例:5680 行日志,4345 匹配
3-3 配 jail.local(红线填进去)
/etc/fail2ban/jail.local:
[DEFAULT] bantime = 1800 findtime = 60 [sshd] enabled = true maxretry = 5 [frps-ssh] backend = pyinotify enabled = true filter = frps-ssh logpath = <frps日志> maxretry = 72 # = 红线72次/分钟 × findtime(1分钟),按 Step2 公式填 findtime = 60 bantime = 1800 # 封半小时 action = iptables[name=frps-ssh, port="<port>"] # 先窄后宽:只封目标端口 [DEFAULT] ignoreip = 127.0.0.1/8 ::1 # 纵深保险;如需加你的出口 IP 可以,但不要依赖它
重载生效:
sudo fail2ban-client reload sudo fail2ban-client status frps-ssh
3-4 应急:fail2ban 就位前的高频洪水先手动压
sudo iptables -I INPUT 1 -s <攻击IP1> -j DROP sudo iptables -I INPUT 1 -s <攻击IP2> -j DROP
⚠️ 手动规则是内存态,重启丢。要固存用 ufw(写到 /etc/ufw/,开机由服务拉起):
sudo ufw deny from <攻击IP>
fail2ban 的封禁则自动持久化(记在数据库,重启自动重新下发),无需处理。
Step 4 · 验证与回滚
验证封禁生效:
sudo fail2ban-client status frps-ssh # 看 Currently banned 与 Banned IP list sudo iptables -L INPUT -n --line-numbers | head # 看 DROP 规则在不在、行号多少 # 对端试连通(DROP 生效应该是超时无响应): timeout 3 curl -s -o /dev/null --connect-timeout 3 http://<攻击IP>/ ; echo "exit=$?" # exit 124 = 超时 = 生效
手动加封 / 解封(fail2ban 管理的):
sudo fail2ban-client set frps-ssh banip <IP> # 想立刻封某个 IP sudo fail2ban-client set frps-ssh unbanip <IP> # 解封(回滚)
删手动 iptables 规则(回滚):
sudo iptables -L INPUT -n --line-numbers # 找到要删的行号 N sudo iptables -D INPUT N
重启后:fail2ban 通过数据库自动续封;手动 ufw 规则自动恢复;手动 iptables 规则(未走 ufw)丢失——记得用 ufw 固化。
四、结果(本案例)
- 高频洪水(每分钟 84 次)被压灭,对外端口恢复直连、SSH 秒连(0.4–0.6s);
- 低频 bot 仍在刷存在感,但始终在红线之下、永不触发、且无害;
- 我们正常连接与红线差 6–24 倍,出口 IP 怎么变都不影响;
- 防线零依赖固定 IP,可持续运行。
五、经验总结
- 动手封禁前先确认源 IP 真相:tcpdump + 应用日志交叉验证,否则第一批误伤自己(本手册就是反面教材)。
- 白名单死于"出口 IP 固定"假设:动态公网下不可依赖;频率阈值才是无惧地址变化的硬防线。
- 频率取量必须把攻击者与正常使用放同一张表:红线压在两者之间的空档。
- 低频骚扰无害不必封:封禁资源要用在能打瘫服务的高频爆发上。
- fail2ban 是"不看人、看频率"的自动安保:高频自动 DROP、定时解封、自动续封,人不用盯屏。
- 动作面先窄后宽:新环境先用只封目标端口的 action(
iptables)跑通,确认无误后可视情况加固。
完
文章评论