Fail2Ban 开发者指南:从代码测试、编码规范到服务端架构设计
【免费下载链接】fail2banDaemon to ban hosts that cause multiple authentication errors项目地址: https://gitcode.com/gh_mirrors/fa/fail2ban
本篇以仓库根目录 DEVELOP(由 doc/develop.rst 引入)为骨架编写,面向希望为 Fail2Ban 贡献代码、或想深入理解其服务端内部设计的开发者。文章覆盖从 Git 工作流、Pull Request 提交规范、测试与覆盖率工具链,到 server/ 目录下 Jail、Filter、FailManager、Actions 等核心组件的类层次与数据流,并结合
fail2ban/server/源码逐一印证文档中的架构描述,帮助读者快速上手开发与调试。
一、开发环境与协作流程
Fail2Ban 使用 Git 分布式版本控制进行开发,每个开发者都拥有仓库的完整副本,可以自由地添加分支、切换分支、提交本地修改,随后请求维护者合并(merge)自己的改动。
- 代码托管在 GitHub 的 fail2ban/fail2ban 仓库,GitHub 提供开源项目免费托管、基于 Web 的 Git 仓库浏览与 Issue 跟踪。
- 如果你熟悉 Python,并且想提交一个 bug 修复或新特性,推荐的方式是使用 GitHub 的 Pull Request(PR)功能。
重要前提:本文基于当前仓库源码描述开发流程。实际向官方项目提交代码时,需要以官方仓库的实时分支状态和贡献要求为准;本文给出的所有命令、路径与代码引用均以本仓库当前内容为基准。
1.1 Pull Request 提交要求
文档 DEVELOP 明确要求,提交 Pull Request 时应当:
- 清晰描述你要解决的问题(Clearly describe the problem you're solving);
- 避免引入回归,不给系统管理员升级带来困难(Don't introduce regressions);
- 如果添加的是主要特性(major feature),请在 master 上 rebase 你的改动并压缩为单个 commit;
- 包含测试用例(详见下文"代码测试");
- 包含样例日志(如果相关,尤其对于 filter 开发);
- 更新 ChangeLog的相应章节;
- 如果 THANKS 中还没有你,请加入自己的名字。
如果正在开发新的 filter(日志过滤规则),请查阅 FILTERS 文件,其中有专门的文档说明。
二、代码测试:测试用例、覆盖率与手动执行
2.1 运行现有测试
现有测试通过bin/fail2ban-testcases脚本运行,该脚本位于仓库 bin/fail2ban-testcases(另有聚合脚本 fail2ban-testcases-all 与 fail2ban-testcases-all-python3)。它带有--log-level等常用选项:
bin/fail2ban-testcases --help--help会列出全部可用选项。文档特别提醒:测试用例应当覆盖所有常规情况、所有异常情况,以及所有边界内/边界外条件,并且应当覆盖所有分支。
测试源码位于 fail2ban/tests/,按模块拆分,例如:
- fail2ban/tests/failmanagertestcase.py ——
FailManager的单元测试(含AddFailure、FailmanagerComplex等测试类); - fail2ban/tests/filtertestcase.py —— Filter 相关测试;
- fail2ban/tests/actiontestcase.py 与 fail2ban/tests/actionstestcase.py —— Action 相关测试;
- fail2ban/tests/servertestcase.py、fail2ban/tests/databasetestcase.py、fail2ban/tests/datedetectortestcase.py 等。
2.2 覆盖率(coverage)工具链
安装python-coverage包后可可视化测试覆盖率(注意:在 Debian 系系统中脚本名为python-coverage)。运行:
coverage run bin/fail2ban-testcases coverage report可选地生成 HTML 报告:
coverage html然后浏览器打开htmlcov/index.html即可查看测试用例对代码库的覆盖程度。覆盖率百分之百是好事,但文档同时提醒:"全覆盖"并不意味着完备,应尽量让测试覆盖尽可能多的独立代码路径。覆盖率工具还能帮助识别缺失的分支——关于分支覆盖率可参考 coverage.py 官方的分支文档。
2.3 手动执行:在开发环境中运行 fail2ban
在开发环境(不安装到系统)中手动运行,文档给出了标准命令:
./fail2ban-client -c config/ -s /tmp/f2b.sock -i start参数含义:-c config/指定配置文件目录为仓库内的 config/,-s /tmp/f2b.sock指定控制 socket 路径,-i以交互模式运行,start启动服务端。
启动后可以依次输入下列命令做快速验证(这也是文档推荐的 smoke test 流程):
status add test pyinotify status test set test addaction iptables set test actionban iptables echo <ip> <cidr> >> /tmp/ban set test actionunban iptables echo <ip> <cidr> >> /tmp/unban get test actionban iptables get test actionunban iptables set test banip 192.168.2.2 status test这段流程演示了 Fail2Ban 交互式控制台的核心操作模式:
status:查看全局/监狱状态;add <jail> <backend>:动态添加一个名为test、后端为pyinotify的监狱;set test addaction iptables:为test监狱挂载iptables动作;set test actionban iptables .../set test actionunban iptables ...:运行时改写动作的actionban/actionunban命令模板,这里将封禁/解封事件回显到/tmp/ban、/tmp/unban,便于观察;get test actionban iptables:读取当前生效的actionban命令;set test banip 192.168.2.2:手动封禁一个 IP(等价于执行一次动作);- 再次
status test:确认封禁已生效。
这些命令对应服务端 transmitter.py 中set/get/add/status等命令处理器,并由 server.py 的setBanIP、addAction等方法落地。命令行工具的完整说明见 man/fail2ban-client.1(例如set <JAIL> addaction <ACT>...、set <JAIL> banip <IP>...)。
2.4 使用 Vagrant 进行隔离测试
仓库根目录提供了 Vagrantfile,可以在虚拟机中做攻防测试,共建立两台 VM:
- "secure":用于测试 fail2ban 代码;
- "attacker":用于对 secure VM 发起攻击。
两台 VM 共享192.168.200/24网段。如果你所在的网络恰好使用该网段,请检查 Vagrantfile 并修改 IP 以避免冲突。
三、编码规范(Coding Standards)
3.1 风格与测试要求
| 项目 | 要求 |
|---|---|
| Style | 目前请使用**制表符(tab)**缩进;可读文本尽量保持80 列以内 |
| Tests | 为新增代码补充有意义的测试 |
| Coverage | 随代码增加,测试覆盖率只许上升 |
| pyflakes | 在包括基于 Python 的动作在内的所有 Python 代码上运行 |
| Documentation | 改动后保持本文档、man 手册页同步更新,新特性要有足够的使用文档 |
| Bugs | 移除 bug,且不要引入新的 bug |
3.2 分支覆盖的例外说明
对于为兼容旧版 Python 而保留的分支,允许在代码中使用# pragma: no cover注释(在 fail2ban/server/action.py 等抽象方法上也能看到这类用法)。但对其他任何pragma: no cover或pragma: no branch的使用,必须写明理由——"我还没写测试"不是充分的理由。
3.3 pyflakes 静态检查
pyflakes 用于发现未使用的 import、未使用/未定义/被重定义的变量。文档建议对以下路径运行:
pyflakes bin/ config/ fail2ban/其中config/包含基于 Python 的 action(如 config/action.d/smtp.py),同样需要检查。
3.4 Git 提交信息标签规范
提交信息中请使用以下标签前缀:
BF:—— bug 修复(Bug Fix)DOC:—— 文档修复ENH:—— 功能增强(Enhancement)TST:—— 仅涉及测试的提交(不触碰主代码库)
多个标签可用+连接,例如BF+TST:。仓库的 ChangeLog 中即大量使用这类前缀。
另外,可用closes #333、resolves #333、fixes #333等文本让提交自动关闭对应 Issue(333 为示例 Issue 号)。如果合并产生了冲突,需要在合并提交信息的Conflicts:段落中说明对相应文件做了哪些修改。
3.5 添加新 Action 的约定
如果新增了action.d/*.conf文件,还必须在 config/jail.conf 中添加一个示例:enabled = false、针对 ssh 且maxretry=5的配置块(config/jail.conf内已有大量此类enabled = false的示例模板,可直接参照编写)。这样系统管理员既能开箱参考,又不会因示例默认启用而误封。
四、服务端设计:核心组件与类层次
DEVELOP 文档指出,Fail2Ban 最初基于 Python 2.3 开发(作者回忆),至今仍力求兼容 Python 2.4,这种兼容性承诺使得部分代码显得"老派"(文档中标记为 RF-Note,即重构时值得关注的点)。0.7 版本经历了重大重构,形成了client/server 分离、每监狱一线程(a-thread-per-jail)的架构。下面用文档给出的类层次图作为导航(符号约定:->继承、+委托/聚合、*存储多个实例):
JailThread -> Filter -> FileFilter -> {FilterPoll, FilterPyinotify, ...} | * FileContainer + FailManager + DateDetector + Jail(构造时传入,用于把 ticket 从 FailManager 送入 Jail 的队列) Server + Jails * Jail + Filter (in __filter) * tickets (in __queue) + Actions (in __action) * Action + BanManager从当前源码看,这一结构依然成立:fail2ban/server/jailthread.py 定义了抽象线程基类JailThread(维护active/idle状态与run/stop/onStop生命周期);fail2ban/server/filter.py 中依次定义了Filter(JailThread)、FileFilter(Filter)、FileContainer;具体后端 filterpoll.py(FilterPoll(FileFilter))、filterpyinotify.py(FilterPyinotify(FileFilter))、filtersystemd.py(FilterSystemd(JournalFilter))都沿此继承体系实现。
4.1 FailManager:失败票证的集中管理
源码 fail2ban/server/failmanager.py 与文档描述完全对应:
FailManager:以ticket(票证)为单位记录失败。所有操作都通过self.__lock = Lock()加锁完成(文档强调"All operations are done via acquiring a lock"),内部用__failList字典按失败标识(fid,通常为 IP)存储FailTicket。关键属性与方法:setMaxRetry/getMaxRetry:最大重试次数(默认3);setMaxTime/getMaxTime:失败统计窗口(默认600秒);addFailure(ticket, count=1, observed=False):累加失败次数、扩展匹配日志(受maxMatches截断),并累加__failTotal;cleanup(time):删除超出maxTime的过期 ticket;toBan(fid=None):遍历失败列表,把达到maxRetry的 ticket 移出列表并返回,若没有达到阈值的 ticket 则抛出FailManagerEmpty。
FailManagerEmpty(Exception):文档说明它由FailManager.toBan在遍历完 ticket 列表后抛出(并附 RF-Note:这个设计未来可以考虑改成生成器)。
4.2 Filter 与 FileFilter:行处理与文件监控
文档对 fail2ban/server/filter.py 的描述:
Filter(JailThread):包装(非线程化的)FailManager并大量代理其方法,提供处理新日志行的全部主要逻辑——哪些 IP 需要忽略等。内部关键成员:.failManager:FailManager实例;.dateDetector:DateDetector实例;.__failRegex/.__ignoreRegex:失败与忽略规则的正则表达式列表;.__findTime:数值型时间窗口,在processLineAndAdd中用于跳过过期行。
FileFilter(Filter):文件感知的 Filter:.__logPath:被跟踪的文件列表,通过addLogPath逐个添加,存储为FileContainer对象;.getFailures:返回True表示成功打开并读取了行(直到读空),返回False表示打开失败或没有匹配该文件名的容器。
FileContainer:文件的适配器,专门处理日志轮转(log rotation)。提供.open、.close、.readline(RF-Note 指出:文件句柄缺失时readline返回"",也许应返回None更合理)以及位置指针.__pos。
在 filter.py 中可以看到实际的票证流转实现(performBan):
while True: try: ticket = self.failManager.toBan(ip) except FailManagerEmpty: break self.jail.putFailTicket(ticket) if ip: break这正是 DEVELOP 文档所描述的"把 ban tickets 从 failManager 送入对应 jail 队列"的通道:toBan()取出达到阈值的票证,jail.putFailTicket(ticket)将其投递到 Jail 的队列中等待 Actions 线程消费。
4.3 具体后端过滤器(filter*.py)
文档说明:filter*.py是针对特定后端的FileFilter实现。派生类应提供run()的实现,通常还需覆写addLogPath、delLogPath方法。所有后端的run()最终都以一种或另一种方式提供如下循环:
try: while True: ticket = self.failManager.toBan() self.jail.putFailTicket(ticket) except FailManagerEmpty: self.failManager.cleanup(MyTime.time())即:持续从 failManager 取"达到封禁阈值"的票证送入 jail;当列表为空(抛出FailManagerEmpty)时,清理过期的失败记录。
- filterpoll.py 的
FilterPoll通过os.stat()比较mtime/ino/size判断日志是否被修改(isModified),主循环run()每轮调用getModified收集被修改的文件并逐个getFailures(filename);对文件缺失(可能由轮转引起)会记录__file404Cnt计数,超过 50 次错误则将该文件移出监控(delLogPath)。 - filterpyinotify.py 的
FilterPyinotify基于 inotify 事件回调(callback、_process_file、_addFileWatcher等)实时感知文件变化。 - filtersystemd.py 的
FilterSystemd则对接 systemd journal(addJournalMatch、seekToTime、formatJournalEntry等)。
4.4 ipdns.py:DNS 与 IP 处理工具
ipdns.py 提供两类工具(文档所述):
DNSUtils:DNS 处理的工具类,包含dnsToIp、ipToName、textToIp(text, useDns)、getSelfIPs、getIPsFromFile等;IPAddr:IP 地址处理的对象类,支持 IPv4/IPv6、CIDR(plen/isInNet/contains)、PTR 反查(getPTR)、地址族判断(isIPv4/isIPv6)等。
4.5 Action 与 Actions:封禁命令的执行
文档对 action.py 的描述:"Takes care about executing start/check/ban/unban/stop commands"(负责执行 start/check/ban/unban/stop 命令)。源码层面:
- 抽象基类定义在 fail2ban/server/action.py,
start/stop/ban/reban/unban均为抽象方法; - fail2ban/server/actions.py 中的
Actions是监狱级动作容器,其run()是动作线程主循环:启动所有 action → 等待 jail 队列中的封禁票证(hasFailTickets)→__checkBan()处理封禁、__checkUnBan()处理到期解封 → 线程停止时__flushBan(stop=True)并stopActions(); - 同一文件中的
ActionInfo定义了动作命令可用的插值标签字典AI_DICT,如<ip>、<family>、<bantime>、<failures>、<matches>、<ipmatches>、<jail.name>、<jail.banned>等——这些就是 config/action.d/ 下各动作配置(如iptables.conf、nftables.conf、mail.conf)中<...>占位符的取值来源; BanManager(fail2ban/server/banmanager.py)负责维护当前封禁列表(addBanTicket、unBanList、getBanList等)。
4.6 其他辅助组件
- server.py:核心服务器,管理 Jails 集合(
addJail/delJail/startJail/stopJail),并暴露大量set/get接口(addFailRegex、setMaxRetry、setBanTime、setBanIP等),由transmitter转发控制命令; - jails.py:
Jails容器,add(name, backend, db=None)创建 Jail; - jail.py:
Jail聚合 Filter(__filter)、失败票证队列(__queue)、Actions(__action),并负责_setBackend选择polling/pyinotify/systemd后端; - failmanager.py 的
FailTicket/BanTicket定义在 ticket.py(getIP、getAttempt、getMatches、incrBanCount、isTimedOut等); - datedetector.py 与 datetemplate.py:日志行日期识别;
- database.py:SQLite 持久化(
addBan/getBans/getCurrentBans/purge),用于重启后恢复封禁状态。
五、开发时的自检清单
综合 DEVELOP 文档与仓库实践,提交代码前建议按以下清单自检:
- 问题描述:PR 是否清晰说明了解决的问题?
- 回归风险:改动是否会给系统管理员升级造成困难?配置默认值是否保持向后兼容?
- 测试:是否补充了覆盖常规、异常与边界条件的测试用例?运行
coverage run bin/fail2ban-testcases && coverage report确认覆盖率没有下降?# pragma: no cover是否都有合理理由? - 静态检查:
pyflakes bin/ config/ fail2ban/是否无新增告警(未使用 import、未定义变量等)? - 文档:DEVELOP、FILTERS 与 man/ 手册是否同步更新?新特性是否有使用文档?
- 提交规范:提交信息是否使用
BF:/DOC:/ENH:/TST:标签?是否需要fixes #N关联 Issue?是否需要更新 ChangeLog 与 THANKS? - 新增动作:若新增
action.d/*.conf,是否在 config/jail.conf 提供了enabled = false、maxretry=5的 ssh 示例? - 手动验证:是否用
./fail2ban-client -c config/ -s /tmp/f2b.sock -i start走通"添加监狱 → 挂载动作 → 手动 banip → 查看状态"的完整链路?
按照上述流程,即可在保证质量的前提下为 Fail2Ban 提交修复与新特性;而理解第四节的类层次与票证流转机制,则是深入调试 Filter 与 Action 行为的关键基础。
【免费下载链接】fail2banDaemon to ban hosts that cause multiple authentication errors项目地址: https://gitcode.com/gh_mirrors/fa/fail2ban
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考