9

我编写了一个 C 程序,该程序将以太网帧直接写入线路(以两种模式运行,发送方或接收方)。发送方正在发送带有两个 VLAN 标签的帧(QinQ),但奇怪的是,当帧到达接收方时,以太网类型已更改为标准(单个)VLAN 封装帧的类型。网卡有可能这样做,还是 Linux 不允许这样做?Wireshark 显示与 tcpdump 相同的行为。

为了解释下图,发送方正在向以太网广播地址 FF:FF:FF:FF:FF:FF 发送帧以找到接收方(这是通过交叉电缆连接的两台测试机,但下面的结果与交换机或集线器)。如您所见,帧上带有两个 VLAN 标记,外部标记的以太网类型为 0x8100,VLAN ID 为 40,内部 VLAN 的以太网类型为 0x8100,VLAN ID 为 20。众所周知,对于 QinQ 框架,外框架的 ethertype 应为 0x88a8!

当帧从我的应用程序中的发送方发送时,它们的外部以太网类型为 0x88a8,但根据下图,它们在内部和外部以太网类型上均以 0x8100 接收。突出显示的文本是接收方发回的回复,您可以看到帧的外框为 0x88a8,内框为 0x8100。另一台机器上的 tcpdump 显示相同(它是相同的代码!帧以 0x88a8 外部 0x8100 内部发送,但始终作为 0x8100 外部和 0x8100 内部接收)。

在此处输入图像描述

void BuildHeaders(char* &txBuffer, unsigned char (&destMAC)[6], 
     unsigned char (&sourceMAC)[6], short &PCP, short &vlanID,
     short &qinqID, short &qinqPCP, int &headersLength)
{

int offset = 0;

short TPI = 0;
short TCI = 0;
short *p = &TPI;
short *c = &TCI;
short vlanIDtemp;

// Copy the destination and source MAC addresses
memcpy((void*)txBuffer, (void*)destMAC, ETH_ALEN);
memcpy((void*)(txBuffer+ETH_ALEN), (void*)sourceMAC, ETH_ALEN);
offset = (ETH_ALEN*2);

// Add on the QinQ Tag Protocol Identifier
vlanIDtemp = qinq
TPI = htons(0x88a8); //0x88a8 == IEEE802.1ad, 0x9100 == older IEEE802.1QinQ
memcpy((void*)(txBuffer+offset), p, 2);
offset+=2;

// Now build the QinQ Tag Control Identifier:
TCI = (qinqPCP & 0x07) << 5;
qinqID = qinqID >> 8;
TCI = TCI | (qinqID & 0x0f);
qinqID = vlanIDtemp;
qinqID = qinqID << 8;
TCI = TCI | (qinqID & 0xffff);

memcpy((void*)(txBuffer+offset), c, 2);
offset+=2;

// VLAN headers
vlanIDtemp = vlanID;
TPI = htons(0x8100);
memcpy((void*)(txBuffer+offset), p, 2);
offset+=2;

TCI = (PCP & 0x07) << 5;
vlanID = vlanID >> 8;
TCI = TCI | (vlanID & 0x0f);
vlanID = vlanIDtemp;
vlanID = vlanID << 8;
TCI = TCI | (vlanID & 0xffff);

memcpy((void*)(txBuffer+offset), c, 2);
offset+=2;

// Push on the Ethertype (IPv4) for the payload
TPI = htons(0x0800);
memcpy((void*)(txBuffer+offset), p, 2);
offset+=2;

headersLength = offset;

}

sendResult = sendto(sockFD, txBuffer, fSizeTotal, 0, (struct sockaddr*)&socket_address, sizeof(socket_address));
4

1 回答 1

14

(完全重写以简化答案。我还在下面列出的 C 头文件和源文件中修复了很多错误。)

在 2014 年 4 月的linux-netdev 邮件列表上对此进行了讨论,主题为“802.1AD 数据包 - 内核将所有数据包上的以太类型从 88A8 更改为 8100”

事实证明,内核并没有改变以太类型,它只是在接收数据包时消耗它。我在下面展示了它正确地用于 VLAN 路由(包括 802.1AD 和 802.1Q VLAN 的单独规则),给定足够新的内核。即使 VLAN 标记不用于路由(例如,如果没有配置 VLAN,或者没有加载 8021q 内核模块),内核也会使用 VLAN 标记。

