MINIBLOG

Blog Note Tags Links About
Home Search
Apr 16, 2026
miniyuan

计算机网络 Lab4 路由协议


libpcap 库

libpcap 的整体执行流程如下:

图 1:libpcap 整体执行流程
图 1:libpcap 整体执行流程

核心数据结构

  1. 会话句柄

    typedef struct pcap pcap_t;           // 捕获会话句柄(不透明结构)
    typedef struct pcap_dumper pcap_dumper_t;  // 转储文件句柄

    注:句柄可以类比函数指针,但是不完全相同。函数指针是内存地址,可以直接调用;句柄只是一个对象的标识,只能传入接口函数由其内部调用,实际上它可能是函数指针也可能仅仅是一个整数标识符。

  2. 数据包头部

    struct pcap_pkthdr {
        struct timeval ts;        /* 时间戳(捕获时间) */
        bpf_u_int32 caplen;       /* 实际捕获的数据长度(可能截断) */
        bpf_u_int32 len;          /* 数据包原始长度 */
    };

    其中:

    #include <sys/time.h>
    
    struct timeval {
        time_t      tv_sec;       // 秒数(从 1970-01-01 00:00:00 UTC 开始)
        suseconds_t tv_usec;      // 微秒数(0-999999)
    };

    注:caplen ⩽\leqslant⩽ len,当设置 snaplen 截断捕获时,两者可能不等。

  3. 回调函数

    在抓到包后调用,按用户自定义的方式处理数据包。

    typedef void (*pcap_handler)(u_char *user, const struct pcap_pkthdr *h, 
                                const u_char *bytes);
    • user:用户自定义数据
    • h:libpcap 提供的数据包的头部
    • bytes:libpcap 提供的数据包的实际内容(以太网帧开始)
  4. BPF 过滤器程序

    伯克利数据包过滤器(Berkeley Packet Filter)。

    struct bpf_program {
        u_int bf_len;             /* 指令数量 */
        struct bpf_insn *bf_insns;/* 指向过滤指令数组的指针 */
    };
  5. 网络设备信息

    typedef struct pcap_if {
        struct pcap_if *next;     /* 下一个设备 */
        char *name;               /* 设备名(如 "eth0") */
        char *description;        /* 设备描述 */
        struct pcap_addr *addresses; /* 地址列表 */
        u_int flags;              /* 标志(PCAP_IF_LOOPBACK等) */
    } pcap_if_t;
  6. 统计信息

    struct pcap_stat {
        u_int ps_recv;            /* 收到的包数 */
        u_int ps_drop;            /* 丢弃的包数(缓冲区满) */
        u_int ps_ifdrop;          /* 接口层丢弃的包数 */
    };

