一、先从实际场景说起

很多朋友部署Snort的时候,都是找一台服务器,把网卡抓包打开,然后让它安安静静地跑在机房里。平时没人看它,只有出事了才跑过去翻日志。这种用法在前几年还行,但现在的网络环境变化太快,要是这台机器突然断电、网卡掉了、进程崩了,整个监控就会哑火。更尴尬的是,攻击者可能就在你掉线的这几分钟里溜进了内网,等你知道的时候,黄花菜都凉了。

所以,我们要给Snort找一个“替身”,平时主机的活由它干,替身待命。一旦主机出问题,替身马上顶上,这就是高可用最直白的样子。生活里你可以把它想成两个门卫换班,一个请假了另一个立刻上岗,不能让大门开着没人管。

1.1 单机Snort的尴尬

单机部署的Snort,就像只有一个哨兵的岗亭。哨兵累了、病了、被调走了,岗亭就空了。具体到技术层面,问题集中在几个地方:第一,硬件故障不可控,电源烧了、内存坏了,说挂就挂;第二,软件进程可能因为bug或者资源耗尽而退出;第三,网络链路可能被误操作或者攻击切断。任一个发生,Snort都等于不存在。

1.2 高可用到底解决什么问题

高可用不是让故障不发生,而是让故障发生后,业务还能继续。对于Snort来说,核心诉求就是:主节点挂了,备份节点能在几秒内接管流量,继续报日志。因为绝大多数安全事件不会只在某一秒发生,断档几秒钟可以接受,但断档几小时绝对不行。另外,高可用也给运维人员留出了修复时间,不用半夜爬起来换机器,因为备用节点已经在干活了,你可以从容地把坏机器修好再放回去。

这里要说明的是,Snort作为入侵检测系统,一般不是直接串联在流量路径上的,而是旁路监听。所以高可用和那种负载均衡的集群不太一样,我们不需要把流量分担到多台机器,只需要保证始终有一台机器在监听。这就让架构可以做得简洁,不需要复杂的分布式一致性协议。

二、高可用架构的设计思路

2.1 主备模式是最实在的起点

主备模式,也叫Active-Standby。一台主节点处理所有抓包分析,另一台备用节点不工作,只等着。主节点挂了,备用节点自动升级成主节点。实现这个切换最常用的工具是Keepalived,它通过VRRP协议来完成谁来做主的选举。

Keepalived这个名字听起来很Linux,但它的逻辑很朴素:几个人开会,选一个老大,老大定期发心跳,别人发现老大不说话了,就重新选。这种感觉跟几个朋友约饭,发起人临时来不了,另一个就主动说我来组织是一样的。对于Snort这种应用,主备模式已经能覆盖绝大多数故障场景,而且配置简单,容易排错。

2.2 虚拟IP和VRRP这对搭档

在Keepalived的方案里,最核心的概念是虚拟IP(VIP)。虚拟IP不属于任何一张网卡,它就像是一个团队的对外电话号码。平时这个号码指向主角色的机器,切换后指向新的主节点。外部设备访问这个虚拟IP时,根本感觉不到背后换人了。

VRRP(虚拟路由冗余协议)则是维持这个虚拟IP归属的规则。它让多台机器组成一个虚拟路由组,每台机器有一个优先级。优先级最高的就是主,默认情况下其他机器不响应。主节点周期性发送通告,告诉别人自己还活着。如果一段时间收不到通告,其他备用节点就开始抢活,优先级别高的先上位。这里描述的是一个简化过程,实际上VRRP还分主备、抢占和非抢占等模式,但核心思想就是比大小、抢角色。

为了让你更直观地理解VRRP的工作过程,我们可以在主节点上抓一下VRRP包。执行下面的命令,你会看到目的地址是224.0.0.18,协议号是112的报文,就是VRRP通告。不过这个操作需要root权限,而且要在集群通信网卡上抓。

# 在snort-a的eth1网卡上抓VRRP报文,看10个包就停
tcpdump -i eth1 vrrp -c 10

2.3 虚拟IP对Snort的价值

