为什么CAP_NET_RAW无法与SO_BINDTODEVICE一起使用?

时间:2016-02-05 17:19:11

标签: linux sockets linux-capabilities

我有以下简单的测试程序来创建UDP套接字并将其绑定到SO_BINDTODEVICE的特定接口,这样我就可以bind()INADDR_ANY来接收UDP广播那个界面。

//filename: bindtest.c
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>

#define MY_PORT (333)
#define MY_DEVICE "enp0s3"

#define BUFFERSIZE (1000)

/* global variables */
int sock;
struct sockaddr_in sa;
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;
  sleep(1);
  /* 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 time [s]: %d time [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 worked\n");
  /* bind to a specific interface */
  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 worked\n");

  /* bind my own Port */
  my_addr.sin_family = AF_INET;
  my_addr.sin_addr.s_addr = INADDR_ANY;
  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 ("bind() worked\n");
  sa.sin_family = AF_INET;
  sa.sin_addr.s_addr = INADDR_BROADCAST;
  sa.sin_port = htons(MY_PORT);

  char data[20];
  sprintf(data,"FOOBAR");
  int res = sendto(sock, &data, strlen(data), 0, (struct sockaddr*)&sa, sizeof(sa));
  if(res < 0){
    printf("could not send\n");
  } else {
    printf("data sent\n");
  }


  close(sock);
  printf ("socket closed\n");

  exit(0);
}

当我以非root用户身份运行此程序时,我得到以下输出:

$ ./bindtest 
UDP socket created
set timeout to time [s]: 5 time [ms]: 0
set SO_BROADCAST worked
./bindtest: could not set SO_BINDTODEVICE (Operation not permitted)

这很合乎逻辑,因为我不是rootSO_BINDTODEVICE是特权行动。但据我从this snippet of code from the Linux kernel了解,它已包含在功能CAP_NET_RAW中:

static int sock_setbindtodevice(struct sock *sk, char __user *optval,
                                int optlen)
 {
         int ret = -ENOPROTOOPT;
 #ifdef CONFIG_NETDEVICES
         struct net *net = sock_net(sk);
         char devname[IFNAMSIZ];
         int index;

         /* Sorry... */
         ret = -EPERM;
         if (!ns_capable(net->user_ns, CAP_NET_RAW))
                 goto out;

当我这样做的时候还是很好:

$ getcap bindtest
$ sudo setcap cap_net_raw+ep bindtest
$ getcap bindtest
bindtest = cap_net_raw+ep

我得到相同的错误输出:

$ ./bindtest 
UDP socket created
set timeout to time [s]: 5 time [ms]: 0
set SO_BROADCAST worked
./bindtest: could not set SO_BINDTODEVICE (Operation not permitted)

当然它的作用是root

$ sudo ./bindtest
UDP socket created
set timeout to time [s]: 5 time [ms]: 0
set SO_BROADCAST worked
SO_BINDTODEVICE worked
bind() worked
data sent
socket closed

那么他们为什么不按预期工作?

1 个答案:

答案 0 :(得分:4)

代码是正确的,getcap / setcap的使用是正确的,所以其他东西必须阻止它工作。

事实上,这是因为所有这一切都是在/home/user中完成的,在nosuid这个系统上安装了/usr/bin/选项。

所以简单地将二进制文件移动到例如nosuid或未挂载的任何其他部分CAP_NET_BIND_SERVICE将首先按预期工作。

(尽管您还需要bind() <%= div_for(comment) do %> <p><%= comment.body %></p> <p class="comment-submitted-by"> <%= time_ago_in_words(comment.created_at) %> ago by <%= comment.user ? comment.user.email : 'deleted user' %> </p> <%end%> 才能使用端口333,如示例所示)