核心函数接口

  1. 设备查找与打开

    由于 PC 上有多个网卡(设备),比如有线网卡 wth0、无线网卡 wlan0、回环网卡 lo,所以我们必须指定捕获设备。

    获取设备信息:

    函数原型功能说明
    char *pcap_lookupdev(char *errbuf)返回第一个可用的非回环设备名(已废弃,建议用 pcap_findalldevs)
    int pcap_findalldevs(pcap_if_t **alldevsp, char *errbuf)获取所有可用网络设备列表,alldevsp 是一个在堆上的链表
    void pcap_freealldevs(pcap_if_t *alldevs)释放设备列表,也即释放堆上分配的内存
    int pcap_lookupnet(const char *device, bpf_u_int32 *netp, bpf_u_int32 *maskp, char *errbuf)获取设备的网络地址和掩码

    打开实时捕获设备:

    pcap_t *pcap_open_live(const char *device, int snaplen, 
                        int promisc, int to_ms, char *errbuf);
    • snaplen: 最大捕获字节数(通常设 65535 或 BUFSIZ)。
    • promisc: 1 开启混杂模式,0 非混杂。混杂模式下会接收所有经过设备的数据包。
    • to_ms: 读取超时(毫秒),0 表示无超时阻塞等待。

    离线操作:

    // 打开一个之前保存的抓包文件
    pcap_t *pcap_open_offline(const char *fname, char *errbuf);
    // 创建虚拟句柄(用于编译 BPF 过滤器)
    pcap_t *pcap_open_dead(int linktype, int snaplen); 
  2. 数据包捕获

    函数原型功能说明
    int pcap_loop(pcap_t *p, int cnt, pcap_handler callback, u_char *user)循环捕获 cnt 个包(-1 表示无限),每次捕获调用 callback
    int pcap_dispatch(pcap_t *p, int cnt, pcap_handler callback, u_char *user)类似 loop,但超时或处理完缓冲区内数据即返回
    const u_char *pcap_next(pcap_t *p, struct pcap_pkthdr *h)捕获单个数据包,阻塞等待,返回指向原始数据包的指针
    int pcap_next_ex(pcap_t *p, struct pcap_pkthdr **pkt_header, const u_char **pkt_data)捕获单个包(返回 1:成功, 0:超时, -1:错误, -2:离线文件EOF)
    void pcap_breakloop(pcap_t *p)中断正在运行的 pcap_loop/dispatch
  3. 数据包过滤

    // 编译过滤表达式
    int pcap_compile(pcap_t *p, struct bpf_program *fp, 
                    char *str, int optimize, bpf_u_int32 netmask);
    
    // 应用过滤器
    int pcap_setfilter(pcap_t *p, struct bpf_program *fp);
    
    // 释放编译后的 BPF 程序内存
    void pcap_freecode(struct bpf_program *fp);
    • p:提供编译所需的链路层类型、快照长度。
    • fp:是输出参数,编译生成的 BPF 指令会存储在这里。
    • str:人类可读的过滤表达式字符串,例如 "tcp port 80" 或 "host 192.168.1.1"。
    • optimize:是否优化生成的 BPF 指令,1 表示优化,0 表示不优化。
    • netmask:网络掩码,用于解析 net/mask 这类过滤器;不需要时可传 PCAP_NETMASK_UNKNOWN(通常就是 0)。

    示例流程:

    pcap_t *handle = pcap_open_live("eth0", 65535, 1, 1000, errbuf);
    struct bpf_program fp;
    pcap_compile(handle, &fp, "icmp or tcp port 80", 1, net);
    pcap_setfilter(handle, &fp);
    pcap_freecode(&fp);
  4. 数据包转储

    // 打开转储文件
    pcap_dumper_t *pcap_dump_open(pcap_t *p, const char *fname);
    pcap_dumper_t *pcap_dump_fopen(pcap_t *p, FILE *fp);
    
    // 写入数据包(可作为 pcap_loop 的回调函数)
    void pcap_dump(u_char *user, struct pcap_pkthdr *h, u_char *sp);
    
    // 刷新缓冲区到磁盘
    int pcap_dump_flush(pcap_dumper_t *p);
    
    // 关闭转储文件
    void pcap_dump_close(pcap_dumper_t *p);

    注:

  5. 辅助与清理函数

    函数说明
    int pcap_datalink(pcap_t *p)获取链路层类型(如 Ethernet 为 1)
    int pcap_snapshot(pcap_t *p)获取 snaplen
    int pcap_stats(pcap_t *p, struct pcap_stat *ps)获取捕获统计
    char *pcap_geterr(pcap_t *p)获取错误信息
    void pcap_perror(pcap_t *p, char *prefix)打印错误信息
    void pcap_close(pcap_t *p)关闭会话并释放资源

典型使用流程

char errbuf[PCAP_ERRBUF_SIZE];
pcap_if_t *alldevs;
pcap_t *handle;
struct bpf_program fp;

// 1. 查找设备
pcap_findalldevs(&alldevs, errbuf);
// 2. 打开设备
handle = pcap_open_live(alldevs->name, 65535, 1, 1000, errbuf);
// 3. 编译并设置过滤器
pcap_compile(handle, &fp, "port 80", 0, PCAP_NETMASK_UNKNOWN);
pcap_setfilter(handle, &fp);
// 4. 循环捕获
pcap_loop(handle, 0, packet_handler, NULL);
// 5. 清理
pcap_freecode(&fp);
pcap_close(handle);
pcap_freealldevs(alldevs);

