编写双线程以太网桥接代码:避免以太网帧无限循环的方法
解决双线程以太网桥的帧循环问题
这个问题我在做硬件中间人设备的时候踩过一模一样的坑!核心原因其实很简单:当网卡处于混杂模式时,它会捕获所有经过的帧——包括我们自己刚刚发送出去的帧。双线程并行运行时,线程A把ETH1的帧写到ETH2,ETH2的监听线程会立刻把这个刚发出去的帧读回来,然后转发回ETH1,无限循环就这么产生了。单线程时因为是串行处理,这个问题不明显,但本质上还是存在的。
核心解决方案:过滤自身发送的帧
要打破循环,最直接的办法就是让每个转发线程忽略源MAC地址等于当前监听网卡MAC的帧——因为这类帧就是我们自己刚才发送出去的,完全不需要再转发。
下面我分别给出C(pthreads)和Python的可运行实现:
C语言(pthreads)实现
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> #include <sys/socket.h> #include <sys/ioctl.h> #include <net/if.h> #include <netinet/ether.h> #define BUF_SIZE 1518 typedef struct { char* src_iface; char* dst_iface; unsigned char src_mac[6]; int dst_sock; } ThreadData; // 获取网卡MAC地址 int get_mac_address(const char* iface, unsigned char mac[6]) { int sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock < 0) { perror("socket"); return -1; } struct ifreq ifr; strncpy(ifr.ifr_name, iface, IFNAMSIZ-1); if (ioctl(sock, SIOCGIFHWADDR, &ifr) < 0) { perror("ioctl"); close(sock); return -1; } memcpy(mac, ifr.ifr_hwaddr.sa_data, 6); close(sock); return 0; } // 创建原始套接字并绑定到网卡(开启混杂模式) int create_raw_socket(const char* iface) { int sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); if (sock < 0) { perror("socket"); return -1; } struct ifreq ifr; strncpy(ifr.ifr_name, iface, IFNAMSIZ-1); if (ioctl(sock, SIOCGIFINDEX, &ifr) < 0) { perror("ioctl"); close(sock); return -1; } struct sockaddr_ll sll; memset(&sll, 0, sizeof(sll)); sll.sll_family = AF_PACKET; sll.sll_ifindex = ifr.ifr_ifindex; sll.sll_protocol = htons(ETH_P_ALL); if (bind(sock, (struct sockaddr*)&sll, sizeof(sll)) < 0) { perror("bind"); close(sock); return -1; } // 开启混杂模式以捕获所有帧 if (ioctl(sock, SIOCGIFFLAGS, &ifr) < 0) { perror("ioctl get flags"); close(sock); return -1; } ifr.ifr_flags |= IFF_PROMISC; if (ioctl(sock, SIOCSIFFLAGS, &ifr) < 0) { perror("ioctl set promisc"); close(sock); return -1; } return sock; } // 转发线程函数 void* forward_frames(void* arg) { ThreadData* data = (ThreadData*)arg; int src_sock = create_raw_socket(data->src_iface); if (src_sock < 0) { pthread_exit(NULL); } unsigned char buf[BUF_SIZE]; while (1) { ssize_t len = recvfrom(src_sock, buf, BUF_SIZE, 0, NULL, NULL); if (len < 0) { perror("recvfrom"); break; } // 关键:过滤自身发送的帧(源MAC等于当前网卡MAC) struct ether_header* eth_hdr = (struct ether_header*)buf; if (memcmp(eth_hdr->ether_shost, data->src_mac, 6) == 0) { continue; } // 转发到目标网卡 ssize_t sent = sendto(data->dst_sock, buf, len, 0, NULL, 0); if (sent < 0) { perror("sendto"); } } close(src_sock); pthread_exit(NULL); } int main() { const char* eth1 = "eth1"; const char* eth2 = "eth2"; // 获取网卡MAC unsigned char eth1_mac[6], eth2_mac[6]; if (get_mac_address(eth1, eth1_mac) < 0 || get_mac_address(eth2, eth2_mac) < 0) { exit(EXIT_FAILURE); } // 创建发送用的套接字 int eth1_sock = create_raw_socket(eth1); int eth2_sock = create_raw_socket(eth2); if (eth1_sock < 0 || eth2_sock < 0) { exit(EXIT_FAILURE); } // 准备线程数据 ThreadData data1 = {.src_iface = (char*)eth1, .dst_iface = (char*)eth2, .dst_sock = eth2_sock}; memcpy(data1.src_mac, eth1_mac, 6); ThreadData data2 = {.src_iface = (char*)eth2, .dst_iface = (char*)eth1, .dst_sock = eth1_sock}; memcpy(data2.src_mac, eth2_mac, 6); // 创建并启动线程 pthread_t thread1, thread2; if (pthread_create(&thread1, NULL, forward_frames, &data1) != 0 || pthread_create(&thread2, NULL, forward_frames, &data2) != 0) { perror("pthread_create"); exit(EXIT_FAILURE); } // 等待线程退出 pthread_join(thread1, NULL); pthread_join(thread2, NULL); close(eth1_sock); close(eth2_sock); return 0; }
Python(threading + Scapy)实现
用Scapy可以更简洁地完成这个任务,不需要手动处理原始套接字:
from scapy.all import sniff, sendp, Ether import threading import netifaces def get_mac(iface): """获取指定网卡的MAC地址""" return netifaces.ifaddresses(iface)[netifaces.AF_LINK][0]['addr'] def forward_thread(src_iface, dst_iface, src_mac): """转发线程:捕获源网卡帧,过滤自身发送的,转发到目标网卡""" def packet_callback(pkt): # 跳过自身发送的帧 if pkt[Ether].src == src_mac: return # 静默转发,关闭verbose避免输出干扰 sendp(pkt, iface=dst_iface, verbose=0) # 启动捕获,store=0不存储数据包,节省内存 sniff(iface=src_iface, prn=packet_callback, store=0) if __name__ == "__main__": # 替换成你的网卡名称 ETH1 = "eth1" ETH2 = "eth2" # 获取网卡MAC eth1_mac = get_mac(ETH1) eth2_mac = get_mac(ETH2) # 创建并启动线程 t_eth1_to_eth2 = threading.Thread(target=forward_thread, args=(ETH1, ETH2, eth1_mac), daemon=True) t_eth2_to_eth1 = threading.Thread(target=forward_thread, args=(ETH2, ETH1, eth2_mac), daemon=True) t_eth1_to_eth2.start() t_eth2_to_eth1.start() # 主线程保持运行,等待用户中断 try: input("按Ctrl+C退出网桥程序...\n") except KeyboardInterrupt: print("正在关闭网桥...")
关键细节说明
- MAC过滤逻辑:这是解决循环的核心。当我们从ETH1转发帧到ETH2时,帧的源MAC是原始发送端的,不是ETH2的,所以ETH2的监听线程会正常转发;而如果是ETH2自己发送的帧(比如其他设备发过来的转发帧),源MAC是ETH2,会被过滤掉,不会回传给ETH1。
- 混杂模式:必须开启网卡的混杂模式才能捕获所有经过的帧,C代码里通过
ioctl设置IFF_PROMISC,Scapy的sniff函数默认会自动开启。 - 权限要求:无论是C还是Python版本,都需要以root权限运行,因为原始套接字和混杂模式需要特权。
- 单线程 vs 双线程:单线程时循环不明显是因为串行处理的时序问题,但本质上还是存在丢帧或者潜在循环的可能,双线程并行才是更高效的网桥实现方式。
内容的提问来源于stack exchange,提问作者Gabe
相关产品推荐
相关产品推荐