因此,最初的问题“当发送以太网帧时,以太网类型正在被重写”,是不正确的:以太网类型没有被重写。它由内核消耗

因为内核使用了 VLAN 标记,所以 libpcap 是 tcpdump、wireshark 等人使用的数据包捕获库。-- 尝试将其重新引入数据包标头。不幸的是,它总是使用 802.1Q VLAN 标头 (8100)。

建议对 libpcap 进行更改,以准确修复 libpcap 中的这个问题,但在撰写本文时,它似乎还没有被包含在内;您仍然可以在Linux 的 libpcap 源文件的htons(ETH_P_8021Q)多个位置看到硬编码。


我不能假设你会相信我的话,所以让我告诉你如何自己确定这一点。

让我们编写一个简单的数据包发送器和接收器,它直接使用内核接口,无需 libpcap 的帮助。

原始数据包.h:

#ifndef   RAWPACKET_H
#define   RAWPACKET_H
#include <unistd.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <netpacket/packet.h>
#include <net/ethernet.h>
#include <net/if.h>
#include <arpa/inet.h>
#include <linux/if_ether.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>

static int rawpacket_socket(const int protocol,
                            const char *const interface,
                            void *const hwaddr)
{
    struct ifreq        iface;
    struct sockaddr_ll  addr;
    int                 socketfd, result;
    int                 ifindex = 0;

    if (!interface || !*interface) {
        errno = EINVAL;
        return -1;
    }

    socketfd = socket(AF_PACKET, SOCK_RAW, htons(protocol));
    if (socketfd == -1)
        return -1;

    do {

        memset(&iface, 0, sizeof iface);
        strncpy((char *)&iface.ifr_name, interface, IFNAMSIZ);
        result = ioctl(socketfd, SIOCGIFINDEX, &iface);
        if (result == -1)
            break;
        ifindex = iface.ifr_ifindex;

        memset(&iface, 0, sizeof iface);
        strncpy((char *)&iface.ifr_name, interface, IFNAMSIZ);
        result = ioctl(socketfd, SIOCGIFFLAGS, &iface);
        if (result == -1)
            break;
        iface.ifr_flags |= IFF_PROMISC;
        result = ioctl(socketfd, SIOCSIFFLAGS, &iface);
        if (result == -1)
            break;

        memset(&iface, 0, sizeof iface);
        strncpy((char *)&iface.ifr_name, interface, IFNAMSIZ);
        result = ioctl(socketfd, SIOCGIFHWADDR, &iface);
        if (result == -1)
            break;

        memset(&addr, 0, sizeof addr);
        addr.sll_family = AF_PACKET;
        addr.sll_protocol = htons(protocol);
        addr.sll_ifindex = ifindex;
        addr.sll_hatype = 0;
        addr.sll_pkttype = 0;
        addr.sll_halen = ETH_ALEN; /* Assume ethernet! */
        memcpy(&addr.sll_addr, &iface.ifr_hwaddr.sa_data, addr.sll_halen);
        if (hwaddr)
            memcpy(hwaddr, &iface.ifr_hwaddr.sa_data, ETH_ALEN);

        if (bind(socketfd, (struct sockaddr *)&addr, sizeof addr))
            break;

        errno = 0;
        return socketfd;

    } while (0);

    {
        const int saved_errno = errno;
        close(socketfd);
        errno = saved_errno;
        return -1;
    }
}

static unsigned int tci(const unsigned int priority,
                        const unsigned int drop,
                        const unsigned int vlan)
{
    return (vlan & 0xFFFU)
         | ((!!drop) << 12U)
         | ((priority & 7U) << 13U);
}