参考文档:

  • TCPDUMP & LIBPCAP 官方文档
  • Linux man page - pcap
  • Stanford libpcap Tutorial

lab 中的重要数据结构与函数

协议头部

// Ethernet frame header
typedef struct {
    uint8_t dst_mac[6];  // Destination MAC address
    uint8_t src_mac[6];  // Source MAC address
    uint16_t ether_type; // Ethernet type (e.g. 0x0800 for IP)
} __attribute__((packed)) eth_header_t;

// IP protocol header
typedef struct {
    uint8_t version_ihl; // Version (4 bits) + IHL (4 bits)
    uint8_t tos;         // Type of service
    uint16_t total_len;  // Total length
    uint16_t id;         // Identification
    uint16_t frag_off;   // Flags (3 bits) + Fragment offset (13 bits)
    uint8_t ttl;         // Time to live
    uint8_t protocol;    // Protocol (TCP=6, UDP=17)
    uint16_t checksum;   // Header checksum
    uint32_t src_ip;     // Source IP address
    uint32_t dst_ip;     // Destination IP address
} __attribute__((packed)) ip_header_t;

网络设备有关数据结构

我们先区分一下 device 的概念。 下面所有的 device 均指网卡 NIC / 端口 port,而使用 node 指代网络中的物理设备节点(如 Hub、Switch、Router、Host)。

device 类型 net_device_t 中存储了网卡的名称、索引以及捕获线程信息。实际上还应储存网卡的 MAC,由于缺失导致 Router 实现会有问题。

node 内有 packet buffer 用于储存收到的 packet。

packet 的条目类型为 packet_entry_t,储存了 packet 的入端口(ingress device)、原始数据、真实数据长度以及收包时间。

而整个 packet buffer 类型 packet_buffer_t 则是一个环形队列,为了并行添加了信号量进行保护。

可以在 setup_star.sh 中看到拓扑网络的详细信息。 其中定义了 node device、host1、host2、host3、host4(注意这里的 device 是指物理设备)。 每个 node 添加了 device node/hostx-ethx。

/* Network device information */
typedef struct {
    char name[32];       // Device name
    pcap_t *handle;      // pcap handle
    pthread_t thread_id; // Capture thread ID
    int index;           // Device index
} net_device_t;

/* Packet buffer entry */
typedef struct {
    net_device_t *device;          // Ingress device
    uint8_t data[PACKET_BUF_SIZE]; // Packet data
    uint32_t len;                  // Packet length
    uint64_t timestamp;            // Capture timestamp
} packet_entry_t;

/* Global packet buffer */
typedef struct {
    packet_entry_t packets[MAX_PACKETS];
    int head;
    int tail;             // circular buffer
    pthread_mutex_t lock; // mutex lock
} packet_buffer_t;

在 lab.c 中实例化。 devices 储存所有 node 的所有我们自己创建的 device,也即名为 device/hostx-ethx。注意每个 node 会有一些 Docker 默认 device,比如 eth0、lo 等。 由于只模拟一台物理设备 Hub/Switch/Router,所以只实例化了它内部的一个 packet_buffer。

net_device_t devices[MAX_DEVICES];
int device_count = 0;
packet_buffer_t pkt_buffer;

send_packet

发送数据包的函数。 dev 指明了 node 发包的出端口(egress device),data 和 len 为原始数据与长度。

int send_packet(net_device_t *dev, const uint8_t *data, uint32_t len) {
    if (pcap_inject(dev->handle, data, len) == -1) {
        fprintf(stderr, "Error sending packet on %s: %s\n",
                dev->name, pcap_geterr(dev->handle));
        return -1;
    }
    return 0;
}

capture_thread

接收数据包的线程函数。 注意传入的参数为收包设备的指针 net_device_t*。

