recvfrom(2) 收到两次 UDP 广播,但 tcpdump(8) 只收到一次
recvfrom(2) receives UDP broadcast twice, but tcpdump(8) receives it only once
总结:我想从单个接口接收数据包,但是 setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, iface, 1 + strlen(iface))
不能很好地与 recvfrom
一起显示所有数据包接口。然而,tcpdump
效果很好。
我有一种强烈的感觉是接收器程序有问题,但我一直没弄清楚。
我正在使用 Netronome Agilio CX SmartNIC。 NIC 上的两个端口用一根电缆连接在一起,主板上的端口连接到墙上(所以我可以通过 SSH 连接到它)。板载网卡是OS中的eth0
,而SmartNIC呈现两个接口enp1s0np0
和enp1s0np1
。
由于SmartNIC 上的两个接口没有关联的IP 地址,我必须向一个端口发送广播,以便它到达另一个端口。现在,我发送到 enp1s0np0
并期望它来自 enp1s0np1
。
我还部署了一个修改部分数据包的XDP卸载程序,这样我就可以知道数据包是否到达了enp1s0np1
。程序将位置 28~35 的字符串更改为另一个字符串(形式为 !......!
)。
我遇到的问题是,我自己写了一个接收程序,它接收 两个 数据包我发送每个数据包 - 第一个是原始的,而第二个是XDP 修改后的数据包。但是,tcpdump
仅接收修改后的数据包(预期行为)。
我不确定为什么我的程序会收到两次 - 我不认为它应该能够看到未修改的数据包。
这是数据包发送程序。它读取 32 个双精度浮点数并将它们打包成一个 256 字节的块,并在该块前面添加 16 个字节的 "magic numbers".
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "config.h"
#include "util.h"
typedef unsigned char byte;
void sanity_check(void);
int main(int argc, char** argv) {
sanity_check();
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
errorexit("socket");
char iface[16] = "enp1s0np0";
if (setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, iface, 1 + strlen(iface)))
errorexit("setsockopt");
int optval = -1;
if (setsockopt(sock, SOL_SOCKET, SO_BROADCAST | SO_REUSEADDR, &optval, sizeof(int)))
errorexit("setsockopt");
struct sockaddr_in target_addr = {
.sin_family = AF_INET,
.sin_addr.s_addr = 0xFFFFFFFF,
.sin_port = htons(6666)
};
byte buf[272];
// Prepare data
{
unsigned long magic = MAGIC;
memcpy(buf + 0, &magic, sizeof magic);
unsigned long zero = 0UL;
memcpy(buf + 8, &zero, sizeof zero);
double data;
for (int i = 0; i < 32; i++) {
scanf(" %lf", &data);
memcpy(buf + 16 + 8 * i, &data, sizeof data);
}
}
int sent = sendto(sock, buf, sizeof(buf), 0, (struct sockaddr*)&target_addr, sizeof(struct sockaddr));
if (sent != 0)
errorexit("send");
printf("%d bytes sent.\n", sent);
// if (shutdown(sock, SHUT_RDWR))
if (close(sock))
errorexit("close");
return 0;
}
void sanity_check(void) {
if (getuid() || geteuid()) {
fprintf(stderr, "Need root to proceed\n");
exit(1);
}
}
这是接收程序。事实上,它正在接收进入机器的每个 每个 数据包,其中大部分是 SSH 数据。我必须添加对幻数的检查,否则它只会向终端发送垃圾邮件。我猜它只是未能听取具体的接口。 (检查是if (buf[28] != '!' || buf[35] != '!') continue;
)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "config.h"
#include "util.h"
typedef unsigned char byte;
void sanity_check(void);
int main(int argc, char** argv) {
sanity_check();
int sock = socket(AF_PACKET, SOCK_DGRAM, htons(3));
if (sock < 0)
errorexit("socket");
char iface[16] = "enp1s0np1";
if (setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, iface, 1 + strlen(iface)))
errorexit("setsockopt");
struct sockaddr_in target_addr = {
.sin_family = AF_INET,
.sin_addr.s_addr = htonl(INADDR_ANY),
.sin_port = htons(UDP_PORT)
};
size_t bufsize = 8192;
byte *buf = malloc(bufsize);
unsigned long magic = MAGIC;
int saddr_size = sizeof(struct sockaddr);
printf("Preparing to receive... ");
fflush(stdout);
while (1) {
int received = recvfrom(sock, buf, bufsize, 0,(struct sockaddr *)&target_addr , (socklen_t*)&saddr_size);
if (received < 0)
errorexit("receive");
if (received == 0)
break;
else if (buf[28] != '!' || buf[35] != '!') // Magic check
continue;
printf("%d bytes received.\n", received);
hexdump(buf, received);
}
if (close(sock))
errorexit("close");
free(buf);
return 0;
}
void sanity_check(void) {
if (getuid() || geteuid()) {
fprintf(stderr, "Need root to proceed\n");
exit(1);
}
}
文件 util.c
(link to Gist) 包含两个实用函数 (errorexit
只是 perror
和 exit
的包装,还有一个糟糕的hand-crafted hexdump
function for displaying) 与此处无关。
常量MAGIC
定义为
#define MAGIC 0x216C7174786A7A21UL // string "!zjxtql!"
这是我的程序(recv.c
编译成 recv
)和 tcpdump 命令的控制台输出,不相关的数据被截断了。在两个程序都被杀死之前,发送程序只发送了一个数据包。特别需要注意的是第28位的数据(原为!zjxtql!
,XDP卸载程序修改为!wjfskb!
)
$ sudo ./recv
Preparing to receive...
300 bytes received.
00000000 45 00 01 2C 4C 1A 40 00 40 11 B4 7F 72 D6 C6 51 |E..,L.@.@...r..Q|
00000010 FF FF FF FF B0 9F 1A 0A 01 18 3A 51 21 7A 6A 78 |..........:Q!zjx|
00000020 74 71 6C 21 00 00 00 00 00 00 00 00 29 5C 8F C2 |tql!........)\..|
00000120 14 AE F7 3F AE 47 E1 7A 14 AE F7 3F |...?.G.z...?|
0000012C
300 bytes received.
00000000 45 00 01 2C 4C 1A 40 00 40 11 B4 7F 72 D6 C6 51 |E..,L.@.@...r..Q|
00000010 FF FF FF FF B0 9F 1A 0A 01 18 C0 9B 21 77 6A 66 |............!wjf|
00000020 73 6B 62 21 00 00 00 00 00 00 00 00 29 5C 8F C2 |skb!........)\..|
00000120 14 AE F7 3F AE 47 E1 7A 14 AE F7 3F |...?.G.z...?|
0000012C
^C
$ sudo tcpdump -vv -X -i enp1s0np1 port 6666
tcpdump: listening on enp1s0np1, link-type EN10MB (Ethernet), capture size 262144 bytes
04:53:52.819657 IP (tos 0x0, ttl 64, id 8595, offset 0, flags [DF], proto UDP (17), length 300)
agilio1415.47585 > 255.255.255.255.ircu-2: [bad udp cksum 0xb759 -> 0xc274!] UDP, length 272
0x0000: 4500 012c 2193 4000 4011 df06 72d6 c651 E..,!.@.@...r..Q
0x0010: ffff ffff b9e1 1a0a 0118 b759 2177 6a66 ...........Y!wjf
0x0020: 736b 6221 0000 0000 0000 0000 295c 8fc2 skb!........)\..
0x0120: 14ae f73f ae47 e17a 14ae f73f ...?.G.z...?
^C
我已经尝试 strace
ing tcpdump 并尝试使用 setsockopt
:
sudo strace -e setsockopt tcpdump -vv -X -i enp1s0np1 port 6666
这给出了
setsockopt(3, SOL_PACKET, PACKET_ADD_MEMBERSHIP, {mr_ifindex=if_nametoindex("enp1s0np1"), mr_type=PACKET_MR_PROMISC, mr_alen=0, mr_address=}, 16) = 0
setsockopt(3, SOL_PACKET, PACKET_AUXDATA, [1], 4) = 0
setsockopt(3, SOL_PACKET, PACKET_VERSION, [1], 4) = 0
setsockopt(3, SOL_PACKET, PACKET_RESERVE, [4], 4) = 0
setsockopt(3, SOL_PACKET, PACKET_RX_RING, 0x7ffe9d8d1510, 28) = 0
setsockopt(7, SOL_SOCKET, SO_RCVBUF, [8388608], 4) = 0
setsockopt(7, SOL_SOCKET, SO_SNDBUF, [8388608], 4) = 0
setsockopt(3, SOL_SOCKET, SO_ATTACH_FILTER, {len=1, filter=0x7fa5289de000}, 16) = 0
setsockopt(3, SOL_SOCKET, SO_ATTACH_FILTER, {len=24, filter=0x56025631f280}, 16) = 0
tcpdump: listening on enp1s0np1, link-type EN10MB (Ethernet), capture size 262144 bytes
因为其他的我都不懂,所以只模仿了tcpdump的第一个调用setsockopt
:
struct packet_mreq mreq = {
.mr_ifindex = if_nametoindex(iface),
.mr_type = PACKET_MR_PROMISC,
.mr_alen = 0
};
if (setsockopt(sock, SOL_PACKET, PACKET_ADD_MEMBERSHIP, &mreq, sizeof(mreq)))
errorexit("setsockopt");
上面的代码替换了接收程序中的 setsockopt(SO_BINDTODEVICE)
调用,但我没有观察到任何差异(仍然捕获了来自所有接口的所有数据包)。
看来我只缺少一个 bind(2)
。不幸的是,SO_BINDTOINTERFACE
不适用于 AF_PACKET
,因此 bind(2)
是唯一的解决方案。
代码并不复杂:
struct sockaddr_ll sll = {
.sll_family = AF_PACKET,
.sll_ifindex = if_nametoindex(iface),
.sll_protocol = htons(3) // 3 = ETH_P_ALL
};
if (bind(sock, (struct sockaddr*)&sll, sizeof sll))
errorexit("sock");
来自同一个socket.7
page:
SO_BINDTOSOCKET
... Note that this works only for some socket types, particularly AF_INET
sockets. It is not supported for packet sockets (use normal bind(2)
there).
嗯,我想我应该更彻底地阅读手册。
总结:我想从单个接口接收数据包,但是 setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, iface, 1 + strlen(iface))
不能很好地与 recvfrom
一起显示所有数据包接口。然而,tcpdump
效果很好。
我有一种强烈的感觉是接收器程序有问题,但我一直没弄清楚。
我正在使用 Netronome Agilio CX SmartNIC。 NIC 上的两个端口用一根电缆连接在一起,主板上的端口连接到墙上(所以我可以通过 SSH 连接到它)。板载网卡是OS中的eth0
,而SmartNIC呈现两个接口enp1s0np0
和enp1s0np1
。
由于SmartNIC 上的两个接口没有关联的IP 地址,我必须向一个端口发送广播,以便它到达另一个端口。现在,我发送到 enp1s0np0
并期望它来自 enp1s0np1
。
我还部署了一个修改部分数据包的XDP卸载程序,这样我就可以知道数据包是否到达了enp1s0np1
。程序将位置 28~35 的字符串更改为另一个字符串(形式为 !......!
)。
我遇到的问题是,我自己写了一个接收程序,它接收 两个 数据包我发送每个数据包 - 第一个是原始的,而第二个是XDP 修改后的数据包。但是,tcpdump
仅接收修改后的数据包(预期行为)。
我不确定为什么我的程序会收到两次 - 我不认为它应该能够看到未修改的数据包。
这是数据包发送程序。它读取 32 个双精度浮点数并将它们打包成一个 256 字节的块,并在该块前面添加 16 个字节的 "magic numbers".
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "config.h"
#include "util.h"
typedef unsigned char byte;
void sanity_check(void);
int main(int argc, char** argv) {
sanity_check();
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
errorexit("socket");
char iface[16] = "enp1s0np0";
if (setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, iface, 1 + strlen(iface)))
errorexit("setsockopt");
int optval = -1;
if (setsockopt(sock, SOL_SOCKET, SO_BROADCAST | SO_REUSEADDR, &optval, sizeof(int)))
errorexit("setsockopt");
struct sockaddr_in target_addr = {
.sin_family = AF_INET,
.sin_addr.s_addr = 0xFFFFFFFF,
.sin_port = htons(6666)
};
byte buf[272];
// Prepare data
{
unsigned long magic = MAGIC;
memcpy(buf + 0, &magic, sizeof magic);
unsigned long zero = 0UL;
memcpy(buf + 8, &zero, sizeof zero);
double data;
for (int i = 0; i < 32; i++) {
scanf(" %lf", &data);
memcpy(buf + 16 + 8 * i, &data, sizeof data);
}
}
int sent = sendto(sock, buf, sizeof(buf), 0, (struct sockaddr*)&target_addr, sizeof(struct sockaddr));
if (sent != 0)
errorexit("send");
printf("%d bytes sent.\n", sent);
// if (shutdown(sock, SHUT_RDWR))
if (close(sock))
errorexit("close");
return 0;
}
void sanity_check(void) {
if (getuid() || geteuid()) {
fprintf(stderr, "Need root to proceed\n");
exit(1);
}
}
这是接收程序。事实上,它正在接收进入机器的每个 每个 数据包,其中大部分是 SSH 数据。我必须添加对幻数的检查,否则它只会向终端发送垃圾邮件。我猜它只是未能听取具体的接口。 (检查是if (buf[28] != '!' || buf[35] != '!') continue;
)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "config.h"
#include "util.h"
typedef unsigned char byte;
void sanity_check(void);
int main(int argc, char** argv) {
sanity_check();
int sock = socket(AF_PACKET, SOCK_DGRAM, htons(3));
if (sock < 0)
errorexit("socket");
char iface[16] = "enp1s0np1";
if (setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, iface, 1 + strlen(iface)))
errorexit("setsockopt");
struct sockaddr_in target_addr = {
.sin_family = AF_INET,
.sin_addr.s_addr = htonl(INADDR_ANY),
.sin_port = htons(UDP_PORT)
};
size_t bufsize = 8192;
byte *buf = malloc(bufsize);
unsigned long magic = MAGIC;
int saddr_size = sizeof(struct sockaddr);
printf("Preparing to receive... ");
fflush(stdout);
while (1) {
int received = recvfrom(sock, buf, bufsize, 0,(struct sockaddr *)&target_addr , (socklen_t*)&saddr_size);
if (received < 0)
errorexit("receive");
if (received == 0)
break;
else if (buf[28] != '!' || buf[35] != '!') // Magic check
continue;
printf("%d bytes received.\n", received);
hexdump(buf, received);
}
if (close(sock))
errorexit("close");
free(buf);
return 0;
}
void sanity_check(void) {
if (getuid() || geteuid()) {
fprintf(stderr, "Need root to proceed\n");
exit(1);
}
}
文件 util.c
(link to Gist) 包含两个实用函数 (errorexit
只是 perror
和 exit
的包装,还有一个糟糕的hand-crafted hexdump
function for displaying) 与此处无关。
常量MAGIC
定义为
#define MAGIC 0x216C7174786A7A21UL // string "!zjxtql!"
这是我的程序(recv.c
编译成 recv
)和 tcpdump 命令的控制台输出,不相关的数据被截断了。在两个程序都被杀死之前,发送程序只发送了一个数据包。特别需要注意的是第28位的数据(原为!zjxtql!
,XDP卸载程序修改为!wjfskb!
)
$ sudo ./recv
Preparing to receive...
300 bytes received.
00000000 45 00 01 2C 4C 1A 40 00 40 11 B4 7F 72 D6 C6 51 |E..,L.@.@...r..Q|
00000010 FF FF FF FF B0 9F 1A 0A 01 18 3A 51 21 7A 6A 78 |..........:Q!zjx|
00000020 74 71 6C 21 00 00 00 00 00 00 00 00 29 5C 8F C2 |tql!........)\..|
00000120 14 AE F7 3F AE 47 E1 7A 14 AE F7 3F |...?.G.z...?|
0000012C
300 bytes received.
00000000 45 00 01 2C 4C 1A 40 00 40 11 B4 7F 72 D6 C6 51 |E..,L.@.@...r..Q|
00000010 FF FF FF FF B0 9F 1A 0A 01 18 C0 9B 21 77 6A 66 |............!wjf|
00000020 73 6B 62 21 00 00 00 00 00 00 00 00 29 5C 8F C2 |skb!........)\..|
00000120 14 AE F7 3F AE 47 E1 7A 14 AE F7 3F |...?.G.z...?|
0000012C
^C
$ sudo tcpdump -vv -X -i enp1s0np1 port 6666
tcpdump: listening on enp1s0np1, link-type EN10MB (Ethernet), capture size 262144 bytes
04:53:52.819657 IP (tos 0x0, ttl 64, id 8595, offset 0, flags [DF], proto UDP (17), length 300)
agilio1415.47585 > 255.255.255.255.ircu-2: [bad udp cksum 0xb759 -> 0xc274!] UDP, length 272
0x0000: 4500 012c 2193 4000 4011 df06 72d6 c651 E..,!.@.@...r..Q
0x0010: ffff ffff b9e1 1a0a 0118 b759 2177 6a66 ...........Y!wjf
0x0020: 736b 6221 0000 0000 0000 0000 295c 8fc2 skb!........)\..
0x0120: 14ae f73f ae47 e17a 14ae f73f ...?.G.z...?
^C
我已经尝试 strace
ing tcpdump 并尝试使用 setsockopt
:
sudo strace -e setsockopt tcpdump -vv -X -i enp1s0np1 port 6666
这给出了
setsockopt(3, SOL_PACKET, PACKET_ADD_MEMBERSHIP, {mr_ifindex=if_nametoindex("enp1s0np1"), mr_type=PACKET_MR_PROMISC, mr_alen=0, mr_address=}, 16) = 0
setsockopt(3, SOL_PACKET, PACKET_AUXDATA, [1], 4) = 0
setsockopt(3, SOL_PACKET, PACKET_VERSION, [1], 4) = 0
setsockopt(3, SOL_PACKET, PACKET_RESERVE, [4], 4) = 0
setsockopt(3, SOL_PACKET, PACKET_RX_RING, 0x7ffe9d8d1510, 28) = 0
setsockopt(7, SOL_SOCKET, SO_RCVBUF, [8388608], 4) = 0
setsockopt(7, SOL_SOCKET, SO_SNDBUF, [8388608], 4) = 0
setsockopt(3, SOL_SOCKET, SO_ATTACH_FILTER, {len=1, filter=0x7fa5289de000}, 16) = 0
setsockopt(3, SOL_SOCKET, SO_ATTACH_FILTER, {len=24, filter=0x56025631f280}, 16) = 0
tcpdump: listening on enp1s0np1, link-type EN10MB (Ethernet), capture size 262144 bytes
因为其他的我都不懂,所以只模仿了tcpdump的第一个调用setsockopt
:
struct packet_mreq mreq = {
.mr_ifindex = if_nametoindex(iface),
.mr_type = PACKET_MR_PROMISC,
.mr_alen = 0
};
if (setsockopt(sock, SOL_PACKET, PACKET_ADD_MEMBERSHIP, &mreq, sizeof(mreq)))
errorexit("setsockopt");
上面的代码替换了接收程序中的 setsockopt(SO_BINDTODEVICE)
调用,但我没有观察到任何差异(仍然捕获了来自所有接口的所有数据包)。
看来我只缺少一个 bind(2)
。不幸的是,SO_BINDTOINTERFACE
不适用于 AF_PACKET
,因此 bind(2)
是唯一的解决方案。
代码并不复杂:
struct sockaddr_ll sll = {
.sll_family = AF_PACKET,
.sll_ifindex = if_nametoindex(iface),
.sll_protocol = htons(3) // 3 = ETH_P_ALL
};
if (bind(sock, (struct sockaddr*)&sll, sizeof sll))
errorexit("sock");
来自同一个socket.7
page:
SO_BINDTOSOCKET
... Note that this works only for some socket types, particularly
AF_INET
sockets. It is not supported for packet sockets (use normalbind(2)
there).
嗯,我想我应该更彻底地阅读手册。