static size_t rawpacket_qinq(unsigned char *const buffer, size_t const length,
                             const unsigned char *const srcaddr,
                             const unsigned char *const dstaddr,
                             const unsigned int service_tci,
                             const unsigned int customer_tci,
                             const unsigned int ethertype)
{
    unsigned char *ptr = buffer;
    uint32_t       tag;
    uint16_t       type;

    if (length < 2 * ETH_ALEN + 4 + 4 + 2) {
        errno = ENOSPC;
        return (size_t)0;
    }

    memcpy(ptr, dstaddr, ETH_ALEN);
    ptr += ETH_ALEN;

    memcpy(ptr, srcaddr, ETH_ALEN);
    ptr += ETH_ALEN;

    /* Service 802.1AD tag. */
    tag = htonl( ((uint32_t)(ETH_P_8021AD) << 16U)
               | ((uint32_t)service_tci & 0xFFFFU) );
    memcpy(ptr, &tag, 4);
    ptr += 4;

    /* Client 802.1Q tag. */
    tag = htonl( ((uint32_t)(ETH_P_8021Q) << 16U)
               | ((uint32_t)customer_tci & 0xFFFFU) );
    memcpy(ptr, &tag, 4);
    ptr += 4;

    /* Ethertype tag. */
    type = htons((uint16_t)ethertype);
    memcpy(ptr, &type, 2);
    ptr += 2;

    return (size_t)(ptr - buffer);
}

#endif /* RAWPACKET_H */

发件人.c:

#include <string.h>
#include <errno.h>
#include <stdio.h>
#include "rawpacket.h"

static size_t parse_data(unsigned char *const data, const size_t size,
                         const char *const string)
{
    char *ends = strncpy((char *)data, string, size);
    return (size_t)(ends - (char *)data);
}


static int parse_hwaddr(const char *const string,
                        void *const hwaddr)
{
    unsigned int addr[6];
    char         dummy;

    if (sscanf(string, " %02x:%02x:%02x:%02x:%02x:%02x %c",
                       &addr[0], &addr[1], &addr[2],
                       &addr[3], &addr[4], &addr[5],
                       &dummy) == 6 ||
        sscanf(string, " %02x%02x%02x%02x%02x%02x %c",
                       &addr[0], &addr[1], &addr[2],
                       &addr[3], &addr[4], &addr[5],
                       &dummy) == 6) {
        if (hwaddr) {
            ((unsigned char *)hwaddr)[0] = addr[0];
            ((unsigned char *)hwaddr)[1] = addr[1];
            ((unsigned char *)hwaddr)[2] = addr[2];
            ((unsigned char *)hwaddr)[3] = addr[3];
            ((unsigned char *)hwaddr)[4] = addr[4];
            ((unsigned char *)hwaddr)[5] = addr[5];
        }
        return 0;
    }

    errno = EINVAL;
    return -1;
}

int main(int argc, char *argv[])
{
    unsigned char packet[ETH_FRAME_LEN + ETH_FCS_LEN];
    unsigned char srcaddr[6], dstaddr[6];
    int           socketfd;
    size_t        size, i;
    ssize_t       n;

    if (argc < 3 || argc > 4 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
        fprintf(stderr, "\n");
        fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
        fprintf(stderr, "       %s interface hwaddr [message]\n", argv[0]);
        fprintf(stderr, "\n");
        return 1;
    }

    if (parse_hwaddr(argv[2], &dstaddr)) {
        fprintf(stderr, "%s: Invalid destination hardware address.\n", argv[2]);
        return 1;
    }

    socketfd = rawpacket_socket(ETH_P_ALL, argv[1], &srcaddr);
    if (socketfd == -1) {
        fprintf(stderr, "%s: %s.\n", argv[1], strerror(errno));
        return 1;
    }

    memset(packet, 0, sizeof packet);

    /* Construct a QinQ header for a fake Ethernet packet type. */
    size = rawpacket_qinq(packet, sizeof packet, srcaddr, dstaddr,
                                  tci(7, 0, 1U), tci(7, 0, 2U),
                                  ETH_P_IP);
    if (!size) {
        fprintf(stderr, "Failed to construct QinQ headers: %s.\n", strerror(errno));
        close(socketfd);
        return 1;
    }

    /* Add packet payload. */
    if (argc > 3)
        size += parse_data(packet + size, sizeof packet - size, argv[3]);
    else
        size += parse_data(packet + size, sizeof packet - size, "Hello!");

    /* Pad with zeroes to minimum 64 octet length. */
    if (size < 64)
        size = 64;

    /* Send it. */
    n = send(socketfd, packet, size, 0);
    if (n == -1) {
        fprintf(stderr, "Failed to send packet: %s.\n", strerror(errno));
        shutdown(socketfd, SHUT_RDWR);
        close(socketfd);
        return 1;
    }

    fprintf(stderr, "Sent %ld bytes:", (long)n);
    for (i = 0; i < size; i++)
        fprintf(stderr, " %02x", packet[i]);
    fprintf(stderr, "\n");
    fflush(stderr);

    shutdown(socketfd, SHUT_RDWR);
    if (close(socketfd)) {
        fprintf(stderr, "Error closing socket: %s.\n", strerror(errno));
        return 1;
    }

    return 0;
}

