SO_BINDTODEVICE Linux 套接字选项的问题
- 2024-11-11 08:26:00
- admin 原创
- 22
问题描述:
我有一台带有两张网卡的 PC。一张 ( eth0
) 用于 LAN/互联网,另一张用于与一个微控制器设备进行 UDP 通信。微控制器有一个 IP (192.168.7.2) 和一个 MAC 地址。第二台 PC 网络适配器 ( eth1
) 有 192.168.7.1。
微控制器具有非常简单的 IP 堆栈,因此 mc 发送 UDP 数据包的最简单方法是广播它们。
在 PC 端,我想接收广播 - 但仅来自eth1
。因此,我尝试将 UDP 套接字绑定到eth1
设备。
问题(下面是源代码):
setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, device, sizeof(device))
需要 root 权限,为什么?(设置其他选项以用户身份运行)getsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, (void *)buffer, &opt_length)
显示“协议不可用”。我想读回我通过setsockopt
命令设置的设备。在哪里可以找到有用的信息?我查阅了一些 Linux 编程、网络书籍,但例如该
SO_BINDTODEVICE
选项,我只能在互联网上找到。
我的冗长(肮脏)测试程序显示了问题。设置和恢复SO_RCVTIMEO
和SO_BROADCAST
选项按预期工作。
当用户退出时运行代码:
could not set SO_BINDTODEVICE (Operation not permitted)"
使用 sudo 运行可得到:
SO_BINDTODEVICE set
./mc-test: could not get SO_BINDTODEVICE (Protocol not available)
那么,设置选项似乎有效,但无法读回它?
/* SO_BINDTODEVICE test */
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/time.h>
#include <errno.h>
#define MC_IP "192.168.7.2"
#define MC_PORT (54321)
#define MY_PORT (54321)
#define MY_DEVICE "eth1"
#define BUFFERSIZE (1000)
/* global variables */
int sock;
struct sockaddr_in MC_addr;
struct sockaddr_in my_addr;
char buffer[BUFFERSIZE];
int main(int argc, char *argv[])
{
unsigned int echolen, clientlen;
int rc, n;
char opt_buffer[1000];
struct protoent *udp_protoent;
struct timeval receive_timeout;
int optval;
socklen_t opt_length;
/* Create the UDP socket */
if ((sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0)
{
printf ("%s: failed to create UDP socket (%s)
",
argv[0], strerror(errno));
exit (EXIT_FAILURE);
}
printf ("UDP socket created
");
/* set the recvfrom timeout value */
receive_timeout.tv_sec = 5;
receive_timeout.tv_usec = 0;
rc=setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &receive_timeout,
sizeof(receive_timeout));
if (rc != 0)
{
printf ("%s: could not set SO_RCVTIMEO (%s)
",
argv[0], strerror(errno));
exit (EXIT_FAILURE);
}
printf ("set timeout to
time [s]: %d
time [ms]: %d
", receive_timeout.tv_sec, receive_timeout.tv_usec);
/* verify the recvfrom timeout value */
rc=getsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &receive_timeout, &opt_length);
if (rc != 0)
{
printf ("%s: could not get socket options (%s)
",
argv[0], strerror(errno));
exit (EXIT_FAILURE);
}
printf ("timeout value
time [s]: %d
time [ms]: %d
", receive_timeout.tv_sec, receive_timeout.tv_usec);
/* allow broadcast messages for the socket */
int true = 1;
rc=setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &true, sizeof(true));
if (rc != 0)
{
printf ("%s: could not set SO_BROADCAST (%s)
",
argv[0], strerror(errno));
exit (EXIT_FAILURE);
}
printf ("set SO_BROADCAST
");
/* verify SO_BROADCAST setting */
rc=getsockopt(sock, SOL_SOCKET, SO_BROADCAST, &optval, &opt_length);
if (optval != 0)
{
printf("SO_BROADCAST is enabled
");
}
/* bind the socket to one network device */
const char device[] = MY_DEVICE;
rc=setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, device, sizeof(device));
if (rc != 0)
{
printf ("%s: could not set SO_BINDTODEVICE (%s)
",
argv[0], strerror(errno));
exit (EXIT_FAILURE);
}
printf ("SO_BINDTODEVICE set
");
/* verify SO_BINDTODEVICE setting */
rc = getsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, (void *)buffer, &opt_length);
if (rc != 0)
{
printf ("%s: could not get SO_BINDTODEVICE (%s)
",
argv[0], strerror(errno));
exit (EXIT_FAILURE);
}
if (rc == 0)
{
printf("SO_BINDTODEVICE is: %s
", buffer);
}
/* Construct the server sockaddr_in structure */
memset(&MC_addr, 0, sizeof(MC_addr)); /* Clear struct */
MC_addr.sin_family = AF_INET; /* Internet/IP */
MC_addr.sin_addr.s_addr = inet_addr(MC_IP); /* IP address */
MC_addr.sin_port = htons(MC_PORT); /* server port */
/* bind my own Port */
my_addr.sin_family = AF_INET;
my_addr.sin_addr.s_addr = INADDR_ANY; /* INADDR_ANY all local addresses */
my_addr.sin_port = htons(MY_PORT);
rc = bind (sock, (struct sockaddr *) &my_addr, sizeof(my_addr));
if (rc < 0)
{
printf ("%s: could not bind port (%s)
",
argv[0], strerror(errno));
exit (EXIT_FAILURE);
}
printf ("port bound
");
/* identify mc */
buffer[0] = (char)1;
buffer[1] = (char)0;
send_data (buffer, 2);
printf ("sent command: %d
", (char)buffer[0]);
rc=receive_data(buffer);
printf ("%d bytes received
", rc);
buffer[rc] = (char)0; /* string end symbol */
printf ("%d - %s
", (int)(char)buffer[0], &buffer[1]);
close(sock);
printf ("socket closed
");
exit(0);
}
/* send data to the MC *****************************************************/
/* buffer points to the bytes to send */
/* buf_length is the number of bytes to send */
/* returns allways 0 */
int send_data( char *buffer, int buf_length )
{
int rc;
rc = sendto (sock, buffer, buf_length, 0,
(struct sockaddr *) &MC_addr,
sizeof(MC_addr));
if (rc < 0)
{
printf ("could not send data
");
close (sock);
exit (EXIT_FAILURE);
}
return(0);
}
/* receive data from the MC *****************************************************/
/* buffer points to the memory for the received data */
/* max BUFFERSIZE bytes can be received */
/* returns number of bytes received */
int receive_data(char *buffer)
{
int rc, MC_addr_length;
MC_addr_length = sizeof(MC_addr);
rc = recvfrom (sock, buffer, BUFFERSIZE, 0,
(struct sockaddr *) &MC_addr,
&MC_addr_length);
if (rc < 0)
{
printf ("could not receive data
");
close (sock);
exit (EXIT_FAILURE);
}
return(rc);
}
解决方案 1:
在看到关于 SO_BINDTODEVICE 实际使用方法的相互矛盾的答案后,我研究了这个问题一段时间。一些消息来源声称正确的用法是传入一个struct ifreq
指针,该指针具有通过 ioctl 获得的设备名称和索引。例如:
struct ifreq ifr;
memset(&ifr, 0, sizeof(struct ifreq));
snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "eth0");
ioctl(fd, SIOCGIFINDEX, &ifr);
setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, (void*)&ifr, sizeof(struct ifreq));
而Beej 的网络教程则说要将设备名称作为字符指针传递。例如:
char *devname = "eth0";
setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, devname, strlen(devname));
我尝试了这两种方法,它们都能完成所需的操作,但我想指出的是,第一种方法中获得的设备索引是多余的。如果您查看net/core/sock.c中的内核代码,sock_bindtodevice
只需复制设备名称字符串,调用dev_get_by_name_rcu
以获取设备并绑定到它。
第一种方法有效的原因是设备名称是ifreq
结构中的第一个元素,请参阅http://linux.die.net/man/7/netdevice。
注意:SO_BINDTODEVICE 需要提升的权限:
以完全 root 权限运行可执行文件
构建可执行文件后,您可以使用
sudo setcap
授予可执行文件使用此特定套接字选项的权限,然后您可以在没有 root 权限的情况下运行可执行文件,并且可执行文件有权使用该SO_BINDTODEVICE
功能(通过先前的调用setcap
)。
解决方案 2:
setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, "eth0", 4);
上面的代码行足以接收来自的消息eth0 interface
。我在 Linux 上对此进行了测试。
注意:如果有桥接接口控制实际接口,则它将不起作用。
谨致问候,桑托什。
解决方案 3:
好的,我对此做了进一步的研究。早在 1999 年,SO_BINDTODEVICE 就被认为“接近过时”,并且由于某些未指定的“安全隐患”(我无法确切找出是什么),因此仅供 root 使用。
但是,您应该能够通过绑定到 INADDR_ANY 并设置 IP_PKTINFO socketopt 来获得所需的行为。这将在套接字上传递一条额外的消息,其中包含描述传入数据包的 pktinfo 结构。此结构包括数据包传入的接口的索引:
struct in_pktinfo {
unsigned int ipi_ifindex; /* Interface index */
struct in_addr ipi_spec_dst; /* Local address */
struct in_addr ipi_addr; /* Header Destination address */
};
ipi_ifindex 与 netdevice ioctls(如 SIOCGIFCONF)返回的 struct ifreq 中的 ifr_ifindex 相匹配。因此,您应该能够使用它来忽略除您感兴趣的接口之外的接口上接收到的数据包。
IP_PKTINFO 的文档位于 ip(7) 中,而接口 ioctls 的文档位于 netdevice(7) 中。
解决方案 4:
在Linux 3.8之前,可以设置此套接字选项,但无法使用getsockopt()检索。
int getsockopt(int socket, int level, int optname,
void *restrict optvalue, socklen_t *restrict optlen);
自Linux 3.8起,套接字选项可读。optvalue
参数应包含可用于接收设备名称的缓冲区,建议为IFNAMSZ
字节大小(include/linux/if.h
)。
实际设备名称的长度在参数中报告optlen
。
解决方案 5:
我遇到的问题似乎是 Linux、Windows 等对从特定接口接收广播的处理方式不同……
http://www.developerweb.net/forum/showthread.php ?t=5722
我现在决定通过更改微控制器的 TCP/IP 堆栈来解决这个问题(文档很少,可移植性差)。它将不再向广播地址发送答案,而是将传入的 UDP 数据包中的 IP/MAC 作为目标 IP/MAC。然后我可以(在 PC 端)简单地将套接字绑定到 eth1 的 IP。
欢呼吧,迈克尔
解决方案 6:
只需使用 getifaddrs() 查找您感兴趣的接口的 IP 地址,然后使用 bind() 将您的套接字绑定到该 IP 地址。如果您在套接字上启用 SO_BROADCAST,那么您将只会在该接口上收到广播。
或者如果您愿意的话,您可以跳过 getifaddrs() 部分并直接将 bind() 到 192.168.7.1。
解决方案 7:
我可以确认向特定接口发送多播也是这样。请参阅下面的示例代码。但是,如果接口由 SO_BINDTODEVICE 设置为我的辅助接口 eth4,则我无法让 listener.c 程序运行。
我使用完全不同的机器发送多播数据包,侦听器从接口 eth3 而不是接口 eth4 工作。但是,tcpdump 在两个接口中都显示了数据包(sudo tcpdump -i eth4 |grep UDP)。
这些是对 Antony Courtney 的示例代码的修改:
sender.c 和 listener.c:
/*
* sender.c -- multicasts "hello, world!" to a multicast group once a second
*
* Antony Courtney, 25/11/94
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <time.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <net/if.h>
#define HELLO_PORT 12345
#define HELLO_GROUP "225.0.0.37"
main(int argc, char *argv[])
{
struct sockaddr_in addr;
int fd, cnt;
struct ip_mreq mreq;
char *message="Hello, World!";
char com[1000];
/* create what looks like an ordinary UDP socket */
if ((fd=socket(AF_INET,SOCK_DGRAM,0)) < 0) {
perror("socket");
exit(1);
}
/* set up destination address */
memset(&addr,0,sizeof(addr));
addr.sin_family=AF_INET;
addr.sin_addr.s_addr=inet_addr(HELLO_GROUP);
addr.sin_port=htons(HELLO_PORT);
u_char ttl=7;
setsockopt(fd, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, sizeof(ttl));
struct ifreq ifr;
memset(&ifr, 0, sizeof(struct ifreq));
snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "eth4");
ioctl(fd, SIOCGIFINDEX, &ifr);
printf("[[%d]]
", ifr.ifr_ifindex );
setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, (void*)&ifr, sizeof(struct ifreq));
inet_ntop(AF_INET, &(addr), com, INET_ADDRSTRLEN);
printf("addr=%s
", com );
/* now just sendto() our destination! */
while (1) {
if (sendto(fd,message,strlen(message),0,(struct sockaddr *) &addr,
sizeof(addr)) < 0) {
perror("sendto");
exit(1);
}
sleep(1);
}
}
listener.c :
/*
* listener.c -- joins a multicast group and echoes all data it receives from
* the group to its stdout...
*
* Antony Courtney, 25/11/94
* Modified by: Frédéric Bastien (25/03/04)
* to compile without warning and work correctly
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <time.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <net/if.h>
#define HELLO_PORT 12345
#define HELLO_GROUP "225.0.0.37"
#define MSGBUFSIZE 256
main(int argc, char *argv[])
{
struct sockaddr_in addr;
int fd, nbytes,addrlen;
struct ip_mreq mreq;
char msgbuf[MSGBUFSIZE];
u_int yes=1; /*** MODIFICATION TO ORIGINAL */
/* create what looks like an ordinary UDP socket */
if ((fd=socket(AF_INET,SOCK_DGRAM,0)) < 0) {
perror("socket");
exit(1);
}
struct ifreq ifr;
memset(&ifr, 0, sizeof(struct ifreq));
snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "eth4");
ioctl(fd, SIOCGIFINDEX, &ifr);
printf("[[%d]]
", ifr.ifr_ifindex );
if( setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, (void*)&ifr, sizeof(struct ifreq)) < 0 )
{
perror("SO_BINDTODEVICE");
exit(1);
}
/**** MODIFICATION TO ORIGINAL */
/* allow multiple sockets to use the same PORT number */
if (setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof(yes)) < 0) {
perror("Reusing ADDR failed");
exit(1);
}
/*** END OF MODIFICATION TO ORIGINAL */
/* set up destination address */
memset(&addr,0,sizeof(addr));
addr.sin_family=AF_INET;
addr.sin_addr.s_addr=htonl(INADDR_ANY); /* N.B.: differs from sender */
addr.sin_port=htons(HELLO_PORT);
/* bind to receive address */
if (bind(fd,(struct sockaddr *) &addr,sizeof(addr)) < 0) {
perror("bind");
exit(1);
}
/*
ifr.ifr_flags = IFF_UP | IFF_ALLMULTI | IFF_MULTICAST;
ioctl(fd, SIOCSIFFLAGS, &ifr );
*/
/* use setsockopt() to request that the kernel join a multicast group */
mreq.imr_multiaddr.s_addr=inet_addr(HELLO_GROUP);
mreq.imr_interface.s_addr=htonl(INADDR_ANY);
if (setsockopt(fd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mreq,sizeof(mreq)) < 0) {
perror("setsockopt");
exit(1);
}
/* now just enter a read-print loop */
while (1) {
addrlen=sizeof(addr);
if ((nbytes=recvfrom(fd,msgbuf,MSGBUFSIZE,0,
(struct sockaddr *) &addr,&addrlen)) < 0) {
perror("recvfrom");
exit(1);
}
msgbuf[nbytes]=' ';
puts(msgbuf);
}
}
解决方案 8:
如果您无法在辅助接口上接收多播数据包,则可能是反向路径过滤阻止了这些数据包。如果这些数据包不会从其传入的接口发出,则会过滤掉这些数据包。
要禁用此功能,请使用以下命令:
sudo -i
echo 2 > /proc/sys/net/ipv4/conf/eth1/rp_filter
echo 2 > /proc/sys/net/ipv4/conf/all/rp_filter
exit
解决方案 9:
问题 2 的答案似乎是 getsockopt 不支持 SO_BINDTODEVICE 选项。在 Linux 内核源代码 (2.6.27) 中,该选项仅在 linux-2.6.27.25-0.1/net/core/sock.c 的 sock_setsockopt 函数中处理
对于问题 3,似乎很多人都推荐 W. Richard Stevens 的《UNIX 网络编程》一书。我浏览了 google book online 版本的套接字选项页面 - SO_BINDTODEVICE 选项未列在表 7.1 和 7.2 中 :-( ...可能是因为此选项仅适用于 Linux?
解决方案 10:
setsocketopt 需要设备索引,而不是名称。此外,您应该使用 struct ifreq 来传递索引:
struct ifreq ifr;
memset(&ifr, 0, sizeof(ifr));
snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "eth3");
ioctl(s, SIOCGIFINDEX, &ifr)
setsockopt(s, SOL_SOCKET, SO_BINDTODEVICE, (void*)&ifr, sizeof(ifr));
解决方案 11:
我通过将以下内容添加到 /etc/sudoers(或 /etc/sudoers.d 中的文件)解决了类似的问题:
myuser myhost=(root) NOPASSWD: /usr/bin/fping
然后不要使用 fping 目录,而是使用sudo fping
。
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件