你可能要问,Snort是旁路监听,要虚拟IP有什么用?其实虚拟IP主要给两类东西用:一类是给管理端访问,比如查看告警或者拉取日志;另一类作为健康检查的锚点,让备用节点知道现在该不该接管。更关键的是,有些旁路部署需要把监听网卡的流量镜像到某个IP,用虚拟IP能让交换机配置更稳定,不用跟着主备节点改来改去。另外,虚拟IP还可以用来做告警消息中的源地址,安全分析师看到的是一个固定的IP,而不是今天这个明天那个,这对追溯和分析很有帮助。

2.4 切换时Snort状态怎么办

Snort自己是有状态的,比如流量分片重组、TCP会话跟踪,这些状态都保存在内存里。如果主节点突然死了,备用节点拿不到这些状态,重新开始分析时,就会漏掉一部分上下文。这就像你看电影看到一半,突然换台,后面的剧情你只能靠猜。

不过,实际使用中,大部分安全监控对状态同步的要求没那么苛刻。因为Snort的规则主要看包的特征和简单协议解析,丢几个状态不会导致灾难。如果你确实需要会话级监控,可以考虑让主备节点共用同一个存储位置,比如把Snort的日志写入共享文件系统,通过rsync或者NFS实时同步。但这类方案会增加复杂度,而且NFS本身也可能成为单点,所以一般中小环境建议先把故障切换做到位,再考虑状态同步。

2.5 典型应用场景

这种高可用设计最典型的场景有几种。第一种是中小企业出口网络,没有专职安全团队,设备挂了可能很久没人发现,用高可用可以自动顶上。第二种是对合规有要求的网络,监管要求安全设备必须具备冗余能力,哪怕平时备用节点不干活,也必须在那里。第三种是流量峰值不高的旁路监控,两台普通服务器就能搞定,犯不上用昂贵的安全设备。如果你的环境具备这些特征,那下面的实践内容就非常对路。

三、动手搭建一套最小环境

技术栈:Shell + Keepalived

下面我们搭一个双节点Snort高可用环境。整体拓扑很简单:两台机器,分别叫snort-a和snort-b,都装Snort和Keepalived。为了让演示直观,我们用两个网卡,一个负责监听流量(eth0),一个负责集群通信(eth1)。虚拟IP配置在eth1上。

3.1 需要准备什么

操作之前,你需要准备两台Linux服务器,建议是root权限。这里用Ubuntu 20.04来演示,但命令在CentOS上稍微改一下包管理也能用。每台机器至少两颗CPU、2GB内存,抓包网卡支持混杂模式。同时,两台机器之间要能互通,最好是同一个二层网络,这样VRRP广播才能正常到达。如果中间有防火墙,记得放行协议号112,也就是VRRP。

3.2 安装基础软件

首先,在每台机器上安装Snort和Keepalived。这里用apt来安装,简单省事。不过apt源里的Snort版本可能不是最新,但功能足够演示。安装命令如下:

# 先更新软件源
apt update

# 安装Snort
apt install -y snort

# 安装Keepalived
apt install -y keepalived

安装完成后,Snort的配置文件默认在 /etc/snort/snort.conf,Keepalived的配置文件在 /etc/keepalived/keepalived.conf。如果你用的是纯抓包模式,还需要确认Snort能识别你的监听网卡。这一步不是必须的,但建议检查一下。

3.3 编写Keepalived主备配置

现在写主节点snort-a的Keepalived配置。关键点是设置状态为MASTER,优先级为120,并且声明一个虚拟IP。另一个配置文件里,备用节点设置为BACKUP,优先级低一点,为100。为了清晰,这里给出主节点的完整配置:

# /etc/keepalived/keepalived.conf 主节点配置
global_defs {
    # 全局配置段,可以写一些通知配置
    router_id snort_a_id
}

vrrp_instance snort_ha {
    state MASTER            # 初始角色是主
    interface eth1          # 集群通信网卡
    virtual_router_id 50    # 虚拟路由ID,同一个HA组里必须一致
    priority 120            # 优先级,越大越容易成为主
    advert_int 1            # 心跳通告间隔,单位秒

    authentication {
        auth_type PASS      # 简单密码认证
        auth_pass mysecret  # 密码,所有节点保持一致
    }

    virtual_ipaddress {
        192.168.10.100/24 dev eth1 label eth1:vip
    }

    # 状态切换时触发的外部脚本
    notify_master "/etc/keepalived/notify.sh master"
    notify_backup "/etc/keepalived/notify.sh backup"
    notify_fault  "/etc/keepalived/notify.sh fault"
}