接收器.c:

#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <stdio.h>
#include "rawpacket.h"

static volatile sig_atomic_t  done = 0;

static void handle_done(int signum)
{
    done = signum;
}

static int install_done(const int signum)
{
    struct sigaction act;
    sigemptyset(&act.sa_mask);
    act.sa_handler = handle_done;
    act.sa_flags = 0;
    if (sigaction(signum, &act, NULL))
        return errno;
    return 0;
}

static const char *protocol_name(const unsigned int protocol)
{
    static char buffer[16];
    switch (protocol & 0xFFFFU) {
    case 0x0001: return "ETH_P_802_3";
    case 0x0002: return "ETH_P_AX25";
    case 0x0003: return "ETH_P_ALL";
    case 0x0060: return "ETH_P_LOOP";
    case 0x0800: return "ETH_P_IP";
    case 0x0806: return "ETH_P_ARP";
    case 0x8100: return "ETH_P_8021Q (802.1Q VLAN)";
    case 0x88A8: return "ETH_P_8021AD (802.1AD VLAN)";
    default:
        snprintf(buffer, sizeof buffer, "0x%04x", protocol & 0xFFFFU);
        return (const char *)buffer;
    }
}

static const char *header_type(const unsigned int hatype)
{
    static char buffer[16];
    switch (hatype) {
    case   1: return "ARPHRD_ETHER: Ethernet 10Mbps";
    case   2: return "ARPHRD_EETHER: Experimental Ethernet";
    case 768: return "ARPHRD_TUNNEL: IP Tunnel";
    case 772: return "ARPHRD_LOOP: Loopback";
    default:
        snprintf(buffer, sizeof buffer, "0x%04x", hatype);
        return buffer;
    }
}

static const char *packet_type(const unsigned int pkttype)
{
    static char buffer[16];
    switch (pkttype) {
    case PACKET_HOST:      return "PACKET_HOST";
    case PACKET_BROADCAST: return "PACKET_BROADCAST";
    case PACKET_MULTICAST: return "PACKET_MULTICAST";
    case PACKET_OTHERHOST: return "PACKET_OTHERHOST";
    case PACKET_OUTGOING:  return "PACKET_OUTGOING";
    default:
        snprintf(buffer, sizeof buffer, "0x%02x", pkttype);
        return (const char *)buffer;
    }
}

static void fhex(FILE *const out,
                 const char *const before,
                 const char *const after,
                 const void *const src, const size_t len)
{
    const unsigned char *const data = src;
    size_t i;

    if (len < 1)
        return;

    if (before)
        fputs(before, out);

    for (i = 0; i < len; i++)
        fprintf(out, " %02x", data[i]);

    if (after)
        fputs(after, out);
}

