使用 eBPF 与 XDP 构建高性能 SSH 暴力破解防火墙 —— TyrShield深度解析

366 阅读4分钟

Untitled.png

本篇技术博客将带你深入了解 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 支持开源!