本篇技术博客将带你深入了解 TyrShield 如何利用 eBPF 和 XDP 在内核层实现 SSH 暴力破解防护。我们将逐步拆解 BPF 程序的实现细节,说明各个 Map 的设计原理与性能优势,并展示用户态 Go 应用如何高效消费内核事件。
1. 项目初衷
随着系统规模不断扩大,针对 SSH 服务的暴力破解攻击激增:攻击者通过海量 SYN 包探测弱口令,给网络与资源带来巨大压力。传统基于 iptables 或 fail2ban 的防护需要频繁切换内核与用户态,产生较大延迟,不适用于高并发场景。
TyrShield 的目标:
- 线速过滤:在网卡驱动层(XDP Hook)直接丢弃超限 SYN 包,实现近乎零延迟。
- 非阻塞日志:结合 io_uring 与 Zap,实现异步结构化日志,避免 I/O 阻塞主流程。
- 轻量可扩展:BPF 程序资源消耗极少,可在多核或容器环境下水平扩展。
2. eBPF 与 XDP 简要介绍
- eBPF:可将自定义字节码加载到 Linux 内核的安全沙箱中执行,通过 Map 与用户态交换数据。
- XDP (eXpress Data Path) :在网络驱动最早阶段拦截数据包,支持丢弃、通过或重定向,处理延迟可达纳秒级。
3. 核心 eBPF 程序解析
源代码位于 ssh_defense.bpf.c,主要包含:连接尝试计数、封禁决策和事件通知三大功能。
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#include <bpf/bpf_tracing.h>
#define SSH_PORT 22
#define MAX_ATTEMPTS 5
#define TIME_WINDOW_NS (60ULL * 1000000000ULL) // 60 秒
#define BLOCK_TIME_NS (300ULL * 1000000000ULL) // 300 秒
// 全局配置 Map
struct config {
__u32 ssh_port;
__u32 max_attempts;
__u64 time_window_ns;
__u64 block_time_ns;
};
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1);
__type(key, __u32);
__type(value, struct config);
} config_map SEC(".maps");
// 每个源 IP 的尝试信息
struct attempt_info {
__u32 count;
__u64 first_time;
__u64 last_time;
__u64 block_until;
};
// Perf 事件结构
struct event {
__u32 ip;
__u32 count;
};
BPF_HASH(ssh_attempts, __u32, struct attempt_info);
BPF_PERF_OUTPUT(events);
3.1 全局配置 Map
config_map 存储运行时配置,包括 SSH 端口、最大尝试次数、时间窗口及封禁时长,可在用户态通过 bpf_update_elem 动态下发。
3.2 尝试计数与封禁逻辑
SEC("xdp_ssh_filter")
int xdp_prog(struct xdp_md *ctx) {
// 省略以太网/IP/TCP 头解析...
// 仅拦截首个 SYN 包
if (!(tcph->syn && !tcph->ack))
return XDP_PASS;
__u32 src_ip = iphdr->saddr;
struct config cfg;
bpf_map_lookup_elem(&config_map, &zero, &cfg);
struct attempt_info *info = bpf_map_lookup_elem(&ssh_attempts, &src_ip);
__u64 now = bpf_ktime_get_ns();
if (!info) {
// 首次尝试
struct attempt_info init = {1, now, now, 0};
bpf_map_update_elem(&ssh_attempts, &src_ip, &init, BPF_ANY);
} else {
// 在封禁期内直接丢弃
if (now < info->block_until)
return XDP_DROP;
// 滑动窗口过期则重置
if (now - info->first_time > cfg.time_window_ns) {
info->count = 1;
info->first_time = now;
} else {
info->count++;
}
info->last_time = now;
// 超限封禁并通知用户态
if (info->count > cfg.max_attempts) {
info->block_until = now + cfg.block_time_ns;
struct event ev = {src_ip, info->count};
events.perf_submit(ctx, &ev, sizeof(ev));
return XDP_DROP;
}
}
return XDP_PASS;
}
3.3 Perf 事件通知
BPF_PERF_OUTPUT(events) 通过高效环形缓冲将封禁事件推送到用户态,避免内核与用户态频繁拷贝。
4. 用户态 Go 程序消费 Perf 事件
在 main.go 中,使用 Cilium/ebpf 库读取事件:
reader, _ := ebpf.NewPerfReader(perfReaderOptions)
go func() {
for {
record, err := reader.Read()
if err != nil {
log.Printf("读取 perf 事件出错: %v", err)
continue
}
var ev Event
binary.Read(bytes.NewBuffer(record.RawSample), binary.LittleEndian, &ev)
logger.Infow("SSH 暴力破解源已封禁", "ip", ev.IP.String(), "count", ev.Count)
}
}()
由于纳秒级的eBPF数据传输至用户态,一般基于epoll实现的日志库不一定能接得住,所以采用iouring + Zap实现,但可惜golang版本io_uring作者已经不再维护,因此自己fork后进行了局部优化和调整,最终效果还是比较不错。 最终结合 io_uring + Zap,实现高性能异步非阻塞、结构化的日志处理方式,确保主流程不受 I/O 延迟影响。
5. 性能对比
延迟开销
- TyrShield(XDP/eBPF):< 1 µs
- fail2ban(iptables):~100–500 µs
- CrowdSec(iptables):~100–300 µs
CPU 占用
- TyrShield:极低
- fail2ban:中等(Python)
- CrowdSec:中等(Go)
日志模式
- TyrShield:异步 io_uring
- fail2ban:同步文件 I/O + 日志解析
- CrowdSec:同步 + Agent 模式
最后,TyrShield 利用 eBPF + XDP 在内核最早网络钩子处拦截暴力破解流量,并结合 Perf Events + io_uring 实现超低开销可观测。欢迎访问仓库并贡献代码:
🔗 GitHub 仓库
⭐️ 点赞并 Fork 支持开源!