int main(int argc, char *argv[])
{
    struct sockaddr_ll  addr;
    socklen_t           addrlen;
    unsigned char       data[2048];
    ssize_t             n;
    int                 socketfd, flag;

    if (argc != 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
        fprintf(stderr, "\n");
        fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
        fprintf(stderr, "       %s interface\n", argv[0]);
        fprintf(stderr, "\n");
        return 1;
    }

    if (install_done(SIGINT) ||
        install_done(SIGHUP) ||
        install_done(SIGTERM)) {
        fprintf(stderr, "Cannot install signal handlers: %s.\n", strerror(errno));
        return 1;
    }

    socketfd = rawpacket_socket(ETH_P_ALL, argv[1], NULL);
    if (socketfd == -1) {
        fprintf(stderr, "%s: %s.\n", argv[1], strerror(errno));
        return 1;
    }

    flag = 1;
    if (setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof flag)) {
        fprintf(stderr, "Cannot set REUSEADDR socket option: %s.\n", strerror(errno));
        close(socketfd);
        return 1;
    }

    if (setsockopt(socketfd, SOL_SOCKET, SO_BINDTODEVICE, argv[1], strlen(argv[1]) + 1)) {
        fprintf(stderr, "Cannot bind to device %s: %s.\n", argv[1], strerror(errno));
        close(socketfd);
        return 1;
    }

    while (!done) {

        memset(data, 0, sizeof data);
        memset(&addr, 0, sizeof addr);
        addrlen = sizeof addr;
        n = recvfrom(socketfd, &data, sizeof data, 0,
                     (struct sockaddr *)&addr, &addrlen);
        if (n == -1) {
            if (errno == EINTR)
                continue;
            fprintf(stderr, "Receive error: %s.\n", strerror(errno));
            break;
        }

        printf("Received %d bytes:\n", (int)n);
        printf("\t    Protocol: %s\n", protocol_name(htons(addr.sll_protocol)));
        printf("\t   Interface: %d\n", (int)addr.sll_ifindex);
        printf("\t Header type: %s\n", header_type(addr.sll_hatype));
        printf("\t Packet type: %s\n", packet_type(addr.sll_pkttype));
        fhex(stdout, "\t     Address:", "\n", addr.sll_addr, addr.sll_halen);
        fhex(stdout, "\t        Data:", "\n", data, n);
        printf("\n");

        fflush(stdout);
    }

    shutdown(socketfd, SHUT_RDWR);
    close(socketfd);
    return 0;
}

要编译,您可以使用

gcc -O2 receiver.c -o receiver
gcc -O2 sender.c -o sender

不带参数或带参数运行-h以查看任一参数的用法。sender只发送一个数据包。receiver侦听指定的接口(在混杂模式下),直到您中断它(Ctrl+ C)或向它发送TERM信号。

在环回接口的一个虚拟终端中启动接收器:

sudo ./receiver lo

在同一台机器上的另一个虚拟终端中,运行

sudo ./sender lo FF:FF:FF:FF:FF:FF '_The contents of a 64-byte Ethernet frame_'

将输出(为便于理解添加了换行符和缩进)

Sent 64 bytes: ff ff ff ff ff ff
               00 00 00 00 00 00
               88 a8 e0 01
               81 00 e0 02
               08 00
               5f 54 68 65 20 63 6f 6e
               74 65 6e 74 73 20 6f 66
               20 61 20 36 34 2d 62 79
               74 65 20 45 74 68 65 72
               6e 65 74 20 66 72 61 6d
               65 5f

然而,在接收器终端中,我们看到(添加了换行符和缩进):

Received 64 bytes:
    Protocol: ETH_P_ALL
   Interface: 1
 Header type: ATPHRD_LOOP: Loopback
 Packet type: PACKET_OUTGOING
     Address: 00 00 00 00 00 00
        Data: ff ff ff ff ff ff
              00 00 00 00 00 00
              88 a8 e0 01
              81 00 e0 02
              08 00
              5f 54 68 65 20 63 6f 6e
              74 65 6e 74 73 20 6f 66
              20 61 20 36 34 2d 62 79
              74 65 20 45 74 68 65 72
              6e 65 74 20 66 72 61 6d
              65 5f

Received 60 bytes:
    Protocol: ETH_P_8021Q (802.1Q VLAN)
   Interface: 1
 Header type: ATPHRD_LOOP: Loopback
 Packet type: PACKET_MULTICAST
     Address: 00 00 00 00 00 00
        Data: ff ff ff ff ff ff
              00 00 00 00 00 00
              81 00 e0 02
              08 00
              5f 54 68 65 20 63 6f 6e
              74 65 6e 74 73 20 6f 66
              20 61 20 36 34 2d 62 79
              74 65 20 45 74 68 65 72
              6e 65 74 20 66 72 61 6d
              65 5f

