基于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转发逻辑,效率远低于内核网桥。
正确配置网桥的步骤
- 创建并启用网桥:
ip link add name br0 type bridge ip link set br0 up
- 将两个TAP设备加入网桥:
ip link set tap0 master br0 ip link set tap1 master br0
- 确保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
相关产品推荐
相关产品推荐

