连接追踪:实现细节

这篇解决什么问题

TCP连接追踪的实现原理和eBPF实现细节。

先说结论

连接追踪的核心是状态机,eBPF可以高效实现连接追踪,但需要仔细设计map结构和状态转换逻辑。

现场背景

2024年8月,我们需要一个TCP连接追踪系统。

需求:

  1. 追踪所有TCP连接
  2. 记录连接状态变化
  3. 统计连接时长
  4. 分析连接失败原因

我最开始的思路

传统方案

考虑的方案:

  1. conntrack
  2. iptables -m conntrack
  3. eBPF实现

为什么不用conntrack

conntrack的优势:

  • 功能完善
  • 性能好
  • 内核原生

但问题:

  • 只能追踪内核管理的连接
  • 无法获取详细信息
  • 无法做复杂分析

查看conntrack信息:

bash 复制代码
# 查看连接
conntrack -L

# 查看统计
conntrack -S

为什么不用iptables

iptables的优势:

  • 配置简单
  • 功能丰富

但问题:

  • 只能做简单统计
  • 无法记录详细信息
  • 无法分析连接失败

真正的技术路径

TCP状态机

TCP连接的状态转换:

复制代码
CLOSED -> SYN_SENT -> SYN_RECEIVED -> ESTABLISHED
ESTABLISHED -> FIN_WAIT_1 -> FIN_WAIT_2 -> TIME_WAIT -> CLOSED
ESTABLISHED -> CLOSE_WAIT -> LAST_ACK -> CLOSED

内核中的TCP状态(include/net/tcp_states.h):

c 复制代码
enum {
    TCP_ESTABLISHED = 1,
    TCP_SYN_SENT,
    TCP_SYN_RECV,
    TCP_FIN_WAIT1,
    TCP_FIN_WAIT2,
    TCP_TIME_WAIT,
    TCP_CLOSE,
    TCP_CLOSE_WAIT,
    TCP_LAST_ACK,
    TCP_LISTEN,
    TCP_CLOSING,
    TCP_NEW_SYN_RECV,
};

eBPF连接追踪实现

设计数据结构:

c 复制代码
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>

struct conn_key {
    __u32 saddr;
    __u32 daddr;
    __u16 sport;
    __u16 daddr;
};

struct conn_info {
    __u8 state;
    __u64 start_time;
    __u64 last_update;
    __u32 packets;
    __u64 bytes;
};

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 100000);
    __type(key, struct conn_key);
    __type(value, struct conn_info);
} conn_map SEC(".maps");

struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 1 << 24);
} events SEC(".maps");

追踪连接状态:

c 复制代码
SEC("tracepoint/sock/inet_sock_set_state")
int trace_sock_set_state(struct trace_event_raw_sys_enter *ctx)
{
    struct sock *sk = (struct sock *)ctx->args[0];
    __u8 oldstate = ctx->args[1];
    __u8 newstate = ctx->args[2];

    struct conn_key key = {};
    struct conn_info *info;
    struct event *e;

    key.saddr = BPF_CORE_READ(sk, __sk_common.skc_rcv_saddr);
    key.daddr = BPF_CORE_READ(sk, __sk_common.skc_daddr);
    key.sport = BPF_CORE_READ(sk, __sk_common.skc_num);
    key.dport = bpf_ntohs(BPF_CORE_READ(sk, __sk_common.skc_dport));

    info = bpf_map_lookup_elem(&conn_map, &key);
    if (!info) {
        if (newstate == TCP_SYN_SENT) {
            struct conn_info init = {
                .state = newstate,
                .start_time = bpf_ktime_get_ns(),
                .last_update = bpf_ktime_get_ns(),
                .packets = 0,
                .bytes = 0
            };
            bpf_map_update_elem(&conn_map, &key, &init, BPF_ANY);
        }
        return 0;
    }

    info->state = newstate;
    info->last_update = bpf_ktime_get_ns();

    if (newstate == TCP_ESTABLISHED && oldstate != TCP_ESTABLISHED) {
        e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
        if (e) {
            e->type = 1;
            e->timestamp = bpf_ktime_get_ns();
            e->duration = info->last_update - info->start_time;
            bpf_ringbuf_submit(e, 0);
        }
    }

    if (newstate == TCP_CLOSE) {
        __u64 duration = bpf_ktime_get_ns() - info->start_time;
        
        e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
        if (e) {
            e->type = 2;
            e->timestamp = bpf_ktime_get_ns();
            e->duration = duration;
            bpf_ringbuf_submit(e, 0);
        }

        bpf_map_delete_elem(&conn_map, &key);
    }

    return 0;
}