第一个,PACKET_OUTGOING,被捕获为传出;它表明内核在发送数据包时没有消耗任何标头。

第二个 PACKET_MULTICAST 在到达时被捕获。(由于以太网地址为 FF:FF:FF:FF:FF:FF,因此它是一个多播数据包。)

如您所见,后一个数据包只有 802.1Q VLAN 头——客户端 VLAN——,内核已经消耗了 802.1AD 服务 VLAN 标记。

以上至少证实了环回接口的场景。使用原始数据包接口,内核使用 802.1AD VLAN 标头(紧跟在接收地址之后的标头)。如果您tcpdump -i eth0与接收器一起使用,您可以看到 libpcap 正在将使用的标头重新插入到数据包中!

Loopback 接口有点特殊,我们用虚拟机重做测试。我碰巧正在运行最新的 Xubuntu 14.04(自 2014 年 6 月 28 日起安装的所有更新,Ubuntu 3.13.0-29-generic #53 x86_64 内核)。发送方硬件地址为 08 00 00 00 00 02,接收方为 08 00 00 00 00 01,两者连接到内部网络,没有其他人在场。

(再次,我在输出中添加换行符和缩进以使其更易于阅读。)

发件人,在虚拟机 2 上:

sudo ./sender eth0 08:00:00:00:00:01 '_The contents of a 64-byte Ethernet frame_'

Sent 64 bytes: 08 00 00 00 00 01
               08 00 00 00 00 02
               88 a8 e0 01
               81 00 e0 02
               08 00
               5f 54 68 65 20 63 6f 6e
               74 65 6e 74 73 20 6f 66
               20 61 20 36 34 2d 62 79
               74 65 20 45 74 68 65 72
               6e 65 74 20 66 72 61 6d
               65 5f

接收方,在虚拟机 1 上:

sudo ./receiver eth0

Received 60 bytes:
    Protocol: ETH_P_8021Q (802.1Q VLAN)
   Interface: 2
 Header type: ARPHRD_ETHER: Ethernet 10Mbps
 Packet type: PACKET_HOST
     Address: 08 00 00 00 00 02
        Data: 08 00 00 00 00 01
              08 00 00 00 00 02
              81 00 e0 02
              08 00
              5f 54 68 65 20 63 6f 6e
              74 65 6e 74 73 20 6f 66
              20 61 20 36 34 2d 62 79
              74 65 20 45 74 68 65 72
              6e 65 74 20 66 72 61 6d
              65 5f

如您所见,结果与环回情况基本相同。特别是,802.1AD 服务 VLAN 标记在接收时被消耗。(可以使用 tcpdump 或 wireshark 来比较接收到的数据包:libpcap 显然是在将消费的 VLAN 标记包重新插入数据包中。)

如果您有足够新的内核( 2013 年 4 月添加了支持),那么您可以使用以下命令在接收方上配置 802.1AD VLAN:

sudo modprobe 8021q

sudo ip link add link eth0 eth0.service1 type vlan proto 802.1ad id 1

接收eth0将接收所有数据包,但eth0.service1仅接收那些带有 802.1AD VLAN 标记、VLAN ID 为 1 的数据包。它不会捕获具有相同 VLAN ID 的 802.1Q VLAN 标记的帧,这意味着您可以在接收时执行完整路由802.1AD 和 802.1Q VLAN。

我自己不仅仅相信上述测试。我创建了许多 802.1AD 和 802.1Q VLAN,每个 VLAN 上都有单独的实例,并更改了sender.c中调用中receive的数据包头(不仅是服务(第一个)tci()和客户端(第二个))以更改服务和客户端 VLAN ID,但也更改rawpacket.h,以验证 802.1AD (88a8) 和 802.1Q (8100) VLAN 标头在接收时正确路由)。一切都很顺利,没有任何问题。tci()rawpacket_qinq()

总之:

鉴于最新的 Linux 内核版本,以太网帧在接收时由 Linux 内核(通过 8021q 模块)正确路由,包括具有相同 VLAN ID 的 802.1AD 和 802.1Q 的单独 VLAN 接口。即使没有配置 VLAN,内核也会使用用于路由的 VLAN 标头。

问题?

于 2014-06-24T22:47:39.800 回答