void *capture_thread(void *arg) {
    net_device_t *dev = (net_device_t *)arg;
    struct pcap_pkthdr header;
    const u_char *packet;

    printf("Starting capture on %s\n", dev->name);

    while (1) {
        packet = pcap_next(dev->handle, &header);
        printf("pcap_next returned on %s: %p\n", dev->name, packet);
        if (!packet)
            continue;

        pthread_mutex_lock(&pkt_buffer.lock);

        // Check if buffer is full
        if ((pkt_buffer.head + 1) % MAX_PACKETS == pkt_buffer.tail) {
            fprintf(stderr, "Packet buffer full, dropping packet\n");
            pthread_mutex_unlock(&pkt_buffer.lock);
            continue;
        }

        // Store packet in buffer
        packet_entry_t *entry = &pkt_buffer.packets[pkt_buffer.head];
        entry->device = dev;
        entry->len = header.len;
        entry->timestamp = header.ts.tv_sec * 1000000 + header.ts.tv_usec;
        memcpy(entry->data, packet, header.len > PACKET_BUF_SIZE ? PACKET_BUF_SIZE : header.len);

        pkt_buffer.head = (pkt_buffer.head + 1) % MAX_PACKETS;
        pthread_mutex_unlock(&pkt_buffer.lock);
    }

    return NULL;
}

common_init

主要进行了以下工作:

  1. 初始化 node 内的环形队列 packet buffer。
  2. 寻找网络设备。
  3. 筛选并储存我们自己创建的 device,也即名为 device/hostx-ethx。
  4. 为筛选后的 device 创建 pcap 捕获会话句柄,并为其创建对应的线程持续收发数据包。

Hub

Hub 位于物理层,直接无条件泛洪收到的包。

for (int i = 0; i < device_count; i++) {
    // 因为 entry->device 存储的是 devices 数组中的地址,所以可以直接比较指针
    // if (&devices[i] == entry->device) {
    //     continue;
    // }
    // 也可以比较设备名
    if (strcmp(devices[i].name, entry->device->name) == 0) {
        continue;
    }

    send_packet(&devices[i], entry->data, entry->len);
}

在 setup_star.sh 中的指令:

sudo docker exec $container1 ip link set $veth1 up
sudo docker exec $container2 ip link set $veth2 up

打开网络接口,接口默认启用 IPv6,所以会自动在链路上探测 IPv6 路由器,持续探测一段时间才会暂停。 而 test_hub 执行过早就会接收到这些 IPv6 包。不过只要在 setup_star.sh 一段时间之后再 test_hub 就不会收到了。

Makefile 的 test_hub 增加 sleep 1,保证避免过快 killall 导致 ping reply 没有记录。

Switch

Switch 位于链路层,内部存有 MAC 学习表。 学习表的条目类型 fdb_entry_t 储存 MAC 与 Switch 的端口 device 的对应关系。整个学习表类型 forwarding_db_t 为一个数组。

/* MAC address to device mapping entry */
typedef struct {
    uint8_t mac[6];  // MAC address
    char device[32]; // Device name
} fdb_entry_t;

/* Forwarding database (MAC learning table) */
typedef struct {
    fdb_entry_t entries[MAX_DEVICES];
    int count;
} forwarding_db_t;

Switch 在一个端口 device 接收到数据包时,会先利用包的 Ether 头部中的 src MAC 字段学习到对应关系 <src MAC, device>。 然后根据 dst MAC 进行转发,此时需要查询学习表。若已有条目则直接转发,否则进行泛洪。

// Get Ethernet header
eth_header_t *entry_eth = (eth_header_t *)entry->data;

// Learn source MAC address
int found = 0;

// Found mac entry, update the corresponding device
for (int i = 0; i < fdb.count; i++) {
    if (memcmp(fdb.entries[i].mac, entry_eth->src_mac, 6) == 0) {
        found = 1;
        memcpy(fdb.entries[i].device, entry->device, 31);
        break;
    }
}

// Otherwise add the learning entry
if (!found) {
    memcpy(fdb.entries[fdb.count].mac, entry_eth->src_mac, 6);
    memcpy(fdb.entries[fdb.count].device, entry->device, 31);
    fdb.count++;
}

// Forward packet
found = 0;

