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

编写双线程以太网桥接代码:避免以太网帧无限循环的方法

解决双线程以太网桥的帧循环问题

这个问题我在做硬件中间人设备的时候踩过一模一样的坑!核心原因其实很简单:当网卡处于混杂模式时,它会捕获所有经过的帧——包括我们自己刚刚发送出去的帧。双线程并行运行时,线程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("正在关闭网桥...")

关键细节说明

  1. MAC过滤逻辑:这是解决循环的核心。当我们从ETH1转发帧到ETH2时,帧的源MAC是原始发送端的,不是ETH2的,所以ETH2的监听线程会正常转发;而如果是ETH2自己发送的帧(比如其他设备发过来的转发帧),源MAC是ETH2,会被过滤掉,不会回传给ETH1。
  2. 混杂模式:必须开启网卡的混杂模式才能捕获所有经过的帧,C代码里通过ioctl设置IFF_PROMISC,Scapy的sniff函数默认会自动开启。
  3. 权限要求:无论是C还是Python版本,都需要以root权限运行,因为原始套接字和混杂模式需要特权。
  4. 单线程 vs 双线程:单线程时循环不明显是因为串行处理的时序问题,但本质上还是存在丢帧或者潜在循环的可能,双线程并行才是更高效的网桥实现方式。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 06:57:00