1. 先说清楚:Snort 到底解决什么问题
1.1 一台机器盯着几万条连接是什么体验
上个月帮一个做电商的朋友梳理他那套内网环境,机房里有台跑了两年的旧服务器,上面装着一堆业务脚本,谁都能 SSH 上去。我问他:"你知道这台机器上个月被扫了多少次端口吗?"他愣了三秒说不知道。我给他挂了个 Snort 跑了一晚上,第二天导出来的告警文件有 40 多兆,光 SSH 暴力破解尝试就有两千多条。
这就是Snort 入侵检测系统最朴素的用法:它像一个常年不眨眼的门卫,把流经网卡的每一个数据包拆开看一遍,对照一套规则判断"这个行为像不像攻击",像就记一笔。
很多人第一次听"入侵检测系统"这五个字会觉得离自己很远,以为只有银行、运营商才用得上。实际上只要你有一台对公网开放的服务器,哪怕是台跑个人博客的小机器,每天被扫的次数都不会少。Snort 干的事情有三件:
- 看:抓包、解析协议栈,从链路层一路解到应用层,HTTP、DNS、TLS 握手都能认出来;
- 判:把解析出来的特征和规则库比对,比如某个请求里带了一串特征明显的 SQL 注入片段;
- 记:匹配上了就写告警、存原始包,方便事后回溯。
它解决的痛点是"你根本不知道自己被打了"。绝大多数中小规模的入侵,在造成实际损失之前都会在网络层留下痕迹——异常的端口扫描、畸形的 DNS 请求、带特征的 payload。没人盯着,这些痕迹就随着日志轮转消失了。
这篇文章适合三类人看:一是手上有机房和服务器、想补一层网络侧监控的运维;二是正在准备安全相关认证、需要动手跑一遍 Snort 的学生;三是已经装了 Snort 但被满屏告警搞得头大、准备放弃的人。第三类尤其建议看完第 4 章和第 6 章,绝大多数人的"Snort 不好用"其实是规则没调,不是工具不行。
1.2 Snort 的三种身份:嗅探器、IDS、IPS
刚接触的人经常把这三个模式搞混,配置的时候参数乱填,结果跑起来既抓不到包也不报警。其实区分很简单,关键看 Snort 处在数据通路的哪个位置。
**嗅探模式(Sniffer)**是最基础的,Snort 只是把包抓下来打印或者存 pcap,不做检测。命令长这样:
snort -i eth0 -v这个模式我用得最多的场景是排障——先确认网卡能不能看到流量,再谈检测。
**IDS 模式(被动检测)**是旁路部署。流量本来走的是交换机的镜像口或者分光器,Snort 只收不发,检测到攻击就告警,但拦不住。它的优势是零风险:Snort 自己挂了、磁盘写满了,业务照样跑。缺点也明显,等你看到告警的时候攻击已经打完了。
**IPS 模式(主动阻断)**是串接部署,流量必须穿过 Snort 这台机器。检测到恶意流量直接丢包或者发 RST 重置连接。这是有代价的——Snort 的处理能力就是整个网络的瓶颈,配错了规则可能把正常业务也拦掉。我见过一次事故,有人把一条误报率很高的规则在 IPS 模式下启用了,结果公司内部一个 SaaS 系统的登录接口全挂,排查了半天才定位到是 Snort 在丢包。
我的建议是:新上线一律先跑 IDS,稳定运行两周、把误报清干净之后再考虑切 IPS。这个顺序别颠倒。
1.3 Snort 2 还是 Snort 3,别选错了
现在网上搜到的教程有一大半还是 Snort 2.9 的,配置文件叫snort.conf,格式是老的那套变量加 include。但 Snort 3 已经出来好几年了,架构做了大改,配置换成了 Lua 脚本,配置文件叫snort.lua。
两者的核心差异我列一下,你按自己的情况选:
| 对比项 | Snort 2.9 | Snort 3.x |
|---|---|---|
| 配置文件 | snort.conf(类 C 宏语法) | snort.lua(Lua 脚本) |
| 多线程 | 单线程为主 | 原生多线程,多核利用率高 |
| 规则兼容 | 老规则集全兼容 | 大部分兼容,部分关键字改写 |
| 预处理插件 | preprocessor 体系 | 内置模块,配置更集中 |
| 生态资料 | 海量中文教程 | 相对少,官方文档为主 |
| 性能 | 单核跑满就瓶颈 | 同样硬件吞吐普遍更高 |
如果你是新部署,直接上 Snort 3。理由很实在:现在的服务器动辄十几核,Snort 2 只能吃满一个核,剩下全闲着,浪费得心疼。Snort 3 的多线程设计能把流量按流分到多个线程上跑,同样一台机器吞吐能翻好几倍。
如果你是维护一套老系统,周围一堆脚本都依赖snort.conf和 unified2 日志格式,那暂时别动。迁移成本主要在规则改写和日志解析脚本上,不值当为了新而新。
我下面讲的实操以 Snort 3 为主,遇到 Snort 2 差异明显的地方会单独标出来。
2. 部署前的准备工作:位置选错,后面全白干
2.1 串接还是旁路,先看你能接受多少风险
这一步是很多人跳过、后面返工最多的地方。我先给结论:除非你有明确的一键阻断需求,否则优先旁路。
旁路的实现方式有两种。一种是交换机镜像口(SPAN),把某个端口或者某个 VLAN 的流量复制一份给 Snort 所在的网卡。这种方式配置简单,在交换机上敲几条命令就行,但要注意镜像口的带宽——如果你镜像的是整个核心交换机的上下行,总和可能超过镜像口本身的线速,会丢包。另一种是分光器,物理层复制光信号,不丢包但要多花钱买硬件。
串接就是把 Snort 部署成网桥或者网关,流量必须过它。这时候最怕的就是 Snort 自身故障导致断网。所以如果你的场景是串接,一定要配 bypass 网卡或者至少准备一套心跳脚本,Snort 进程挂了自动把流量绕过去。
我给一个判断标准:业务中断一分钟的损失,如果大于被入侵一次的期望损失,就走旁路;反过来才考虑串接。大部分中小公司的业务连续性比安全事件更敏感,所以旁路是更稳的选择。
还有一个折中方案,很多人不知道:IDS 检测 + 外部联动封禁,也就是 Snort 只负责发现,发现了之后调防火墙接口把这个源 IP 拉黑。这样既保留了旁路的零风险,又实现了准实时的阻断,延迟大概是几秒到几十秒。这个方案我后面在第 5 章会详细讲。
2.2 硬件与网卡的坑
Snort 对硬件的要求其实不高,但对网卡很挑剔。
CPU 方面,Snort 3 多线程模式下核数比主频更重要。我实测过,一台 8 核 16G 的机器,处理 1Gbps 左右的混合流量(大约 20 万 pps),CPU 占用在 40% 到 60% 之间浮动,余量够用。如果是 10G 环境,那得上服务器级的网卡加多队列,普通的千兆网卡根本吃不下。
网卡是重灾区。市面上大量便宜的 USB 网卡或者消费级板载网卡,驱动不支持混杂模式,或者支持得很勉强。你在ifconfig里看到PROMISC标志亮着,实际抓包还是只能看到自己这台机器的流量。判断方法很简单:
# 查看网卡是否进入混杂模式 ip link show eth0 | grep -i promisc # 直接抓包看是否有其他主机的流量 tcpdump -i eth0 -c 20 -n net 192.168.10.0/24如果只看到本机 IP 的包,那多半是镜像没配好或者网卡不支持。这种情况在虚拟化环境里尤其常见——VMware 的 vSwitch、KVM 的 bridge,默认都不会把别人的流量给你。
注意:在虚拟化环境里跑 Snort 做旁路检测,必须把虚拟交换机的端口镜像或者混杂模式打开,否则你跑一整天都是零告警,还以为是规则没生效。
2.3 系统参数与内核准备
Linux 默认的一些参数对高频抓包不友好,上线前建议调一下。这些改动都很小,但能明显减少丢包。
第一是网卡缓冲区。默认的环形缓冲区(ring buffer)太小,流量突发的时候直接丢包:
# 查看当前值 ethtool -g eth0 # 调大到 4096(具体上限看网卡支持) ethtool -G eth0 rx 4096 tx 4096第二是内核的 backlog 队列。这个值在流量高峰时特别关键:
sysctl -w net.core.netdev_max_backlog=30000 sysctl -w net.core.rmem_max=134217728 sysctl -w net.core.rmem_default=134217728第三是关闭网卡的一些 offload 特性。这点反直觉——offload 本来是提性能的,但在抓包场景下会导致抓到的是"半成品"包,比如校验和没算、大包没分片,Snort 解析的时候就会报错或者漏检:
ethtool -K eth0 gro off lro off tso off gso off rx off tx off这些设置重启会丢,建议写进 systemd 的 service 文件或者/etc/rc.local。
实操心得:我曾经在一台机器上遇到 Snort 频繁报警"checksum error",查了半天以为是硬件问题,最后发现就是 GRO/LRO 没关。关掉之后告警立刻消失,包也全部正常解析了。所以这步别省。
3. 编译安装与最小可用配置
3.1 依赖清单与编译参数选择
Snort 3 用 CMake 构建,跟 Snort 2 的 autotools 不一样。我先给一份在 Ubuntu 22.04 上实测可用的依赖安装命令:
apt update apt install -y build-essential cmake libpcap-dev libpcre2-dev \ libdumbnet-dev bison flex zlib1g-dev liblzma-dev openssl \ libssl-dev libnghttp2-dev libluajit-5.1-dev libhwloc-dev \ uuid-dev libfl-dev这里面几个依赖值得说一下为什么需要:
libpcre2-dev:规则里的正则匹配全靠它,没有它规则里的pcre:关键字用不了;libluajit-5.1-dev:Snort 3 的配置和部分插件是 Lua 写的,这个是运行时;libhwloc-dev:多线程绑定核用的,想让 Snort 把线程均匀铺到各个物理核上就靠它;libdaq:数据采集抽象层,Snort 通过它对接不同的抓包后端。
然后是编译。Snort 3 的源码包里带了一个configure_cmake.sh脚本,比手敲 cmake 省事:
./configure_cmake.sh --prefix=/usr/local/snort \ --enable-tcmalloc \ --disable-static-daq cd build make -j$(nproc) sudo make install--enable-tcmalloc是我强烈建议加的。Snort 在高流量下会频繁申请释放小块内存,glibc 默认的分配器在长时间运行后内存碎片会很明显,RSS 一路涨。换成 tcmalloc 之后我观察到常驻内存稳定得多,跑两周都看不出明显增长。前提是先装libtcmalloc-minimal4和libgoogle-perftools-dev。
编译完之后把库路径加到系统里,不然运行的时候会报找不到.so:
echo '/usr/local/snort/lib' > /etc/ld.so.conf.d/snort.conf ldconfig3.2 snort.lua 逐段拆解
Snort 3 的配置核心就是snort.lua。官方给的默认文件很长,几百行注释,第一次看容易懵。我把它精简成一份够用的骨架,逐段讲。
第一段是网络变量,这个决定了规则里$HOME_NET到底指谁:
HOME_NET = '192.168.10.0/24' EXTERNAL_NET = '!$HOME_NET'很多人在这里犯错:把HOME_NET写成any。后果是所有"从外到内"的规则全都失效,因为内外不分了。必须精确写出你实际要保护的网段。
第二段是 DAQ 采集配置,这个决定了怎么抓包:
daq = { module = 'afpacket', snaplen = 1518, modules = { { name = 'afpacket', mode = 'passive', variables = { 'fanout_type=hash' } } } }snaplen设成 1518 是有讲究的。默认值往往给到 65535,意思是每个包最多截取 64K。实际以太网帧最大也就 1518 字节(不算巨帧),设这么大纯属浪费内存。我见过有人因为 snaplen 设太大,跑起来内存占了几十 G,改成 1518 之后降到几个 G。
fanout_type=hash是多队列分流策略,让同一个流的所有包落到同一个线程上,避免乱序导致的状态跟踪错乱。
第三段是检测模块,这个决定了 Snort 检查哪些东西:
stream = { } stream_tcp = { } stream_udp = { } stream_icmp = { } frag3 = { } normalizer = { } http_inspect = { } dns = { } ssl = { }每个模块干的事情不一样。stream_*负责流重组和状态跟踪,frag3处理 IP 分片,normalizer处理各种协议畸形(比如重复的 TCP 选项),http_inspect把 HTTP 请求拆成方法、URI、头部、正文几个字段供规则匹配。只要有一条规则用到了某个协议字段,对应的模块就必须开,否则规则永远不匹配。这个坑我踩过——写了条匹配 HTTP URI 的规则,死活不报警,最后发现是http_inspect没启用。
第四段是输出:
outputs = { alert_fast = { file = true, packet = false, limit = 10 }, alert_json = { file = true, limit = 10 }, log_pcap = { limit = 10 } }alert_fast是人看的,一行一条告警,紧凑好 grep;alert_json是给日志系统吃的;log_pcap存原始包,出事之后能翻出来复盘。limit是单个告警文件的大小上限(MB),超过就轮转。
3.3 验证:跑通第一条告警
配置写完先别急着上线,用测试模式检查语法:
snort -c /usr/local/snort/etc/snort/snort.lua -T返回Snort successfully validated the configuration!才算过。这一步能拦掉 80% 的低级错误,比如模块名拼错、变量没定义。
然后写一条最简单的本地规则,放在local.rules里:
alert icmp any any -> $HOME_NET any (msg:"ICMP Ping Detected"; sid:1000001; rev:1;)在snort.lua里加载它:
ips = { enable_builtin_rules = true, include = '/usr/local/snort/etc/snort/rules/local.rules', variables = { nets = { HOME_NET = HOME_NET, EXTERNAL_NET = EXTERNAL_NET } } }前台跑一下,边跑边 ping 目标网段的机器:
snort -c /usr/local/snort/etc/snort/snort.lua -i eth0 -A alert_fast -k none控制台应该会刷出ICMP Ping Detected。看到这条就算打通了。-k none是跳过校验和检查,在虚拟化环境里特别有用,因为很多虚拟网卡不做校验和卸载,包过来校验和是错的,不加这个参数 Snort 会把包全丢了。
4. 规则体系:Snort 的灵魂在这里
4.1 一条规则的七段式结构
规则是 Snort 的全部。默认规则集装完能跑,但真正让 Snort 在你环境里发挥作用,靠的是自己写的规则。所以必须先看懂规则的语法。
一条完整规则长这样:
alert tcp $EXTERNAL_NET any -> $HOME_NET 22 \ (msg:"SSH Brute Force Attempt"; \ flow:to_server,established; \ content:"SSH-2.0"; \ detection_filter:track by_src, count 8, seconds 60; \ classtype:attempted-admin; \ sid:1000010; rev:1;)拆开看有七部分:
- 动作:
alert、log、pass、drop、reject。drop和reject只在 IPS 模式生效,IDS 模式会自动降级成 alert。pass是白名单,优先级最高,用来给已知误报打补丁。 - 协议:tcp、udp、icmp、ip(任意 IP 协议)。
- 源地址与端口:支持单个 IP、CIDR、变量、
!取反。 - 方向:
->单向,<>双向。写单向能减少一半的匹配次数,性能上有意义。 - 目的地址与端口。
- 规则选项:括号里那一长串,检测逻辑全在这儿。
- 通用标识:
sid是唯一编号,自己写的从 1000000 起;rev是修订号,改一次加一。
几个选项的细节值得单独说。flow:to_server,established表示只匹配客户端到服务端、且已经建立连接的流。为什么加这个?因为 TCP 三次握手期间、或者连接重置之后的残包,往往没有实际 payload,匹配它们纯属浪费 CPU。我实测过,给一条 SSH 相关规则加上flow约束之后,匹配次数从每分钟几百降到个位数,误报基本清零。
content是最常用的匹配项,支持二进制和文本。注意它默认是大小写敏感的,而且不区分位置。如果是匹配 HTTP 路径,最好配合http_uri修饰符限定位置:
content:"/admin"; http_uri;这样只在 URI 字段里找,不会因为正文里恰好出现/admin就误报。
detection_filter是频次限制,这个在写"暴力破解"类规则时几乎是必备的。它的意思是"同样的源 IP,60 秒内触发 8 次以上才告警"。注意在 Snort 3 里这个关键字改名叫event_filter,语法略有不同,迁移的时候要改。
4.2 规则集怎么管,别用 cp 覆盖
线上环境规则集管理是个容易被忽略的坑。我见过有人直接wget新规则包然后cp -r覆盖整个 rules 目录,结果自己辛苦调的本地规则全没了,白名单也没了,告警瞬间爆炸。
正确的做法是分目录管理,我一般这样分层:
/etc/snort/rules/ ├── local.rules # 自己写的规则,永远不动 ├── whitelist.rules # 白名单 pass 规则 ├── community/ # 社区规则,可以整目录替换 ├── registered/ # 订阅规则 └── disabled.conf # 禁用清单local.rules和whitelist.rules是你自己的东西,任何自动化脚本都不应该碰。社区规则整目录替换没关系。
禁用规则我用一个清单文件管理,每行一个 sid,更新之后跑个脚本自动把清单里的规则注释掉:
#!/bin/bash # disable_rules.sh 从 disabled.conf 读取 sid 并注释掉对应规则 DISABLED=/etc/snort/rules/disabled.conf RULES_DIR=/etc/snort/rules while read -r sid; do [ -z "$sid" ] && continue grep -rl "sid:$sid;" "$RULES_DIR" | while read -r f; do sed -i "s|^\(alert.*sid:$sid;.*\)|# \1|" "$f" done done < "$DISABLED"为什么要有禁用清单?因为总有些规则在你的环境里就是纯噪音。比如你的业务在跑一个内部爬虫,天天触发web-cgi类的目录遍历告警,那不如把这几条关掉,让告警列表干净下来。告警信噪比才是决定 Snort 能不能长期跑下去的关键,不是规则数量。
4.3 手写三条自己的规则
光用现成规则不够,每个环境都有自己关心的东西。我给你三条实测有用的模板,改改就能用。
第一条,检测针对内部管理端口的扫描:
alert tcp $EXTERNAL_NET any -> $HOME_NET 22 \ (msg:"Port Scan to SSH"; \ flags:S; \ threshold:type limit, track by_src, count 1, seconds 5; \ classtype:attempted-recon; \ sid:1000020; rev:1;)flags:S只匹配 SYN 标志的包,也就是连接请求的第一个包。配合threshold做限流,同一个源 5 秒内最多报一次,避免告警刷屏。
第二条,检测恶意 User-Agent:
alert http $EXTERNAL_NET any -> $HOME_NET any \ (msg:"Suspicious User-Agent - sqlmap"; \ flow:established,to_server; \ http_header; content:"User-Agent|3a 20|sqlmap"; nocase; \ classtype:web-application-attack; \ sid:1000021; rev:1;)|3a 20|是十六进制的冒号加空格,规则里可以直接写二进制。nocase让它忽略大小写,因为扫描工具经常随机化大小写来绕过检测。
第三条,检测异常 DNS 查询长度,这类特征常见于数据外传:
alert udp $HOME_NET any -> any 53 \ (msg:"Possible DNS Tunneling - Long Query"; \ content:"|01 00|"; offset:2; depth:2; \ byte_test:1,>,50,12,relative; \ classtype:bad-unknown; \ sid:1000022; rev:1;)byte_test是 Snort 里非常好用但很少有人写的东西,它能直接对 payload 里的某个字节做数值比较。上面这条的意思是在 DNS 头部第 12 字节的位置取一个字节,如果大于 50 就告警——正常域名查询很少会这么长。
4.4 用 BPF 和阈值把噪音压下去
写规则之前,先想清楚哪些流量根本不用看。BPF 过滤器在 DAQ 层就生效,不需要的包连 Snort 的解析流程都进不去,性能收益比写规则大得多。
举个例子,你只关心进出的业务流量,不关心监控系统的心跳:
snort -c snort.lua -i eth0 \ --bpf "not host 192.168.10.200 and not port 9100"--bpf后面跟的是标准 tcpdump 语法,你可以用tcpdump -d先验证语法对不对。
另一个压制噪音的手段是event_filter,比逐条写detection_filter更省事,可以在全局层面统一配置:
event_filter = { { type = 'limit', track = 'by_src', count = 10, seconds = 60 }, { type = 'both', track = 'by_dst', count = 100, seconds = 60 } }这配置的意思是:每个源 IP 每分钟最多 10 条告警,每个目的 IP 每分钟最多 100 条。前者防单点刷屏,后者防大面积扫描把告警文件写爆。
提醒一句:阈值是双刃剑。设得太松,真实攻击被淹在噪音里看不见;设得太紧,连续攻击只报了第一条,后面的全被吞了。我的做法是先用宽松阈值跑一周,把 top 10 高频告警拉出来,针对性地调,而不是一上来就全局限流。
5. 告警落地:从日志到处置闭环
5.1 输出插件怎么选
告警写出来只是第一步,关键是怎么让它在合适的时间出现在合适的人面前。Snort 3 内置了几种输出,适用场景不一样。
alert_fast是最简单的纯文本,一行一条,格式是"时间戳 + 动作 + 协议 + 源目 + 规则信息"。它的优点是 grep 友好,缺点是没有结构化字段,程序解析起来麻烦。
alert_json输出 JSON,字段齐全,包括五元组、规则 sid、优先级等。接日志系统首选这个。缺点是文件体积大,大概是 alert_fast 的三到五倍。
alert_syslog直接往 syslog 写,适合已经有 rsyslog 或者 journald 集中收集的环境。好处是不用额外装 agent,坏处是日志量大时 syslog 可能丢消息。
log_pcap存原始包,这个我认为是必开的。理由是:告警文本只能告诉你"匹配了哪条规则",但事后复盘你真正想知道的是"这个包里到底装了什么"。没有原始包,排查就是猜。
我用的是组合拳:alert_json进日志系统做检索和看板,log_pcap留本地做取证,alert_fast只在调试阶段开。配置上可以给不同插件设不同的limit,比如 pcap 给到 200MB,json 给到 50MB,避免磁盘被单一类型吃满。
5.2 接进 ELK 看板
日志系统这块,我用的是 Filebeat + Elasticsearch + Kibana 这套,部署简单,社区文档多。
Filebeat 配置大概是这样:
filebeat.inputs: - type: filestream paths: - /var/log/snort/alert_json.txt json.keys_under_root: true json.overwrite_keys: true fields: source_type: snort fields_under_root: true output.elasticsearch: hosts: ["http://192.168.10.50:9200"] index: "snort-alert-%{+yyyy.MM.dd}"json.keys_under_root: true这行很关键,它把 Snort 输出 JSON 里的字段直接提升到文档顶层,而不是塞在一个message字段里。不设这个,后面做聚合分析的时候每次都要写message.sid这样的路径,很烦。
Kibana 上我建了三个视图:
- 实时告警流:按时间倒序,突出显示优先级 1 的告警,值班的人盯这个;
- TOP 攻击源:按源 IP 聚合,一小时或一天一个窗口,看谁在盯着你;
- 规则命中分布:按 sid 聚合,用来找哪些规则是噪音制造机。
第三个视图是调优的核心工具。我通常会每周看一次,如果某条规则一周内贡献了 80% 的告警量但点进去看全是误报,那这条规则就该禁掉或者加阈值。
5.3 自动化封禁与联动
前面说的"IDS + 联动封禁"方案,具体怎么落地?核心是一个脚本消费 Snort 的告警,判断之后调防火墙接口。
思路是这样:
import json, time, subprocess, collections # 记录每个源 IP 近期的告警次数 counter = collections.defaultdict(list) WINDOW = 300 # 5 分钟窗口 THRESHOLD = 20 # 超过 20 条就封 BLOCK_TIME = 3600 # 封 1 小时 def block_ip(ip): # 用 nftables 加到黑名单集合里 subprocess.run([ 'nft', 'add', 'element', 'inet', 'filter', 'blacklist', '{', ip, 'timeout', f'{BLOCK_TIME}s', '}' ], check=False) def tail_alerts(path): with open(path, 'r') as f: f.seek(0, 2) # 从文件末尾开始 while True: line = f.readline() if not line: time.sleep(0.5) continue try: rec = json.loads(line) except json.JSONDecodeError: continue ip = rec.get('src_addr') if not ip: continue now = time.time() counter[ip] = [t for t in counter[ip] if now - t < WINDOW] counter[ip].append(now) if len(counter[ip]) >= THRESHOLD: block_ip(ip) counter[ip].clear()配套的 nftables 规则:
nft add table inet filter nft add set inet filter blacklist '{ type ipv4_addr; flags timeout; }' nft add chain inet filter input '{ type filter hook input priority -10; policy accept; }' nft add rule inet filter input ip saddr @blacklist drop几个细节要注意。第一,白名单必须先过一遍,把公司出口 IP、监控系统、合作方的固定 IP 排除掉,不然一封就是业务中断。第二,封禁时间不要太长,我用 1 小时,因为很多攻击源是动态 IP,封太久意义不大,反而容易误伤。第三,这个脚本本身要有保护机制,比如统计异常升高时先告警不封禁,人工确认后再动。
实操心得:这套联动我上线第一周就误封了一次。原因是一个内部压测工具在短时间内发了几万次请求,触发了扫描类规则。后来我加了一条判断:如果源 IP 属于内部网段,只告警不封禁。这个改动之后就没再出过事故。
6. 踩坑实录与排查速查表
6.1 抓不到包的四层排查法
"Snort 跑起来了但没告警"是最常见的问题,我总结了一套从上到下的排查顺序,按这个走基本能定位。
第一层,网卡有没有流量。直接上 tcpdump:
tcpdump -i eth0 -c 100 -nn如果 tcpdump 都抓不到,那问题不在 Snort,在镜像配置或者网卡混杂模式。回到 2.2 节检查。
第二层,Snort 有没有读到包。看运行时的统计信息,Snort 3 里按Ctrl+C退出会打印一段汇总,重点看这一行:
daq.statistics: received: 1234567 analyzed: 1234000 dropped: 567received是 0 说明 DAQ 就没抓到包;analyzed远小于received说明内核缓冲区溢出,要回去调 2.3 节的参数。
第三层,规则有没有加载。启动时加-v参数,看输出里有没有Loading rules相关的行,以及加载了多少条。如果只有几条,说明规则路径写错了。
snort -c snort.lua -i eth0 -v 2>&1 | grep -i "rule"第四层,模块有没有开。如果规则加载了但就是不匹配,八成是依赖的检测模块没启用。比如规则里用了http_uri,但http_inspect没开,那这条规则永远哑火。检查方法是在snort.lua里把-T测试模式跑一遍,Snort 会提示哪些规则用了未启用的模块。
6.2 告警洪水与漏报的取舍
这是个绕不开的矛盾。放宽规则能少漏报,但噪音大;收紧规则告警干净,但可能错过真实攻击。
我的经验是要分层处理,而不是一刀切。
第一层,必报不误报:优先级 1 的规则,比如明确的 webshell 特征、已知 C2 域名,这些无论多吵都要报,宁可误报不可漏报。这类规则我不加任何阈值。
第二层,限流上报:扫描探测类的,加阈值限制频次,比如每个源 IP 每 5 分钟一次。这类行为本身不代表入侵,但突然增多值得关注。
第三层,白名单排除:已知的内部系统、监控工具、合作方 IP,直接写pass规则跳过。这是减少噪音最有效的手段。
-- 白名单示例,放在规则文件最前面 pass tcp 192.168.10.200 any -> any any (msg:"Whitelist - Monitoring"; sid:9000001; rev:1;) pass tcp 192.168.10.0/24 any -> 192.168.20.0/24 any (msg:"Whitelist - Internal"; sid:9000002; rev:1;)sid用 9000000 段,跟业务规则区分开,方便管理。
6.3 性能瓶颈定位
Snort 变慢通常有三种表现:CPU 单核跑满、大量丢包、告警延迟。
CPU 单核跑满:检查线程配置。Snort 3 默认可能只起一个检测线程。用--max-packet-len和线程相关参数调整,或者在配置里显式指定线程数:
tweaks = { max_packet_len = 1518, event_trace = 0, process_all_events = 1, max_rules = 0 } thread_config = { { thread = 0, count = 1 }, { thread = 1, count = 1 }, { thread = 2, count = 1 }, { thread = 3, count = 1 } }大量丢包:看dropped计数。如果是内核层丢,回到 2.3 节调netdev_max_backlog和 ring buffer;如果是 Snort 内部丢,说明规则太多太复杂,需要精简。我一般会把规则总数控制在 15000 条以内,超过这个量级就要考虑拆机器或者换成 Suricata 这类更偏性能设计的引擎。
告警延迟:这个往往不是 Snort 的问题,是磁盘 I/O 跟不上。log_pcap写 pcap 文件是 I/O 密集操作,普通机械盘在高流量下很容易成为瓶颈。换成 SSD 之后我实测延迟从几十毫秒降到几毫秒。
6.4 常见问题速查表
把这些年遇到的问题整理成表,按现象查原因,比翻日志快。
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 完全没有告警 | 网卡未进混杂模式 / 镜像未配 | tcpdump 验证,检查交换机 SPAN |
| 只有本机流量 | 虚拟交换机未开混杂 | 打开 vSwitch 或 bridge 的 promisc |
| 包解析报 checksum error | GRO/LRO 未关 | ethtool -K eth0 gro off lro off |
| 规则不匹配但已加载 | 依赖模块未启用 | 检查 http_inspect/dns 等模块 |
| 内存持续增长 | 分配器碎片 | 编译时加--enable-tcmalloc |
| 告警文件被写爆 | 阈值未设或过松 | 配置 event_filter 限流 |
| 高流量下大量丢包 | ring buffer / backlog 太小 | 调 ethtool -G 和 sysctl 参数 |
| 状态跟踪报乱序 | 多队列分流不均匀 | fanout_type=hash |
| 重启后配置全丢 | 网卡参数未持久化 | 写进 systemd unit 或 rc.local |
| 更新规则后本地规则消失 | 整目录覆盖 | 分目录管理,本地规则独立 |
最后分享一个我自己用了很久的小技巧:上线前先用历史 pcap 回放测试,而不是直接接生产流量。通过--pcap-dir或者 tcpreplay 把过去一周的流量喂给 Snort,看看会出多少告警、误报率大概多少。这一步能把大部分调优工作提前到上线之前,避免上线第一周被满屏告警冲垮。回放跑顺了再切真实流量,心里就有底了。