You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用libpcap捕获分片数据包后,如何识别并通过C代码重组?

识别并重组libpcap捕获的IP分片数据包

嘿,刚好之前折腾过类似的需求,给你一步步拆解怎么搞:

一、先搞懂怎么识别分片数据包

IP分片的判断完全依赖IPv4头部里的分片控制字段,抓两个核心点就行:

  • 片偏移(Fragment Offset):如果这个值不为0,说明这不是第一个分片
  • MF标志(More Fragments):如果这个标志位为1,说明后面还有更多分片

在C语言里,直接解析IP头的分片控制字段就行,用系统提供的宏会更稳妥(避免自己搞位操作出错):

#include <netinet/ip.h> // 注意系统差异,Linux下可能用 <linux/ip.h>

// 假设已经拿到IP头部指针 struct ip *ip_hdr;
uint16_t frag_off = ntohs(ip_hdr->ip_off);
bool is_fragment = false;

// 片偏移不为0,或者MF标志为1,就是分片包
if ((frag_off & IP_OFFMASK) != 0 || (frag_off & IP_MF) != 0) {
    is_fragment = true;
}

二、分片重组的实现思路与C代码示例

重组的核心是用缓存保存同一IP流的分片,直到所有分片都接收完整。同一IP流的唯一标识是:源IP + 目的IP + IPID(IPID是IP头里的标识字段,同一个分片组的IPID完全相同)。

1. 定义缓存结构

先搞个结构体来保存每个分片流的状态:

#include <stdint.h>
#include <stdbool.h>
#include <arpa/inet.h>

// 分片缓存条目
typedef struct FragmentCacheEntry {
    uint32_t src_ip;       // 源IP(网络字节序转主机字节序)
    uint32_t dst_ip;       // 目的IP
    uint16_t ip_id;        // IPID
    uint8_t *full_data;    // 重组后的完整数据包
    uint32_t total_length; // 完整包的总长度
    bool *received;        // 标记对应分片位置是否已接收
    time_t last_seen;      // 最后收到分片的时间,用于超时清理
    struct FragmentCacheEntry *next; // 链表节点,做简单的缓存管理
} FragmentCacheEntry;

// 全局缓存链表头
static FragmentCacheEntry *cache_head = NULL;

2. 核心重组逻辑

步骤大概是:解析分片信息 → 查找/创建缓存条目 → 写入分片数据 → 检查是否完成重组 → 处理完整数据包

#include <string.h>
#include <time.h>
#include <stdlib.h>

// 查找对应IP流的缓存条目
FragmentCacheEntry* find_cache_entry(uint32_t src_ip, uint32_t dst_ip, uint16_t ip_id) {
    FragmentCacheEntry *current = cache_head;
    while (current != NULL) {
        if (current->src_ip == src_ip && current->dst_ip == dst_ip && current->ip_id == ip_id) {
            return current;
        }
        current = current->next;
    }
    return NULL;
}

// 处理单个分片数据包
void process_fragment(struct ip *ip_hdr, const uint8_t *payload, uint32_t payload_len) {
    uint32_t src_ip = ntohl(ip_hdr->ip_src.s_addr);
    uint32_t dst_ip = ntohl(ip_hdr->ip_dst.s_addr);
    uint16_t ip_id = ntohs(ip_hdr->ip_id);
    uint16_t frag_off = ntohs(ip_hdr->ip_off);
    uint16_t offset = (frag_off & IP_OFFMASK) << 3; // 片偏移单位是8字节,所以左移3位(乘8)
    bool has_more_frags = (frag_off & IP_MF) != 0;

    FragmentCacheEntry *entry = find_cache_entry(src_ip, dst_ip, ip_id);

    // 没有缓存条目就新建一个
    if (entry == NULL) {
        entry = (FragmentCacheEntry*)malloc(sizeof(FragmentCacheEntry));
        if (entry == NULL) return;
        memset(entry, 0, sizeof(FragmentCacheEntry));
        entry->src_ip = src_ip;
        entry->dst_ip = dst_ip;
        entry->ip_id = ip_id;
        entry->last_seen = time(NULL);

        // 如果是最后一个分片,直接确定总长度并初始化缓存
        if (!has_more_frags) {
            entry->total_length = offset + payload_len;
            entry->received = (bool*)calloc((entry->total_length + 7)/8, sizeof(bool));
            entry->full_data = (uint8_t*)malloc(entry->total_length);
            if (entry->received == NULL || entry->full_data == NULL) {
                free(entry);
                return;
            }
        }

        // 添加到链表头部
        entry->next = cache_head;
        cache_head = entry;
    }

    // 如果之前没拿到总长度,现在拿到最后一个分片就更新缓存
    if (entry->total_length == 0 && !has_more_frags) {
        entry->total_length = offset + payload_len;
        entry->received = (bool*)calloc((entry->total_length + 7)/8, sizeof(bool));
        entry->full_data = (uint8_t*)malloc(entry->total_length);
        if (entry->received == NULL || entry->full_data == NULL) {
            free(entry->received);
            free(entry->full_data);
            free(entry);
            return;
        }
    }

    // 总长度确定后,把分片数据写入对应位置
    if (entry->total_length != 0 && offset + payload_len <= entry->total_length) {
        memcpy(entry->full_data + offset, payload, payload_len);
        // 标记对应分片位置已接收(这里简化为按8字节块标记,实际可以更精细)
        uint32_t start_block = offset / 8;
        uint32_t end_block = (offset + payload_len - 1) / 8;
        for (uint32_t i = start_block; i <= end_block; i++) {
            entry->received[i] = true;
        }
        entry->last_seen = time(NULL);

        // 检查是否所有分片都已接收
        bool is_complete = true;
        uint32_t total_blocks = (entry->total_length + 7)/8;
        for (uint32_t i = 0; i < total_blocks; i++) {
            if (!entry->received[i]) {
                is_complete = false;
                break;
            }
        }

        // 重组完成,处理完整数据包
        if (is_complete) {
            printf("重组完成!完整数据包长度:%u\n", entry->total_length);
            // 这里可以加你的业务逻辑,比如解析TCP/UDP数据
            // process_complete_packet(entry->full_data, entry->total_length);

            // 清理缓存条目
            FragmentCacheEntry **prev = &cache_head;
            while (*prev != NULL) {
                if (*prev == entry) {
                    *prev = entry->next;
                    break;
                }
                prev = &(*prev)->next;
            }
            free(entry->received);
            free(entry->full_data);
            free(entry);
        }
    }
}

// 定期清理超时缓存(比如超过30秒没收到新分片就删掉)
void clean_timeout_cache(time_t timeout) {
    FragmentCacheEntry **prev = &cache_head;
    time_t now = time(NULL);
    while (*prev != NULL) {
        FragmentCacheEntry *current = *prev;
        if (now - current->last_seen > timeout) {
            *prev = current->next;
            free(current->received);
            free(current->full_data);
            free(current);
        } else {
            prev = &current->next;
        }
    }
}

3. 必注意的细节

  • 系统兼容性:不同系统的IP头结构体可能有差异,比如Linux用struct iphdr,BSD用struct ip,要对应调整头文件和字段名。
  • 内存泄漏:一定要在重组完成或超时后释放缓存内存,不然会越占越多。
  • 超时机制:必须定期清理超时的缓存条目,不然如果某个分片丢失,缓存会一直占用内存。
  • IPv6分片:如果要处理IPv6分片,逻辑不一样,IPv6的分片信息在扩展头里,需要单独解析。

内容的提问来源于stack exchange,提问作者MicrosoctCprog

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.08 21:42:37