验证过程

测试连接追踪:

bash 复制代码
# 加载eBPF程序
sudo bpftool prog load conntrack.o /sys/fs/bpf/conntrack

# 测试连接
curl http://192.168.1.100/

# 查看事件
sudo bpftool map dump id 2

输出:

复制代码
type: 1, timestamp: 1234567890, duration: 100000
type: 2, timestamp: 1234567990, duration: 200000

为什么不用其他方案

为什么不用conntrack

conntrack的优势:

  • 内核原生
  • 性能好

但问题:

  • 无法获取详细信息
  • 无法做复杂分析
  • 无法自定义逻辑

为什么不用iptables

iptables的优势:

  • 配置简单
  • 功能丰富

但问题:

  • 只能做简单统计
  • 无法记录详细信息
  • 无法分析连接失败

eBPF的优势

  • 性能好(<1% CPU)
  • 可以记录详细信息
  • 可以做复杂分析
  • 可以自定义逻辑

线上注意事项

1. 控制map大小

map大小要合理:

c 复制代码
// 错误:太大,浪费内存
__uint(max_entries, 1000000);

// 正确:根据实际需求设置
__uint(max_entries, 100000);

2. 定期清理map

map会一直增长,需要定期清理:

c 复制代码
void cleanup_conn_map(int map_fd)
{
    struct conn_key key, next_key;
    struct conn_info info;
    __u64 current_time = get_current_time_ns();

    while (bpf_map_get_next_key(map_fd, &key, &next_key) == 0) {
        bpf_map_lookup_elem(map_fd, &next_key, &info);
        if (current_time - info.last_update > TIMEOUT) {
            bpf_map_delete_elem(map_fd, &next_key);
        }
        key = next_key;
    }
}

3. 处理并发

多CPU并发访问map时要注意:

c 复制代码
// 错误:没有原子操作
info->packets++;

// 正确:使用原子操作
__sync_fetch_and_add(&info->packets, 1);

4. 监控性能

上线前要做性能测试:

bash 复制代码
# 测试CPU开销
sudo perf stat -e cycles,instructions,cache-misses -a sleep 10

# 测试延迟
sudo perf record -e cycles:kprobes -a sleep 10
sudo perf report

小结

连接追踪的核心是状态机。

eBPF实现连接追踪的优势:

  1. 性能好:<1% CPU
  2. 详细信息:可以记录所有状态变化
  3. 复杂分析:可以自定义逻辑
  4. 灵活:可以根据需求定制

实现要点:

  1. 设计合理的key(五元组)
  2. 实现状态机
  3. 定期清理map
  4. 使用原子操作

工程实践的关键是:理解TCP状态机,设计合理的map结构,处理并发问题

相关推荐
Web极客码2 小时前
WordPress 被植入隐藏管理员后门?清理实战分析
服务器·网络·wordpress
kk的matlab学习之路2 小时前
深入解析Calico:云原生网络的安全守护者
网络·其他·安全·云原生
8125035332 小时前
第2篇:为什么要有分层?从工程实践到架构设计
linux·网络·网络协议·计算机网络
天荒地老笑话么2 小时前
Host-only DHCP 机制:租约、网关是否需要
网络
hhzz3 小时前
阿里云基础网络的创建、业务隔离及多网之间的互联互通
网络·阿里云·网络架构
白太岁3 小时前
Muduo:(5) 主 Reactor 之 Acceptor 与 SubReactor 的分发
服务器·网络·c++·网络协议·tcp/ip
天上飞的粉红小猪4 小时前
数据链路层
linux·服务器·网络
星辰徐哥14 小时前
C语言网络编程入门:socket编程、TCP/IP协议、客户端与服务器通信的实现
c语言·网络·tcp/ip
清漠23314 小时前
win11“网络和Internet“中无“以太网“这个选项解决记录
服务器·网络·数据库