SO_BINDTODEVICE Linux 套接字选项的问题
我有一台带有两个网卡的电脑。 一个 (eth0
) 用于 LAN/互联网,另一个用于与一个微控制器设备进行 UDP 通信。 微控制器有一个 IP (192.168.7.2) 和一个 MAC 地址。 第二个 PC 网络适配器 (eth1
) 的地址为 192.168.7.1。
微控制器有一个非常简单的IP堆栈,因此微控制器发送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) \n",
argv[0], strerror(errno));
exit (EXIT_FAILURE);
}
printf ("UDP socket created\n");
/* 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)\n",
argv[0], strerror(errno));
exit (EXIT_FAILURE);
}
printf ("set timeout to\ntime [s]: %d\ntime [ms]: %d\n", 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)\n",
argv[0], strerror(errno));
exit (EXIT_FAILURE);
}
printf ("timeout value\ntime [s]: %d\ntime [ms]: %d\n", 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)\n",
argv[0], strerror(errno));
exit (EXIT_FAILURE);
}
printf ("set SO_BROADCAST\n");
/* verify SO_BROADCAST setting */
rc=getsockopt(sock, SOL_SOCKET, SO_BROADCAST, &optval, &opt_length);
if (optval != 0)
{
printf("SO_BROADCAST is enabled\n");
}
/* 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)\n",
argv[0], strerror(errno));
exit (EXIT_FAILURE);
}
printf ("SO_BINDTODEVICE set\n");
/* 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)\n",
argv[0], strerror(errno));
exit (EXIT_FAILURE);
}
if (rc == 0)
{
printf("SO_BINDTODEVICE is: %s\n", 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)\n",
argv[0], strerror(errno));
exit (EXIT_FAILURE);
}
printf ("port bound\n");
/* identify mc */
buffer[0] = (char)1;
buffer[1] = (char)0;
send_data (buffer, 2);
printf ("sent command: %d\n", (char)buffer[0]);
rc=receive_data(buffer);
printf ("%d bytes received\n", rc);
buffer[rc] = (char)0; /* string end symbol */
printf ("%d - %s\n", (int)(char)buffer[0], &buffer[1]);
close(sock);
printf ("socket closed\n");
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\n");
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\n");
close (sock);
exit (EXIT_FAILURE);
}
return(rc);
}
I have a PC with two network cards. One (eth0
) is for LAN/internet and the other for UDP communication with one microcontroller device. The microcontroller has an IP (192.168.7.2) and a MAC address. The second pc network adapter (eth1
) has 192.168.7.1.
The microcontroller has a very simple IP stack, so the easiest way for the mc to send UDP packets is to broadcast them.
On the PC side I'd like to receive the broadcasts - but only from eth1
. So I try to bind the UDP socket to the eth1
device.
The problems (source code below):
setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, device, sizeof(device))
requires root privileges, why? (setting other options works as user)getsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, (void *)buffer, &opt_length)
gives "Protocol not available". I would like to read back the device I set viasetsockopt
command.Where can I find good info? I checked some Linux-programming, network books, but for example the
SO_BINDTODEVICE
option I've only found on the internet.
My lengthy (dirty) test program shows the problems. Setting and getting back the SO_RCVTIMEO
and SO_BROADCAST
options works as expected.
Running the code as user exits with:
could not set SO_BINDTODEVICE (Operation not permitted)"
Running with sudo gives:
SO_BINDTODEVICE set
./mc-test: could not get SO_BINDTODEVICE (Protocol not available)
So, setting the option seems to work but reading it back is not possible?
/* 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) \n",
argv[0], strerror(errno));
exit (EXIT_FAILURE);
}
printf ("UDP socket created\n");
/* 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)\n",
argv[0], strerror(errno));
exit (EXIT_FAILURE);
}
printf ("set timeout to\ntime [s]: %d\ntime [ms]: %d\n", 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)\n",
argv[0], strerror(errno));
exit (EXIT_FAILURE);
}
printf ("timeout value\ntime [s]: %d\ntime [ms]: %d\n", 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)\n",
argv[0], strerror(errno));
exit (EXIT_FAILURE);
}
printf ("set SO_BROADCAST\n");
/* verify SO_BROADCAST setting */
rc=getsockopt(sock, SOL_SOCKET, SO_BROADCAST, &optval, &opt_length);
if (optval != 0)
{
printf("SO_BROADCAST is enabled\n");
}
/* 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)\n",
argv[0], strerror(errno));
exit (EXIT_FAILURE);
}
printf ("SO_BINDTODEVICE set\n");
/* 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)\n",
argv[0], strerror(errno));
exit (EXIT_FAILURE);
}
if (rc == 0)
{
printf("SO_BINDTODEVICE is: %s\n", 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)\n",
argv[0], strerror(errno));
exit (EXIT_FAILURE);
}
printf ("port bound\n");
/* identify mc */
buffer[0] = (char)1;
buffer[1] = (char)0;
send_data (buffer, 2);
printf ("sent command: %d\n", (char)buffer[0]);
rc=receive_data(buffer);
printf ("%d bytes received\n", rc);
buffer[rc] = (char)0; /* string end symbol */
printf ("%d - %s\n", (int)(char)buffer[0], &buffer[1]);
close(sock);
printf ("socket closed\n");
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\n");
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\n");
close (sock);
exit (EXIT_FAILURE);
}
return(rc);
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(11)
只需使用 getifaddrs() 查找您感兴趣的接口的 IP 地址,然后使用 bind() 将您的套接字绑定到该 IP 地址。 如果您在套接字上启用 SO_BROADCAST,您将只能在该接口上收到广播。
或者,如果您愿意,您实际上可以跳过 getifaddrs() 部分,直接将 bind() 绑定到 192.168.7.1。
Just lookup the IP address of the interface you're interested in with getifaddrs(), and bind your socket to that IP address with bind(). If you enable SO_BROADCAST on the socket you'll then only get broadcasts recieved on that interface.
Or indeed you could skip the getifaddrs() part and just directly bind() to 192.168.7.1 if you like.
我可以确认向特定接口发送多播也可以这样工作。 请参阅下面的示例代码。
但是,如果 SO_BINDTODEVICE 将接口设置为我的辅助接口 eth4,则我无法使listener.c 程序工作。
我使用完全不同的机器来发送多播数据包,并且侦听器从接口 eth3 工作,而不是从接口 eth4 工作。 但是,tcpdump 显示两个接口中的数据包 (sudo tcpdump -i eth4 |grep UDP)。
这些是对 Antony Courtney 示例代码的修改:
sender.c 和listener.c:
I can confirm that sending multicast to specific interface works also like this. See the sample codes below.
However I can't get listener.c program working if the interface is set by SO_BINDTODEVICE to my secondary interface eth4.
I used completely different machine to send the multicast packets and the listener works from interface eth3, not from interface eth4. However, tcpdump shows the packets in both interfaces (sudo tcpdump -i eth4 |grep UDP).
These are modifications to Antony Courtney's sample code:
sender.c and listener.c:
如果您无法在辅助接口上接收多播数据包,则很可能是反向路径过滤阻止了它们。 如果收到的数据包不会从它们传入的接口发出,则此功能会过滤掉这些数据包。
要禁用此功能,请使用以下命令:
If you are unable to receive multicast packets on the secondary interface, it could well be reverse path filtering that is blocking them. This filters out received packets if those packets would not go out on the interface they are coming in on.
To disable this feature, use the following:
问题 2 的答案似乎是 SO_BINDTODEVICE 选项不支持 getsockopt。 在Linux内核源代码(2.6.27)中,该选项仅在linux-2.6.27.25-0.1/net/core/sock.c的sock_setsockopt函数中处理
对于问题3,似乎很多人推荐“UNIX网络编程” ” 理查德·史蒂文斯 (W. Richard Stevens) 所著的书。
我浏览了谷歌图书在线版本的套接字选项页面 - SO_BINDTODEVICE 选项未在表 7.1 和 7.2 中列出:-(
...也许是因为这个选项仅适用于 Linux?
The answer to question 2 seems to be that getsockopt is just not supported for the SO_BINDTODEVICE option. In the Linux kernel source (2.6.27) the option is only handled in the sock_setsockopt function of linux-2.6.27.25-0.1/net/core/sock.c
For question 3 it seems, lots of people recommend the "UNIX network programming" book by W. Richard Stevens.
I looked through the socket options pages of the google book online version - the SO_BINDTODEVICE option is not listed in table 7.1 and 7.2 :-(
...maybe because this option is Linux only?
setsocketopt 需要设备索引,而不是名称。 此外,您应该使用 struct ifreq 来传递索引:
setsocketopt needs device index, not name. Furthermore you should use struct ifreq to pass the index:
我通过将以下内容添加到 /etc/sudoers (或 /etc/sudoers.d 中的文件中)解决了类似的问题:
然后,不使用 fping 目录,而是使用 sudo fping 。
I've solved a similar problem by adding the following to /etc/sudoers (or in a file in /etc/sudoers.d):
Then instead of using fping directory, use
sudo fping
.在看到有关 SO_BINDTODEVICE 实际使用方式的相互矛盾的答案后,我已经对此进行了一段时间的研究。 一些来源声称正确的用法是传入
struct ifreq
指针,其中包含通过 ioctl 获取的设备名称和索引。 例如:Beej 的网络教程 表示将设备名称作为字符传递指针。 例如:
我已经尝试了这两种方法,并且它们都执行了所需的操作,但我想注意第一种方法中获得的设备索引是多余的。 如果您查看 net/core 中的内核代码/sock.c,
sock_bindtodevice
只是复制设备名称字符串,调用dev_get_by_name_rcu
来获取设备并绑定到它。第一种方法有效的原因是设备名称是 ifreq 结构中的第一个元素,请参阅 http://linux.die.net/man/7/netdevice。
注意:SO_BINDTODEVICE 需要提升权限:
SO_BINDTODEVICE
功能的权限(通过之前调用setcap
)。I have been looking into this for a while after seeing conflicting answers to how SO_BINDTODEVICE is actually used. Some sources claim that the correct usage is to pass in a
struct ifreq
pointer, which has the device name and index obtained via an ioctl. For example:Where as Beej's networking tutorial says to pass the device name as a char pointer. For example:
I have tried both of these methods and they both do what is required, but I wanted to note that the device index obtained in the first method is superfluous. If you look at the kernel code in net/core/sock.c,
sock_bindtodevice
just copies the device name string, callsdev_get_by_name_rcu
to get the device and binds to it.The reason that the first approach works is that the device name is the first element in the
ifreq
structure, see http://linux.die.net/man/7/netdevice.NOTE: SO_BINDTODEVICE requires elevated permissions:
sudo setcap
to grant the executable permission to use this specific socket option then you can run the executable without root permission and the executable has permission to use theSO_BINDTODEVICE
feature (via earlier call tosetcap
).上面的代码行足以仅从
eth0 接口
接收消息。我在 Linux 上测试了这个。
注意:如果有桥接接口控制实际接口,则该方法不起作用。
此致,
桑托什。
Above line of code is enough to receive messages from
eth0 interface
only.I tested this on Linux.
NOTE: It won't work if there is a bridge interface controlling actual interfaces.
Best regards,
Santosh.
好吧,我又研究了一下。 SO_BINDTODEVICE 早在 1999 年就被认为“近乎过时”,并且由于一些未指定的“安全影响”(我无法确切找出是什么)而仅适用于 root。
但是,您应该能够通过绑定到 INADDR_ANY 并设置 IP_PKTINFO 套接字选项来获得您想要的行为。 这将在套接字上传递一条额外的消息,其中包含描述传入数据包的 pktinfo 结构。 该结构包括数据包进入的接口的索引:
ipi_ifindex 与由 netdevice ioctl(如 SIOCGIFCONF)返回的 struct ifreq 中的 ifr_ifindex 匹配。 因此,您应该能够使用它来忽略在您感兴趣的接口以外的接口上收到的数据包。IP_PKTINFO
的 Doco 位于 ip(7) 中,而用于接口 ioctl 的 Doco 位于 netdevice(7) 中。
OK, I've looked into it a little more. SO_BINDTODEVICE was considered "near obsolete" back in 1999, and is root-only due to some unspecified "security implications" (I couldn't find out exactly what).
However, you should be able to get the behaviour you want by binding to INADDR_ANY and setting the IP_PKTINFO socketopt. This will pass an extra message on the socket that contains a pktinfo structure describing the incoming packet. This structure includes the index of the interface that the packet came in on:
The ipi_ifindex matches with the ifr_ifindex from the struct ifreq returned by the netdevice ioctls like SIOCGIFCONF. So you should be able to use that to ignore packets received on interfaces other than the one you're interested in.
Doco for IP_PKTINFO is in ip(7) and for the interface ioctls in netdevice(7).
在Linux 3.8之前,可以设置此套接字选项,但无法使用getsockopt()检索。
从Linux 3.8开始,套接字选项是可读的。
optvalue
参数应包含可用于接收设备名称的缓冲区,建议为IFNAMSZ
字节大小(include/linux/if.h
)。实际设备名称长度在
optlen
参数中报告。Before Linux 3.8, this socket option could be set, but could not retrieved with getsockopt().
Since Linux 3.8, socket options are readable.
The
optvalue
argument should contain the buffer available to receive the device name and is recommended to beIFNAMSZ
bytes size (include/linux/if.h
).The real device name length is reported back in the
optlen
argument.我遇到的问题似乎是 Linux、Windows 等对从特定接口接收广播的处理方式不同。
http://www.developerweb.net/forum/showthread.php?t= 5722
我现在决定通过更改微控制器的 TCP/IP 堆栈来解决该问题(文档少且可移植性差)。 它不再向广播地址发送应答,而是将传入 UDP 数据包中的 IP/MAC 作为目标 IP/MAC。 然后我可以(在电脑端)简单地将套接字绑定到 eth1 的 IP。
干杯,
迈克尔
The problem I ran into seems to be that receiving broadcasts from a specific interface is handled differently by Linux, Windows,...
http://www.developerweb.net/forum/showthread.php?t=5722
I now decided to solve the problem (little documentation and bad portability) by changing the TCP/IP stack of the microcontroller. It will no longer send answers to the broadcast address but instead take the IP/MAC from the incoming UDP packet as the destination IP/MAC. Then I can (on the pc side) simply bind the socket to the IP of eth1.
Cheers,
Michael