// case 1: Found destination, forward to that device
for (int i = 0; i < fdb.count; i++) {
    if (memcmp(fdb.entries[i].mac, entry_eth->dst_mac, 6) == 0) {
        found = 1;

        // Found device info by mac
        for (int j = 0; j < device_count; j++) {
            if (strcmp(devices[j].name, fdb.entries[i].device) == 0) {
                send_packet(&devices[j], entry->data, entry->len);
                break;
            }
        }

        break;
    }
}

// case 2: Flood to all ports except ingress
if (!found) {
    for (int i = 0; i < device_count; i++) {
        if (&devices[i] == entry->device)
            continue;

        send_packet(&devices[i], entry->data, entry->len);
    }
}

不过由于 fdb_entry_t 中储存的不是设备指针而只有设备名,所以每次都必须在所有设备中用设备名进行查询。

Router

Router 位于网络层,内部存有路由表。 Router 依据路由表决定包的转发路径,其匹配粒度为网络前缀(而非单个主机)。查表时遵循 Longest Prefix Match 原则。 Router 转发时只负责送到下一跳(next-hop):下一跳可能是目标子网内的某台主机(直接交付),也可能是另一台 Router(间接交付)。

子网是一组可以不经过路由器直接通信的 IP 地址集合,由网络地址和子网掩码共同定义。

Router 分为 Data Plane 和 Control Plane,二者在功能上分离:

  • Control Plane:运行路由协议,处理协议报文,维护并更新路由表。
  • Data Plane:依据已建立的路由表,对收到的 IP 数据包执行实际的转发、头部修改与重封装。

在该实验框架中,两个 plane 各使用一个 buffer:

  • Data Plane 使用上述 packet_buffer_t 类型的 pkt_buffer。
  • Control Plane 使用 cp_buffer_t 类型的 cp_buffer,结构与 pkt_buffer 完全相同。

路由表与距离向量

路由表条目类型 route_entry_t 储存 egress device 与对应的 network IP、子网掩码以及距离。 整个路由表类型 routing_table_t 以数组存储。 距离向量包类型 dv_packet_t 用于邻居间交换路由更新,包含自身 network IP、子网掩码以及距离。注意所有的 dv_packet 都是大端序的。

/* Routing table entry */
typedef struct {
    uint32_t dest_ip;  // Destination IP address
    uint32_t mask;     // Network mask
    char out_dev[32];  // Output device name
    uint32_t distance; // Distance metric
} route_entry_t;

/* Distance Vector (DV) packet */
typedef struct {
    uint32_t dest_ip;  // Destination IP address
    uint32_t mask;     // Network mask
    uint32_t distance; // Distance metric
} __attribute__((packed)) dv_packet_t;

/* Routing table */
typedef struct {
    route_entry_t entries[MAX_ROUTES];
    int count;
} routing_table_t;

/* Control plane message buffer */
typedef struct {
    packet_entry_t packets[MAX_PACKETS];
    int head;
    int tail;
    pthread_mutex_t lock;
} cp_buffer_t;

转发时的头部处理

Router 作为三层设备,在出端口转发前必须对二层和三层头部进行必要的修改与重封装:

  1. TTL 减 1:若 TTL 降为 0,则丢弃该包并返回 ICMP Time Exceeded(实验实现中可选择丢弃)。
  2. 重新计算 IP Header Checksum:因 TTL 字段变化,必须重新计算校验和。
  3. 重写以太网帧头:
    • src_mac 设为出端口(egress device)自身的 MAC 地址;
    • dst_mac 设为下一跳设备的 MAC 地址(通过 ARP 或静态映射获得)。

正因如此,net_device_t 中必须存储各网卡自身的 MAC 地址;若缺失,Router 将无法正确填写以太网帧的源地址,导致转发异常。

routing table 的互斥锁

lab 中没有,实际上需要添加。

目录
  • libpcap 库
    • 核心数据结构
    • 核心函数接口
    • 典型使用流程
  • lab 中的重要数据结构与函数
    • 协议头部
    • 网络设备有关数据结构
    • send_packet
    • capture_thread
    • common_init
  • Hub
  • Switch
  • Router
    • 路由表与距离向量
    • 转发时的头部处理
    • routing table 的互斥锁
© 2026 miniyuan. All rights reserved.
Go to miniyuan's GitHub repo