备用节点snort-b的配置几乎一样,只需要把state改成BACKUP,priority改成100,router_id改为snort_b_id。这里不再重复写一遍,你直接在文本上改这几个字段就行。重点提醒:virtual_router_id和auth_pass一定要和主节点完全一致,否则两个节点互相不认账,谁都觉得自己能做主,结果就是脑裂。

3.4 编写健康检查和切换脚本

Keepalived只能判断机器本身是否存活,不能判断Snort进程是否正常。比如Snort进程崩了,但系统还在,Keepalived不会自动切换。所以我们要写一个健康检查脚本,定时检测Snort进程,如果发现进程不在,就让Keepalived降低自己的优先级,逼着备用节点接管。

先写检查脚本:

#!/bin/bash
# 检查Snort进程是否存在,存在返回0,不存在返回1
if pgrep -x snort > /dev/null 2>&1; then
    # 找到进程,认为健康
    exit 0
else
    # 没找到进程,认为不健康
    exit 1
fi

然后在Keepalived配置里加上脚本调用。注意,Keepalived运行脚本需要加script_user权限声明,否则可能因为权限不足失败。修改配置片段如下:

vrrp_script chk_snort {
    script "/etc/keepalived/check_snort.sh"  # 脚本路径
    interval 2                                # 每2秒检查一次
    weight -20                                # 检查失败时优先级减少20
}

再在vrrp_instance中引用这个脚本:

vrrp_instance snort_ha {
    # ... 其他配置省略
    track_script {
        chk_snort
    }
}

同时,还需要一个notify.sh脚本来处理角色切换后的动作。比如当节点变成主时,要启动Snort;变成备份时,要停止Snort。这样能保证同一时间只有一台机器在抓包。脚本如下:

#!/bin/bash
# 用法:notify.sh <master|backup|fault>
STATE=$1

case $STATE in
    master)
        echo "$(date) 成为主节点,启动Snort" >> /var/log/snort_ha.log
        # 这里用systemctl管理snort服务
        systemctl start snort
        ;;
    backup)
        echo "$(date) 成为备份节点,停止Snort" >> /var/log/snort_ha.log
        systemctl stop snort
        ;;
    fault)
        echo "$(date) 进入故障状态,停止Snort" >> /var/log/snort_ha.log
        systemctl stop snort
        ;;
esac

别忘了给脚本加执行权限:

chmod +x /etc/keepalived/check_snort.sh /etc/keepalived/notify.sh

3.5 启动集群并验证效果

两台节点都要启动Keepalived服务:

systemctl enable keepalived
systemctl start keepalived

验证主节点上是否绑定了虚拟IP,可以执行下面的命令:

ip addr show eth1

正常情况下,主节点上会看到192.168.10.100这个地址。然后我们在主节点上直接杀掉Snort进程,模拟一次故障:

pkill -9 snort

等几秒钟,再去备用节点上看,你会发现虚拟IP已经跑到了备用节点上,同时Snort也在备用节点上自动启动。到这里,一个最基本的故障切换就完成了。

3.6 看一下切换日志

为了确认切换过程,我们可以在两个节点上查看日志文件。日志中记录了角色变化和Snort的启停。例如执行:

tail -f /var/log/snort_ha.log

你会看到类似“成为主节点,启动Snort”或者“成为备份节点,停止Snort”的记录。如果一切正常,切换时间大概在3秒左右。这个时间受心跳间隔和健康检查间隔影响,你可以通过调整参数来微调。

四、故障切换机制是怎么运作的

4.1 健康检查的节奏

上面的例子里,健康检查每2秒跑一次,Keepalived的心跳通告每1秒发一次。整个过程大概是这样:主节点正常时,每秒通过VRRP通告告诉备用节点我活着。同时,本机每2秒检查Snort进程,如果发现进程没了,就把自己的优先级降低20。原本主节点优先级是120,降完变成100,而备用节点是100,这时备用节点的优先级反而高,于是备用节点宣布自己是新主,虚拟IP就转移了。这种机制在Keepalived里叫优先级抢占。

这里要注意一个细节:健康检查发现异常到真正的切换会有几秒的延迟,这是为了过滤掉偶发的短暂抖动。比如进程重启时可能会有一瞬间没起来,如果检查到立刻切换就会引起抖动。当然你也可以把interval调小,但越敏感误报越多。

