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

基于Linux TAP设备的自定义网络栈:L2帧收发故障排查

问题描述

我出于个人研究目的搭建自定义网络栈,计划借助Linux TAP设备与raw socket实现简易L2层,构建可进行L2点到点通信的节点。

目前已用C代码创建两个TAP设备,通过Wireshark可看到数据包从其中一个设备发出,但将以太网头部的MAC地址设置为接收端TAP设备的MAC后,始终无法在其他位置接收到数据包。

核心需求:由于节点运行在独立进程中无法直接获取文件描述符,因此尝试通过raw socket实现不同TAP设备间的通信。

曾尝试创建网桥连接两个TAP设备但数据包仍无法正常传输,请问是否必须使用网桥?若需要该如何正确配置以确保数据包通过网桥到达目标TAP设备?

以下是创建TAP设备与raw socket的代码:

/**
* Creates and Ups tap intf, creates a raw socket, binds to it
*/
tap_conn_t* start_tap_sock(char* dev_name) 
{
    int tap_fd = tun_alloc(dev_name);
    int raw_fd = socket(AF_PACKET, SOCK_RAW, IPPROTO_RAW);
    if (raw_fd < 0) {
        printf("ERROR CREATING TAP SOCKET, ERRNO: %d\n", errno);
        close(tap_fd);
        return NULL;
    }

    // set the device up by adding IFF_UP flag and make requests 
    // to get if_index and hwaddr
    add_ifflags(raw_fd, dev_name, IFF_UP);
    struct ifreq hwaddr = make_ifreq(raw_fd, dev_name, SIOCGIFHWADDR);
    struct ifreq if_index = make_ifreq(raw_fd, dev_name, SIOCGIFINDEX);

    // create sockaddr_ll and bind tap_conn to it
    struct sockaddr_ll sl;
    sl.sll_family = AF_PACKET;
    sl.sll_protocol = htons(ETH_P_ALL);
    sl.sll_ifindex = if_index.ifr_ifindex;
    sl.sll_halen = ETH_ALEN; // len of hwaddr (MAC addr)
    memcpy(&sl.sll_addr, &hwaddr.ifr_hwaddr, ETH_ALEN);
    int slen = sizeof(struct sockaddr_ll);
    if (bind(raw_fd, (const struct sockaddr*) &sl, (socklen_t) slen) < 0) {
        printf("ERROR TRYING TO BIND SOCKET, ERRNO: %d\n", errno);
        close(tap_fd);
        close(raw_fd);
        return NULL;
    }

    // malloc struct and return
        ...
    return tc;
}

发送数据包的代码:

int send_to_tap(char* remote_name, char* data, int len, tap_conn_t* tc) 
{
    // make ethernet header first
    struct ifreq remote_hwaddr = make_ifreq(tc->raw_sock_fd, remote_name, SIOCGIFHWADDR);
    struct ethhdr ehdr = make_eth(tc->hwaddr, remote_hwaddr.ifr_hwaddr);

    // put header and data into buff
    char send_buf[sizeof(struct ethhdr) + len];
    memcpy(send_buf, &ehdr, sizeof(struct ethhdr));
    memcpy(send_buf + sizeof(struct ethhdr), data, len);
    
    // make sockaddr_ll and send
    struct sockaddr_ll sl;
    sl.sll_family = AF_PACKET;
    sl.sll_ifindex = tc->if_index;
    sl.sll_halen = ETH_ALEN;
    memcpy(&sl.sll_addr, &tc->hwaddr, ETH_ALEN);
    
    int sl_len = sizeof(struct sockaddr_ll);
    int send_len = sendto(tc->raw_sock_fd, (void *) send_buf, sizeof(struct ethhdr) + len, 0, (const struct sockaddr *) &sl, (socklen_t) sl_len);
    if (send_len < 0) {
        printf("ERROR SENDING, ERRNO: %d\n", errno);
        return -1;
    }

    return send_len;
}

另外,了解到veth接口可能适用于该场景,但不清楚如何用C语言创建,是否只能通过bash的ip link命令实现?最终也可考虑使用pcap,但希望先解决当前方案的问题。


问题分析与解决方案

一、当前代码的核心问题

你的发送代码中struct sockaddr_ll的配置存在错误:

  • sl.sll_addr应填写目标设备的MAC地址,而非本地TAP设备的MAC。当前代码将本地MAC填入,内核无法识别数据包的转发目标,导致数据包无法送达接收端。

修改send_to_tap中的sockaddr_ll初始化部分:

struct sockaddr_ll sl;
sl.sll_family = AF_PACKET;
sl.sll_ifindex = tc->if_index;
sl.sll_halen = ETH_ALEN;
// 替换为目标设备的MAC地址
memcpy(&sl.sll_addr, &remote_hwaddr.ifr_hwaddr.sa_data, ETH_ALEN);

