【问题标题】:Problems with SO_BINDTODEVICE Linux socket optionSO_BINDTODEVICE Linux 套接字选项的问题
【发布时间】:2010-11-15 12:25:12
【问题描述】:

我有一台带有两个网卡的 PC。一个 (eth0) 用于 LAN/Internet,另一个用于与一个微控制器设备进行 UDP 通信。微控制器有一个 IP (192.168.7.2) 和一个 MAC 地址。第二个 pc 网络适配器 (eth1) 有 192.168.7.1。

微控制器有一个非常简单的 IP 堆栈,因此 mc 发送 UDP 数据包的最简单方法是广播它们。

在 PC 端,我想接收广播 - 但只能来自 eth1。所以我尝试将 UDP 套接字绑定到eth1 设备。

问题(源码如下):

  1. setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, device, sizeof(device)) 需要 root 权限,为什么? (设置其他选项以用户身份工作)

  2. getsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, (void *)buffer, &opt_length) 给出“协议不可用”。我想读回我通过setsockopt 命令设置的设备。

  3. 我在哪里可以找到好的信息?我查看了一些 Linux 编程、网络书籍,但例如 SO_BINDTODEVICE 选项我只能在互联网上找到。

我冗长(肮脏)的测试程序显示了问题。设置和取回 SO_RCVTIMEOSO_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);
}

【问题讨论】:

  • 你确定你需要这一切吗?您不能将套接字绑定()到 192.168.7.1 地址吗?它对我有用。
  • @Juliano:绑定到特定接口仅适用于 Windows 上的广播数据包。
  • 已尝试绑定到 192.168.7.255 并确保 eth0 和 eth1 具有不同的网络掩码?

标签: c linux sockets udp broadcast


【解决方案1】:

问题 2 的答案似乎是 SO_BINDTODEVICE 选项不支持 getsockopt。在 Linux 内核源代码(2.6.27)中,该选项仅在 linux-2.6.27.25-0.1/net/core/sock.c 的 sock_setsockopt 函数中处理

对于问题 3,似乎很多人推荐 W. Richard Stevens 的“UNIX 网络编程”一书。 我查看了 google book 在线版本的套接字选项页面 - SO_BINDTODEVICE 选项未在表 7.1 和 7.2 中列出:-( ...可能是因为此选项仅适用于 Linux?

【讨论】:

    【解决方案2】:

    只需使用 getifaddrs() 查找您感兴趣的接口的 IP 地址,然后使用 bind() 将您的套接字绑定到该 IP 地址。如果您在套接字上启用 SO_BROADCAST,那么您将只能在该接口上收到广播。

    或者如果你愿意,你可以跳过 getifaddrs() 部分,直接 bind() 到 192.168.7.1。

    【讨论】:

    • 这是我做的第一件事。而且我刚刚验证过了。电脑无法识别 MC 的广播答案!我可以看到带有wireshark的UDP数据包,但接收例程超时而没有收到任何东西。
    • 如果绑定到 192.168.7.255 会发生什么? (这是正在使用的广播,还是 255.255.255.255 的广播?)
    • 那也行不通。但我会发布我现在使用的解决方案。感谢您查看我的网络问题。
    • bind() 仅影响数据包的接收。您只会在相应的接口上接收数据包。但是 IP 堆栈将始终使用路由算法来选择独立于任何bind() 调用的接口。
    【解决方案3】:

    我遇到的问题似乎是从特定接口接收广播被 Linux、Windows、... http://www.developerweb.net/forum/showthread.php?t=5722

    我现在决定通过更改微控制器的 TCP/IP 堆栈来解决问题(文档少且可移植性差)。它将不再向广播地址发送答案,而是将传入 UDP 数据包中的 IP/MAC 作为目标 IP/MAC。然后我可以(在 pc 端)简单地将套接字绑定到 eth1 的 IP。

    干杯, 迈克尔

    【讨论】:

      【解决方案4】:

      好的,我已经研究了更多。 SO_BINDTODEVICE 早在 1999 年就被认为“已过时”,并且由于一些未指定的“安全隐患”(我不知道究竟是什么),它仅是根用户。

      但是,您应该能够通过绑定到 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 与网络设备 ioctl(如 SIOCGIFCONF)返回的结构 ifreq 中的 ifr_ifindex 匹配。因此,您应该能够使用它来忽略在您感兴趣的接口以外的接口上收到的数据包。

      IP_PKTINFO 的 Doco 位于 ip(7) 中,接口 ioctls 位于 netdevice(7) 中。

      【讨论】:

      • 使用IP_PKTINFO 意味着从recv()/recvfrom() 转换为recvmsg(),这并不完全是用户友好的。但可以肯定的是,IP_PKTINFO 使用起来非常方便,特别是如果您希望您的应用程序监听多个(但不是全部)接口。但是,我发现没有提到 SO_BINDTODEVICE 已过时/不推荐使用,而且 Linux 不太可能在将来通过删除它来破坏用户空间。因此,对于常见的用例,我每周都会在 Linux 上使用SO_BINDTODEVICE
      【解决方案5】:

      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));
      

      【讨论】:

        【解决方案6】:

        我可以确认向特定接口发送多播也可以这样工作。请参阅下面的示例代码。 但是,如果 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]]\n", 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\n", 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]]\n", 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]='\0';
              puts(msgbuf);
             }
        }
        

        【讨论】:

          【解决方案7】:

          在看到关于如何实际使用 SO_BINDTODEVICE 的相互矛盾的答案后,我一直在研究这个问题。 Some sources 声称正确的用法是传入一个 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's networking tutorial 表示将设备名称作为字符指针传递。例如:

          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

          【讨论】:

            【解决方案8】:
            setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, "eth0", 4);
            

            以上代码行仅足以接收来自eth0 interface 的消息。 我在 Linux 上对此进行了测试。

            注意:如果有控制实际接口的桥接接口将不起作用。

            最好的问候, 桑托什。

            【讨论】:

              【解决方案9】:

              如果您无法在辅助接口上接收多播数据包,则很可能是反向路径过滤阻止了它们。如果这些数据包不会在它们进入的接口上发出,这会过滤掉接收到的数据包。

              要禁用此功能,请使用以下命令:

              sudo -i
              echo 2 > /proc/sys/net/ipv4/conf/eth1/rp_filter
              echo 2 > /proc/sys/net/ipv4/conf/all/rp_filter
              exit
              

              【讨论】:

                【解决方案10】:

                我通过将以下内容添加到 /etc/sudoers(或 /etc/sudoers.d 中的文件)解决了类似的问题:

                myuser myhost=(root) NOPASSWD: /usr/bin/fping
                

                那么不要使用 fping 目录,而是使用sudo fping

                【讨论】:

                  【解决方案11】:

                  在 Linux 3.8 之前,可以设置此套接字选项,但无法使用 getsockopt() 检索。从 Linux 3.8 开始,它是可读的。 optlen 参数应包含可用于接收设备名称的缓冲区大小,建议为 IFNAMSZ 字节。真实的设备名称长度在 optlen 参数中报告。

                  【讨论】:

                    猜你喜欢
                    • 2012-09-08
                    • 1970-01-01
                    • 2016-02-28
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2017-10-27
                    • 1970-01-01
                    • 1970-01-01
                    相关资源
                    最近更新 更多