4.2 脑裂问题怎么防

所谓脑裂,就是两个节点同时认为自己是主,都绑定了虚拟IP。这通常发生在两边的VRRP通信被阻断,但机器都还活着的情况下。解决脑裂的常用办法有三个:第一,上面配置里已经做了密码认证;第二,一定要保证virtual_router_id唯一,避免和其他网络里的VRRP组冲突;第三,最关键的是,千万别把两个节点都设成MASTER,否则它们根本不理会对方,各自为政。

另外,生产环境还可以加一条防火墙规则,只在集群通信网卡上放开VRRP协议,其他网卡过滤掉。这样能防止有人恶意伪造VRRP报文来干扰选举。

4.3 切换中的注意事项

切换不是瞬间完成的,VRRP协议里有一个advert_int决定通告频率,还有一个nopreempt开关。默认情况下,如果主节点恢复,它会重新抢占回虚拟IP,这对某些业务来说可能导致不必要的抖动。你可以把主节点配置里的nopreempt开启,或者把主节点的优先级设成和备用节点一样,这样恢复后不会立刻抢回,而是等下一次故障再切换。对于Snort这种监控系统,其实没必要频繁抢回,稳定就好。

另外,Snort的网卡监听模式在切换后要重新初始化。有些驱动在网卡被重启或MAC漂移后,需要重新设置混杂模式。建议在notify脚本里加上设置混杂模式的命令,例如:

# 在notify.sh的master分支中,确保监听网卡处于混杂模式
ip link set eth0 promisc on

4.4 状态同步再聊几句

前面说Snort状态同步不是必须的,但如果你真想做得更好,可以用一些折中办法。比如预热思路,备用节点也保持Snort在后台运行,但不对外输出关联告警,只维持会话表。当切换发生时,备用节点已经有一部分上下文了,漏报会少一些。这种办法不算严格的状态同步,但实现成本低,很多场景下效果不错。另一种思路是,把Snort日志和会话数据都写入共享存储,配合分布式锁来保证同一时间只有一个节点写。不过这已经属于另一套架构了,超出了今天的范围。

五、这种架构的优缺点大实话

5.1 看得见的好处

第一,成本低,不需要额外买商业负载均衡设备,两台服务器加一个免费软件就能搞定。第二,实现简单,Keepalived的配置不复杂,脚本逻辑也容易理解,运维人员能快速上手。第三,故障切换速度快,正常配置下几秒内完成,对于安全监控来说完全够用。第四,对Snort本身零侵入,不需要改Snort代码,只需要围绕它做一些辅助脚本。第五,可移植性好,如果你以后想换成Suricata,只需要把健康检查和启停脚本改一下,Keepalived部分几乎不用动。

5.2 不能回避的短板

先说状态同步问题,我们前面提到Snort维护的会话状态无法平滑迁移,如果网络流量里有很多长连接,可能会出现误报或漏报。再一个,Keepalived本身也占用资源,VRRP报文跑在二层网络上,如果网络环境不允许组播或者有防火墙拦截,就会经常误判。还有,这个方案解决的是设备级故障,不解决性能瓶颈。如果单台Snort处理不了峰值流量,你再怎么高可用也没用,需要换更强的机器或者改用集群分流方案。

另外,notify脚本里直接调用来启动Snort有一个小坑:如果Snort启动失败,但Keepalived已经切到了主状态,虚拟IP也绑定上了,而Snort没起来,就会造成假主现象。为了解决这个问题,你可以在切换到master后,循环检查Snort进程,如果几次都没起来,干脆把虚拟IP释放掉,让备用节点再抢过去。这种思路虽然简单,但很实用。

六、最后的总结

给Snort做高可用,本质上就是给安全监控的哨兵找一个随时能顶上来的替补。这篇文章从场景出发,聊了主备架构的设计思路,讲了VRRP和虚拟IP的作用,然后亲手搭了一套Keepalived加Snort的示例。整个机制的核心就是:心跳加优先级,让最适合的节点出来干活;健康检查加通知脚本,让Snort跟着角色走。

在动手实践时,提醒大家先理清自己的网络结构,确定虚拟IP放在哪个网段,再考虑是否需要状态同步。不用一上来就搞很复杂的方案,先把故障切换跑通,后面再逐步增强稳定性。毕竟,安全监控最重要的是不能断。