连接追踪:实现细节

这篇解决什么问题

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结构,处理并发问题

相关推荐
WX:ywyy679829 分钟前
短剧付费转化系统:试看、卡点、解锁、会员全链路商业化设计
网络·短剧·短剧app·短剧系统·短剧系统开发·短剧app开发·短剧系统搭建
tritone33 分钟前
最近在学习网络配置中的Port Forwarding(端口转发)技术,为了有个稳定的实验环境,我试用了阿贝云的免费虚拟主机和免费云服务器
服务器·网络·学习
橘子1339 分钟前
NAT,代理服务,内网穿透
网络·智能路由器
文静小土豆1 小时前
Docker 网络配置指南:Bridge、Host、None、Container 全攻略
网络·docker·容器
white-persist1 小时前
【CTF线下赛 AWD】AWD 比赛全维度实战解析:从加固防御到攻击拿旗
网络·数据结构·windows·python·算法·安全·web安全
_OP_CHEN2 小时前
【Linux网络编程】(一)初识计算机网络:从独立主机到协议世界的入门之旅
linux·服务器·网络·网络协议·计算机网络·socket·c/c++
一袋米扛几楼986 小时前
【密码学】CrypTool2 工具是什么?
服务器·网络·密码学
南棱笑笑生10 小时前
20260310在瑞芯微原厂RK3576的Android14查看系统休眠时间
服务器·网络·数据库·rockchip
yy552710 小时前
LNAMP 网络架构与部署
网络·架构
Godspeed Zhao11 小时前
现代智能汽车系统——CAN网络2
网络·汽车