Fail2Ban 的 systemd journal 后端过滤器:FilterSystemd 模块源码级解析
【免费下载链接】fail2banDaemon to ban hosts that cause multiple authentication errors项目地址: https://gitcode.com/gh_mirrors/fa/fail2ban
本指南以仓库 doc/fail2ban.server.filtersystemd.rst 所定义的fail2ban.server.filtersystemd模块为主题,深入解析 Fail2Ban 如何借助 systemd journal 直接读取日志并检测登录失败。你将掌握FilterSystemd类的核心成员与工作流程(journal 匹配、游标续读、轮转重开、持久化断点)、journalmatch的 journalctl 语法用法,以及backend = systemd在 config/jail.conf 中的配置前提与限制。
一、模块定位:从日志文件到 systemd journal
Fail2Ban 的过滤器后端负责"读取日志 → 正则匹配 → 把失败交给 FailManager 计次"。传统polling/pyinotify后端监视的是普通文本文件(logpath),而 systemd 后端直接订阅 journald 的日志流,不再需要指定logpath。
在 fail2ban/server/jail.py 中,后端被注册为_BACKENDS = ['pyinotify', 'polling', 'systemd'],auto会按该顺序尝试初始化;当显式或自动选中systemd时,jail.py 调用_initSystemd实例化FilterSystemd。模块继承自 fail2ban/server/filter.py 中的JournalFilter基类(基类只是定义了addJournalMatch/delJournalMatch/getJournalMatch的空实现与clearAllParams的清理逻辑),真正的实现全部落在filtersystemd.py中。
二、构造与初始化参数
FilterSystemd.__init__(filtersystemd.py)先通过_getJournalArgs(kwargs)从配置参数中提取 systemd 相关的关键字参数,再创建journal.Reader连接:
self.__jrnlargs = FilterSystemd._getJournalArgs(kwargs) JournalFilter.__init__(self, jail, **kwargs) self.__journal = journal.Reader(**self.__jrnlargs)_getJournalArgs(filtersystemd.py)支持的 jail 配置参数如下表:
| 配置参数 | 含义 | 底层处理 |
|---|---|---|
journalpath | 指定 journal 目录路径 | 传入journal.Reader(path=...) |
journalfiles | 指定要监视的 journal 文件(支持通配符) | 经过glob展开、去重后传入files= |
journalflags | 传给journal.Reader的 flags 整数 | 直接int()转换后传入flags= |
rotated | 是否监视已轮转的日志文件 | 布尔值;为假时改用"排除轮转文件"的策略 |
namespace | 命名空间参数(systemd 239+) | 传入journal.Reader(namespace=...) |
需要注意的默认行为:当未显式给出journalflags时,默认 flags 取决于rotated——若rotated为真,默认journal.SYSTEM_ONLY;也可用环境变量F2B_SYSTEMD_DEFAULT_FLAGS覆盖。同时,为避免打开大量用户会话导致 "Too many open files"(对应 gh-2392),当rotated为假且未指定files/namespace时,会调用_globJournalFiles只选取system.journal与user-<uid>.journal,显式剔除*@*.journal这类轮转文件,并把flags、path置空(因为files不能与二者同时指定)。
_globJournalFiles(filtersystemd.py)还依赖_getSystemdPath调用systemd-path system-state-logs/system-runtime-logs获取持久日志与运行时日志目录(结果带缓存),失败时回退到/var/log/journal与/run/log/journal;非 root 运行时还会过滤掉无读权限的文件。
三、journal 匹配:journalmatch 的增删改查
匹配的核心是journalmatch配置项,它使用 journalctl 兼容语法。addJournalMatch(filtersystemd.py)把形如_SYSTEMD_UNIT=sshd.service + _COMM=sshd的列表按+拆分成多组,再由_addJournalMatches依次调用journal.add_match(...)与journal.add_disjunction(),即同一组内是 AND、组与组之间是 OR:
def _addJournalMatches(self, matches): if self.__matches: self.__journal.add_disjunction() # Add OR for match in matches: for match_element in match: self.__journal.add_match(match_element) self.__journal.add_disjunction()resetJournalMatches(filtersystemd.py)在清理失败或重开 journal 时先flush_matches()再重建全部匹配;delJournalMatch(filtersystemd.py)支持按索引删除或一次性清空。若启动时未配置任何journalmatch,run()会打出 notice 日志,提示将针对全部 journal 条目做正则匹配,出于性能考虑不建议这样做(filtersystemd.py)。
从调用链看,这些方法通过 fail2ban/server/server.py 与 fail2ban/server/transmitter.py 暴露为 fail2ban-client 命令set <jail> addjournalmatch、deljournalmatch、get <jail> journalmatch,并有 fail2ban/tests/servertestcase.py 与 fail2ban/tests/clientbeautifiertestcase.py 的测试用例覆盖。
四、时间戳、格式化与断点续读
getJrnEntTime(filtersystemd.py)优先取_SOURCE_REALTIME_TIMESTAMP,缺失时回退到__REALTIME_TIMESTAMP,返回(ISO 字符串, POSIX 时间戳)。
formatJournalEntry(filtersystemd.py)把 journal 条目拼装成 syslog 风格行:_HOSTNAME、SYSLOG_IDENTIFIER(缺省回退_COMM)、SYSLOG_PID(缺省回退_PID,统一格式化为[pid]),kernel 消息追加单调时间戳,MESSAGE中的换行被转义为\n,最后把时间前缀进日志行,保证与普通文件过滤器共用同一套正则处理管线。
断点续读依赖 fail2ban/server/database.py 的getJournalPos/updateJournal:在run()主循环中,若 jail 关联了数据库,startTime取max(数据库记录位置, now - findtime),再seekToTime(startTime)回溯处理;处理过程中每 100 个 tick 或达到更新间隔时调用_updateDBPending落库,afterStop时也会确保待写位置全部更新(filtersystemd.py)。这正是"重启不重扫、不漏报"的实现基础。
五、主循环:等待、轮转感知与重开
run()(filtersystemd.py)是过滤器线程的主干,关键流程为:
- 定位起点:
seek_tail()取最后一条记录,结合数据库位置与findtime决定回溯起点;空 journal 时直接进入"运行中"模式并seekToTime(now)。 - 等待新条目:运行模式下用
Utils.wait_for包住journal.wait(...),以非轮询方式等待APPEND/INVALIDATE信号;空闲时仍按sleeptime节流,避免高负载。 - 处理轮转(INVALIDATE):收到
INVALIDATE先短暂等待轮转结束,必要时前后移动游标避免进入死区(gh-3396),并通过_journalAlive检测连接是否失效(如OSError: [Errno 99] Cannot assign requested address),失效则调用_reopenJournal重建journal.Reader。 - 逐条处理:
get_next()取条目 →formatJournalEntry格式化 →processLineAndAdd(line, tm)交给基类正则匹配与 FailManager;累计 100 条后继续处理而不进入等待。 - 运行模式切换:处理到"当前时刻"或越过起点游标后切换为
inOperationMode,此后新到条目直接实时处理。
_reopenJournal(filtersystemd.py)对应 gh-3929 场景(轮转后 journal 描述符失效),先尝试原地重新__init__底层 Reader,失败则整体重建,随后恢复 jail 配置的 journal 匹配并短暂抑制重复的 Invalidate 提示。
六、配置实战:在 jail 中启用 systemd 后端
修改config/jail.conf或jail.d/*.local,把目标 jail 的backend设为systemd,并通过journalmatch缩小监视范围,例如 sshd:
[sshd] enabled = true backend = systemd journalmatch = _SYSTEMD_UNIT=sshd.service + _COMM=sshd maxretry = 5 findtime = 10m bantime = 10mjournalmatch的优先级从高到低为:jail 内显式配置 → 过滤器配置文件的journalmatch段(如 fail2ban/tests/files/filter.d/testcase01.conf 中的_COMM=sshd + _SYSTEMD_UNIT=sshd.service _UID=0)→ 缺省(匹配全部 journal)。+连接多个匹配组时组间为 OR,同一组内多个键值对为 AND。
根据 config/jail.conf 的说明,使用systemd后端时不能再指定logpath;若默认后端为 systemd、但某 jail 的日志只存在于自己的日志文件中,应给该 jail 单独指定其他后端(如polling)并留空journalmatch。启用前需确认系统已安装systemd的 Python 绑定(模块顶部from systemd import journal依赖它),例如 Debian/Ubuntu 的python3-systemd包;Fail2Ban 源码中FilterSystemd及相关方法标注了# pragma: systemd no cover,意味着在未安装绑定的环境下该后端不可用。
运行验证可执行:
fail2ban-client status sshd # 查看 Filter 行与 Journal matches fail2ban-client set sshd addjournalmatch "_COMM=sshd" fail2ban-client get sshd journalmatch fail2ban-client set sshd deljournalmatch七、小结
FilterSystemd把 Fail2Ban 的"正则判定失败"能力无缝嫁接到 systemd journal 上:journalmatch提供与 journalctl 一致的筛选语法,formatJournalEntry保证条目与文件日志走同一条处理管线,数据库断点实现重启续读,INVALIDATE感知与_reopenJournal应对日志轮转和描述符失效。整套机制在 fail2ban/server/filtersystemd.py 一个文件中即可完整阅读,是理解 Fail2Ban 过滤器抽象(fail2ban/server/filter.py)与后端扩展机制的最佳范例。
【免费下载链接】fail2banDaemon to ban hosts that cause multiple authentication errors项目地址: https://gitcode.com/gh_mirrors/fa/fail2ban
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考