(完全重写以简化答案。我还在下面列出的 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 标头。
问题?