C程序解码TCP数据包 | AI生成和翻译

Home 2025.03

在 C 语言中解码 TCP 数据包涉及捕获原始网络数据(通常使用原始套接字或数据包捕获机制),然后逐步解析头部:以太网头部(如果存在)、IP 头部,最后是 TCP 头部。由于 TCP 运行在 IP 之上,您需要先提取 IP 头部以定位数据包中的 TCP 段。为简化起见,我假设我们正在处理 IPv4 和捕获 IP 数据包的原始套接字(无以太网层)。

以下是一个 C 程序,使用原始套接字捕获 TCP 数据包,解码 IP 头部,然后解码 TCP 头部以显示关键字段,如源/目标端口、序列号、确认号、标志和窗口大小。


用于解码 TCP 数据包的 C 程序

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/ip.h>   // 用于 struct iphdr
#include <netinet/tcp.h>  // 用于 struct tcphdr

// 以人类可读形式打印 TCP 标志
void print_tcp_flags(struct tcphdr *tcp) {
    printf("Flags: ");
    if (tcp->syn) printf("SYN ");
    if (tcp->ack) printf("ACK ");
    if (tcp->fin) printf("FIN ");
    if (tcp->rst) printf("RST ");
    if (tcp->psh) printf("PSH ");
    if (tcp->urg) printf("URG ");
    printf("\n");
}

int main() {
    int sock_fd;
    char buffer[65535]; // 最大 IP 数据包大小
    struct sockaddr_in source_addr, dest_addr;

    // 创建原始套接字以捕获 TCP 数据包
    sock_fd = socket(AF_INET, SOCK_RAW, IPPROTO_TCP);
    if (sock_fd < 0) {
        perror("Socket creation failed (run as root?)");
        exit(EXIT_FAILURE);
    }

    printf("Listening for TCP packets...\n");

    while (1) {
        // 接收原始数据包
        ssize_t bytes_received = recvfrom(sock_fd, buffer, sizeof(buffer), 0, NULL, NULL);
        if (bytes_received < 0) {
            perror("Receive failed");
            continue;
        }

        // IP 头部 (struct iphdr) 位于数据包起始位置
        struct iphdr *ip = (struct iphdr *)buffer;
        int ip_header_len = ip->ihl * 4; // ihl 以 32 位字为单位,转换为字节

        // 检查是否为 IPv4 数据包且协议为 TCP
        if (ip->version == 4 && ip->protocol == IPPROTO_TCP) {
            // TCP 头部紧随 IP 头部之后
            struct tcphdr *tcp = (struct tcphdr *)(buffer + ip_header_len);

            // 提取源和目标 IP 地址
            inet_ntop(AF_INET, &ip->saddr, (char *)&source_addr.sin_addr, sizeof(source_addr));
            inet_ntop(AF_INET, &ip->daddr, (char *)&dest_addr.sin_addr, sizeof(dest_addr));

            // 解码并打印 TCP 数据包详情
            printf("\n--- TCP Packet ---\n");
            printf("Source IP: %s\n", inet_ntoa(*(struct in_addr *)&ip->saddr));
            printf("Dest IP: %s\n", inet_ntoa(*(struct in_addr *)&ip->daddr));
            printf("Source Port: %u\n", ntohs(tcp->source));
            printf("Dest Port: %u\n", ntohs(tcp->dest));
            printf("Sequence Number: %u\n", ntohl(tcp->seq));
            printf("Acknowledgment Number: %u\n", ntohl(tcp->ack_seq));
            printf("Data Offset: %d bytes\n", tcp->doff * 4); // TCP 头部长度
            print_tcp_flags(tcp);
            printf("Window Size: %u\n", ntohs(tcp->window));
            printf("Checksum: 0x%04x\n", ntohs(tcp->check));
            printf("Urgent Pointer: %u\n", ntohs(tcp->urg_ptr));

            // 载荷(TCP 头部之后的数据)
            int tcp_header_len = tcp->doff * 4;
            int payload_len = bytes_received - ip_header_len - tcp_header_len;
            if (payload_len > 0) {
                printf("Payload Length: %d bytes\n", payload_len);
                printf("Payload (first 16 bytes): ");
                unsigned char *payload = (unsigned char *)(buffer + ip_header_len + tcp_header_len);
                for (int i = 0; i < payload_len && i < 16; i++) {
                    printf("%02x ", payload[i]);
                }
                printf("\n");
            } else {
                printf("No payload\n");
            }
        }
    }

    close(sock_fd); // 在此循环中无法到达,但出于良好实践
    return 0;
}

工作原理

  1. 套接字设置
    • socket(AF_INET, SOCK_RAW, IPPROTO_TCP) 创建一个原始套接字,捕获所有发送到主机的 TCP 数据包。这需要 root 权限(sudo)。
  2. 数据包捕获
    • recvfrom() 获取原始 IP 数据包,包括 IP 头部、TCP 头部和载荷。
  3. IP 头部解析
    • struct iphdr 定义 IPv4 头部(来自 <netinet/ip.h>)。
    • ihl(IP 头部长度)乘以 4 得到字节偏移量,因为它以 32 位字为单位。
    • 检查 version == 4protocol == IPPROTO_TCP 以确保是 IPv4 TCP 数据包。
  4. TCP 头部解析
    • struct tcphdr(来自 <netinet/tcp.h>)定义 TCP 头部,紧接在 IP 头部之后。
    • 关键字段:
      • sourcedest:源端口和目标端口(使用 ntohs 从网络字节序转换为主机字节序)。
      • seqack_seq:序列号和确认号(使用 ntohl 进行 32 位转换)。
      • doff:数据偏移量(TCP 头部长度,以字节为单位,同样乘以 4)。
      • synackfin 等:指示数据包类型的标志。
      • window:接收方窗口大小。
      • check:校验和(为简化起见,此处未验证)。
      • urg_ptr:紧急指针(与 URG 标志一起使用)。
  5. 载荷提取
    • 载荷从 TCP 头部之后开始。其长度计算为:总字节数 - IP 头部长度 - TCP 头部长度
    • 载荷的前 16 字节以十六进制形式打印。

编译与使用


关键结构


注意事项

如果您希望进一步改进(例如,添加校验和验证或过滤特定端口),请告诉我!


Back Donate