同时需确认make_eth函数是否正确设置以太网头部:

  • 目标MAC为接收端TAP设备的MAC
  • 源MAC为本地TAP设备的MAC
  • h_proto字段需设置为合法值(如htons(ETH_P_IP)或自定义协议号)

二、是否必须使用网桥?

不是必须,但网桥是Linux下让两个TAP设备处于同一L2广播域的标准方案。若不用网桥,需自行在用户态实现L2转发逻辑,效率远低于内核网桥。

正确配置网桥的步骤

  1. 创建并启用网桥:
ip link add name br0 type bridge
ip link set br0 up
  1. 将两个TAP设备加入网桥:
ip link set tap0 master br0
ip link set tap1 master br0
  1. 确保TAP设备处于启用状态:
ip link set tap0 up
ip link set tap1 up

之前网桥配置失效的可能原因

  • TAP设备未正确加入网桥:用brctl show br0确认端口列表是否包含两个TAP设备
  • 网桥或TAP设备未启用UP状态
  • 数据包以太网类型不被网桥转发:确保h_proto为标准协议或未被过滤
  • 内核开启反向路径过滤:临时关闭测试
sysctl -w net.ipv4.conf.all.rp_filter=0
sysctl -w net.ipv4.conf.br0.rp_filter=0
sysctl -w net.ipv4.conf.tap0.rp_filter=0
sysctl -w net.ipv4.conf.tap1.rp_filter=0

三、用C语言创建veth接口

veth接口可通过C语言调用netlink接口创建,无需依赖bash命令。以下是简化实现示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>

static int netlink_veth_create(const char *name1, const char *name2) {
    int sockfd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
    if (sockfd < 0) {
        perror("socket");
        return -1;
    }

    struct sockaddr_nl sa;
    memset(&sa, 0, sizeof(sa));
    sa.nl_family = AF_NETLINK;
    sa.nl_groups = 0;

    if (bind(sockfd, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
        perror("bind");
        close(sockfd);
        return -1;
    }

    char buf[4096];
    struct nlmsghdr *nlh = (struct nlmsghdr *)buf;
    struct ifinfomsg *ifm = (struct ifinfomsg *)(nlh + 1);
    struct rtattr *rta = (struct rtattr *)(ifm + 1);

    nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*ifm)) + RTA_LENGTH(strlen(name1)) + RTA_LENGTH(strlen(name2)) + sizeof(struct rtattr);
    nlh->nlmsg_type = RTM_NEWLINK;
    nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_EXCL;
    nlh->nlmsg_seq = 1;
    nlh->nlmsg_pid = getpid();

    ifm->ifi_family = AF_UNSPEC;
    ifm->ifi_type = ARPHRD_ETHER;
    ifm->ifi_flags = IFF_UP | IFF_RUNNING | IFF_VETH;
    ifm->ifi_change = 0xFFFFFFFF;

    // 设置第一个veth名称
    rta->rta_type = IFLA_IFNAME;
    rta->rta_len = RTA_LENGTH(strlen(name1));
    strcpy(RTA_DATA(rta), name1);
    rta = RTA_NEXT(rta, nlh->nlmsg_len);

    // 设置第二个veth的peer名称
    rta->rta_type = IFLA_LINKINFO;
    rta->rta_len = RTA_LENGTH(sizeof(struct rtattr));
    struct rtattr *peer_rta = (struct rtattr *)RTA_DATA(rta);
    peer_rta->rta_type = IFLA_VETH_PEER;
    peer_rta->rta_len = RTA_LENGTH(strlen(name2));
    strcpy(RTA_DATA(peer_rta), name2);

    struct sockaddr_nl dest_addr;
    memset(&dest_addr, 0, sizeof(dest_addr));
    dest_addr.nl_family = AF_NETLINK;

    if (sendto(sockfd, buf, nlh->nlmsg_len, 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr)) < 0) {
        perror("sendto");
        close(sockfd);
        return -1;
    }

    // 接收内核响应
    int len = recv(sockfd, buf, sizeof(buf), 0);
    if (len < 0) {
        perror("recv");
        close(sockfd);
        return -1;
    }

    close(sockfd);
    return 0;
}

int main() {
    if (netlink_veth_create("veth0", "veth1") != 0) {
        fprintf(stderr, "Failed to create veth pair\n");
        return 1;
    }
    printf("Veth pair created: veth0 <-> veth1\n");
    return 0;
}

编译时需链接-lrt,运行需root权限。

四、额外调试建议

  • 用tcpdump在TAP设备和网桥上抓包,确认数据包流向:
tcpdump -i tap0 -e
tcpdump -i br0 -e
  • 查看内核日志(dmesg)是否有数据包丢弃的相关信息
  • 若raw socket接收端无响应,确认绑定的ETH_P_ALL是否正确,或尝试绑定到具体协议号

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 07:02:02