Linux: при отправке кадров Ethernet ethertype перезаписывается
Я написал программу на C, которая записывает кадры Ethernet непосредственно на провод (который работает в двух режимах, отправителем или получателем). отправитель отправляет кадры с двумя тегами VLAN на них (QinQ), но странно, когда кадр достигает приемника, ethertype изменился на стандартный (одиночный) VLAN инкапсулированный кадр. Возможно ли, что NIC делает это, или Linux не позволяет этого? Wireshark показывает то же поведение, что и tcpdump.
объяснить изображение ниже отправителя отправляет кадры на широковещательный адрес Ethernet FF:FF:FF:FF:FF: FF, чтобы найти приемник (это две тестовые машины, подключенные через перекрестный кабель, но результат ниже совпадает с коммутатором или концентратором). Как вы можете видеть, кадры входят с двумя тегами VLAN на них, внешний тег имеет ethertype 0x8100 и идентификатор VLAN 40, внутренний VLAN имеет ethertype 0x8100 и идентификатор VLAN 20. Как мы все знаем, однако, с кадрами QinQ внешняя рамка должна иметь ethertype из 0x88a8!
когда кадры отправляются от отправителя в моем приложении, у них есть внешний ethertype 0x88a8, но согласно изображению ниже они получены с 0x8100 как на внутреннем, так и на внешнем ethertypes. Выделенный текст является получателем, отправляющим ответ, так как вы можете видеть, что кадры имеют 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));
1 ответов
(полностью переписан, чтобы упростить ответ. Я также исправил довольно много ошибок в моем заголовке C и исходных файлах, перечисленных ниже.)
была дискуссия именно это на список рассылки linux-netdev в апреле 2014 года тема "802.1 AD packets-ядро меняет тип эфира с 88A8 на 8100 на всех пакетах".
получается, что ядро не изменяет тип эфира, оно просто потребляет его при получении пакет. Я показываю ниже, что он правильно используется для маршрутизации VLAN (включая отдельные правила для 802.1 AD и 802.1 Q VLANs), учитывая достаточно недавнее ядро. Даже если тег VLAN не используется для маршрутизации (скажем, если нет настроенных VLAN или если модуль ядра 8021q не загружен), тег VLAN потребляется ядром.
таким образом, первоначальный вопрос, "при отправке кадров Ethernet ethertype переписывается", является неверным: ethertype не является переписывается. Это потребляемая ядра.
поскольку тег VLAN потребляется ядром, libpcap -- который является библиотекой захвата пакетов, используемой tcpdump, wireshark и др. -- пытается вернуть его обратно в заголовки пакетов. К сожалению, это всегда использует заголовок 802.1 Q VLAN (8100).
есть предложил изменить в libpcap, который исправляет именно эту проблему в libpcap, но с этого момента письмо, похоже, еще не включено; вы все еще можете видеть htons(ETH_P_8021Q)
hardcoded в нескольких местах в исходный файл libpcap для Linux.
давайте напишем простой отправитель и получатель пакетов, который использует интерфейсы ядра напрямую, без помощи в libpcap.
rawpacket.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
сигнал.
запустить приемник в одном виртуальном терминале на loopback-интерфейс:
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, был захвачен по прибытии. (Поскольку адрес Ethernet был FF:FF:FF:FF:FF: FF, это многоадресный пакет.)
как вы можете видеть, последний пакет имеет только заголовок 802.1 Q VLAN -- клиентская VLAN --, ядро потребило тег VLAN службы 802.1 AD.
вышеуказанное подтверждает сценарий для интерфейса loopback, по крайней мере. Использование интерфейса raw-пакетов, ядро потребляет заголовок 802.1 AD VLAN (тот, который непосредственно следует за адресом получателя). Если вы используете tcpdump -i eth0
рядом с приемником вы можете видеть, что libpcap повторно вставляет потребляемый заголовок обратно в пакет!
интерфейс Loopback немного особенный, поэтому давайте повторим тест с помощью виртуальных машин. Я случайно запускаю последнюю версию Xubuntu 14.04 (все обновления установлены с 2014-06-28, Ubuntu 3.13.0-29-generic #53 x86_64 kernel). Адрес отправителя HW-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
как вы можете видеть, результаты в основном такие же, как и для случая замыкания на себя. В частности, тег VLAN службы 802.1 AD был потреблен на получить. (Вы можете использовать tcpdump или wireshark, чтобы сравнить полученные пакеты: libpcap, очевидно, повторно вставляет потребляемый пакет тегов VLAN в пакет.)
если у вас достаточно новое ядро (поддержка был добавлен в апреле 2013), то вы можете настроить 802.1 ad VLAN(ы) на получателе, используя:
sudo modprobe 8021q
sudo ip link add link eth0 eth0.service1 type vlan proto 802.1ad id 1
получение eth0
получит все пакеты, но на eth0.service1
только те пакеты с тегом 802.1 AD VLAN, с идентификатором VLAN 1. Это не кадры захвата с тегами 802.1 Q VLAN с тем же идентификатором VLAN, что означает, что вы можете выполнить полную маршрутизацию при получении как для 802.1 AD, так и для 802.1 Q VLAN.
я не доверял только вышеупомянутому тесту, сам; я создал ряд 802.1 AD и 802.1 Q VLANs, с отдельными receive
экземпляры на каждом из них и изменили заголовки пакетов (не только service (first) tci()
и клиента (второй) tci()
на rawpacket_qinq()
вызов в отправителя.c изменить обслуживание и идентификаторы VLAN клиента, но также изменение rawpacket.h, чтобы убедиться, что заголовки 802.1 AD (88a8) и 802.1 Q (8100) VLAN маршрутизируются правильно при получении). Все работало прекрасно, без всякой икоты.
в итоге:
учитывая достаточно недавнюю версию ядра Linux, кадры Ethernet правильно маршрутизируются ядром Linux (модулем 8021q) при получении, включая отдельные интерфейсы VLAN для 802.1 AD и 802.1 Q с теми же идентификаторами VLAN. В ядре нет используйте заголовок VLAN, используемый для маршрутизации, даже если никакие VLAN не настроены.
вопросы?