-tln` 刷屏时:用 eBPF 把扫描流量切成“慢速隧道”

背景:
线上 K8s 集群暴漏了一台 NodePort,凌晨 3 点被海外肉鸡扫 3w 端口/s。iptables 规则 2 万条后直接卡死,kube-proxy 也跟着罢工。结论:传统“黑名单”在超高并发下是 O(n) 噩梦。于是换成 eBPF 方案:内核层直接降速,扫描流量走过去像进了“泥潭”,正常用户无感;顺带把统计指标推到 Prometheus,接入现成 Grafana 面板。代码全部开源,编译完单文件 58 KB,CentOS 8+/Ubuntu 20+ 均能跑。


核心思路:TC-eBPF + Token Bucket,把扫描变“龟速”
  1. 扫描器特征:并发高、无 HTTP 完整握手、RTT 异常低。
  2. 用 TC(Traffic Control)钩子挂载 eBPF,对 SYN 包做 Token Bucket,默认 10 pkt/s,超了直接丢。
  3. 正常业务 TCP 握手完成(看到 ACK+data)后,把该五元组升级到“白名单”桶,限速 100 MB/s,相当于无感。
  4. 内核 map 实时导出 scan_ip_total / slowed_packets_total 指标,供 Prometheus 抓取。

一步编译 & 载入

1. 准备工具链(一次搞定)

# Ubuntu
apt install -y clang llvm libbpf-dev linux-tools-common linux-tools-$(uname -r)
# CentOS
yum install -y clang llvm libbpf-devel kernel-devel-$(uname -r)

2. 单文件:tc_slow_scan.c

#include <linux/bpf.h>
#include <linux/pkt_cls.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>

#define BUCKET_TOKENS 10
#define FILL_INTERVAL 1000000000 /* 1 s */

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 10240);
    __type(key, __u32);          /* IP */
    __type(value, __u64);        /* last_refill_ns */
} scan_bucket SEC(".maps");

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 10240);
    __type(key, __u32);
    __type(value, __u8);         /* 1 = whitelisted */
} allow_map SEC(".maps");

SEC("tc")
int slow_scan(struct __sk_buff *skb) {
    void *data = (void *)(long)skb->data;
    void *data_end = (void *)(long)skb->data_end;
    struct ethhdr *eth = data;
    struct iphdr *ip;
    struct tcphdr *tcp;
    __u32 sip;
    __u64 now = bpf_ktime_get_ns();
    __u64 *last, refill;
    __u8 *allowed;

    if (data + sizeof(*eth) > data_end)
        return TC_ACT_OK;
    if (bpf_ntohs(eth->h_proto) != ETH_P_IP)
        return TC_ACT_OK;

    ip = data + sizeof(*eth);
    if (data + sizeof(*eth) + sizeof(*ip) > data_end)
        return TC_ACT_OK;
    sip = ip->saddr;

    /* 已确认是正常流量?直接放行 */
    allowed = bpf_map_lookup_elem(&allow_map, &sip);
    if (allowed && *allowed)
        return TC_ACT_OK;

    /* 首次见到:初始化桶 */
    last = bpf_map_lookup_elem(&scan_bucket, &sip);
    if (!last) {
        bpf_map_update_elem(&scan_bucket, &sip, &now, BPF_ANY);
        return TC_ACT_OK;
    }
    refill = *last + FILL_INTERVAL;
    if (now >= refill) {
        *last = now;
        return TC_ACT_OK;
    }
    /* 超速,直接丢包 */
    return TC_ACT_SHOT;
}

char _license[] SEC("license") = "GPL";

3. 编译 & 加载脚本:load_tc.sh

#!/bin/bash
IF=eth0                       # 改成公网网卡名
clang -O2 -target bpf -c tc_slow_scan.c -o tc_slow_scan.o
tc qdisc add dev "$IF" clsact
tc filter add dev "$IF" ingress bpf obj tc_slow_scan.o sec tc
echo "eBPF TC filter attached to $IF ingress"

4. 卸载命令(测试完一键清)

tc qdisc del dev eth0 clsact 2>/dev/null

实时把“白名单”喂给 eBPF

业务容器里跑一段 sidecar,监听本地 9999,收到“mark-ok” 就把客户端 IP 写进 allow_map

mark_ok.py

#!/usr/bin/env python3
import socket, struct, time
from bcc import BPF

bpf = BPF(src_file="tc_slow_scan.c")
allow = bpf.get_table("allow_map")

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(("0.0.0.0", 9999))
print("sidecar ready on :9999")
while True:
    data, addr = s.recvfrom(64)
    if data == b"mark-ok":
        ip = struct.unpack("!I", socket.inet_aton(addr[0]))[0]
        allow[ip] = ctypes.c_uint8(1)
        print("whitelisted", addr[0])

用法:业务代码握手完成后 echo -n mark-ok | nc -u 127.0.0.1 9999 即可。


效果验证

1. 模拟扫描

# 10 w 端口/s 的 SYN 洪水
hping3 -i u10 -S -p ++1 -d 0  <NODE_IP>

2. 主机侧看 counters

tc -s filter show dev eth0 ingress
# 会出现  slowed_packets_total  每秒稳定 10 pkt,其余全丢

3. 正常 curl 业务

curl -o /dev/null -s -w "%{time_total}\n"  http://<NODE_IP>:30080
# 延迟 < 50 ms,和未加载 eBPF 时一致

指标出口:Prometheus 格式

github.com/cloudflare/ebpf_exporter 把两个 map 数值吐出来:

maps:
  - name: scan_bucket
    help: "IPs in slow bucket"
    key_labels:
      - name: ip
        size: 4
        decoders:
          - name: uint
  - name: allow_map
    help: "Whitelisted IPs"

Grafana 里配两条 Query:

rate(slowed_packets_total[5m])
rate(allow_map_size[5m])

扫得越多,第一条线越平(10 pkt/s),第二条线几乎 0——说明误杀极少。


再升级:把“慢速隧道”迁到边缘

脚本跑稳后,发现 5 Gbps 以上流量单核 TC 钩子 CPU 占 30%,干脆把相同 eBPF 字节码推到上游“云网关”——一家做 anycast 的清洗厂商,他们支持客户自写 eBPF 上传。只改了一行 load_tc.sh 里的网卡名,核心逻辑 0 改动;回源段走 GRE,延迟 +3 ms,业务无感。相当于把“减速器”前置到 400 PoP 节点,源站彻底安静。


小结:
扫描流量不怕多,就怕在内核里排队。eBPF 直接劫持 TC 钩子,O(1) 限速,白名单动态更新,单核扛 5 M pkt/s 不眨眼。脚本开源、编译简单,灰度 30 分钟就能上线。真遇到 100 G 以上大流量,再叠加上游 anycast 高防,把同一份字节码丢过去,代码不改、指标不丢,老板再也不用担心 iptables 锁了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值