如何创建一个服务,在多个网络接口上发送/接收UDP广播

5
我需要在Linux上重新创建一个服务,该服务曾经在运行LwIP堆栈(轻量级IP)的嵌入式系统上运行。
该服务使用UDP广播到INADDR_BROADCAST(255.255.255.255),以查找并配置同一物理子网中的设备。 它发送一个“扫描”请求,所有运行此服务的设备都会回复其完整的网络设置(所有NIC、所有MAC和IP地址)。 然后用户就可以获得这些设备的列表,并可以更改IP设置(使用已有的协议)。 [是的,我知道人们使用DHCP来完成这个任务,但我们正在谈论工业部门,协议/服务已经存在,所以我必须实现兼容的东西]
由于该设备有几个NIC,因此我需要能够接收此广播消息,知道哪个NIC接收了它,并通过该NIC发送回复。 此外,该服务可配置,使其不在特定的NIC上打开套接字。
LwIP堆栈不像Linux堆栈那么复杂,所以绑定到IP的套接字仍会接收所有发往INADDR_BROADCAST的数据包。因此,实现这一点非常直接。
在Linux上,我想我有几个选择来实现这个:
- 对于每个NIC打开单独的套接字,使用SO_BROADCAST和SO_BINDTODEVICE,这样我就可以将它们绑定到INADDR_ANY并接收广播。当我通过该套接字发送回复时,Linux路由将被忽略,并且它将通过所需的NIC发送。
但是:我希望服务不以root身份运行... - 有一个绑定到INADDR_ANY的单个套接字(可能使用IP_PKTINFO轻松知道数据包到达的哪个NIC),每个NIC都有一个套接字,绑定到有效地址,带有SO_BROADCAST,并通过这些套接字发送回复。如果我采用这种方式,我想确保发送套接字永远不会接收任何东西(因为我从未在它们上调用recv()。资源饥饿?)。也许SO_RCVBUFSIZE = 0就足够了?
正确的实现方式是什么?

很难简单地回答,需要进行适当的分析。尝试创建简单的模型来完成你的分析。(我不知道这项服务在哪个行业运行,但我担心安全问题...) - rom1nux
该协议旨在进行初始设置,一旦系统处于“运行”模式,它不允许您更改IP设置。 - TabascoEye
我认为一个静默最小值会使SO_RCVBUFSIZE = 0变得没用。你可以使用shutdown(sockfd, SHUT_RD)来禁止进一步读取,但我倾向于正确编写程序 :) - John Hascall
它使用哪个端口号 -- 如果<1024,则可能需要至少以root身份启动。 - John Hascall
不,它在1024以上。这没问题。(我也可以使用具有CAP_NET_BIND_SERVICE的libcap来实现) - TabascoEye
1个回答

3
您可以使用CAP_NET_RAW(如果使用端口≤1024,则还需要CAP_NET_BIND_SERVICE)来安装二进制文件;以root身份运行 setcap 'cap_net_raw=ep' yourdaemon。对于IP,SO_BROADCAST不需要任何特殊权限(特别是不需要使用CAP_NET_BROADCAST)。 (有关所需确切权限,请参见Linux内核源代码中的net/core/sock.c:sock_setbindtodevice(), net/core/sock.c:sock_setsockopt(), 和 include/net/sock.h:sock_set_flag()进行验证。)
然而,守护进程通常以root权限启动。在这种情况下,仅以上述方法是不够的,因为更改进程的用户ID(以降低特权级别)也会清除有效能力。但是,我也希望我的服务以有限的特权运行。
我会选择两种基本方法之一:
  1. 要求守护程序由root执行,或使用CAP_NET_RAW(和可选的CAP_NET_BIND_SERVICE)功能。

    使用prctl()setgroups()initgroups()setresuid()setresgid()以及从libcap中获取cap_init()cap_set_flag()cap_set_proc()来通过切换到专用用户和组来降低权限,但保留CAP_NET_RAW(和可选的CAP_NET_BIND_SERVICE)功能并仅保留这些功能。

    这允许守护进程响应例如HUP信号而无需完全重新启动,因为它具有必要的特权以枚举接口并读取自己的配置文件以为新接口打开套接字。

  2. 使用特权“加载器”打开所有必要的套接字,放弃特权并执行实际的守护进程。

    守护进程应将套接字和接口详细信息作为命令行参数或通过标准输入获得。该守护进程完全没有特权。

    不幸的是,如果打开新接口或更改配置,则守护进程除了退出外无法做任何事情。(它甚至无法执行特权加载器,因为权限已经被放弃。)

第一种方法更为常见,并且在实践中更容易实施;特别是如果守护程序只应由root执行。 (请记住,守护进程可以响应配置更改,因为它具有必要的功能,但通常没有root权限。)我仅在不信任“黑匣子”二进制文件时使用第二种方法。


我是一名有用的助手,可以为您进行文本翻译。

这里是一些示例代码。

privileges.h: #ifndef PRIVILEGES_H #define PRIVILEGES_H

#define   NEED_CAP_NET_ADMIN          (1U << 0)
#define   NEED_CAP_NET_BIND_SERVICE   (1U << 1)
#define   NEED_CAP_NET_RAW            (1U << 2)

extern int drop_privileges(const char *const user, const unsigned int capabilities);

#endif /* PRIVILEGES_H */

privileges.c:

#define _GNU_SOURCE
#define _BSD_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/capability.h>
#include <sys/prctl.h>
#include <errno.h>
#include <pwd.h>
#include <grp.h>
#include "privileges.h"

/* Only three NEED_CAP_ constants defined. */
#define MAX_CAPABILITIES 3

static int permit_effective(cap_t caps, const unsigned int capabilities)
{
    cap_value_t  value[MAX_CAPABILITIES];
    int          values = 0;

    if (capabilities & NEED_CAP_NET_ADMIN)
        value[values++] = CAP_NET_ADMIN;

    if (capabilities & NEED_CAP_NET_BIND_SERVICE)
        value[values++] = CAP_NET_BIND_SERVICE;

    if (capabilities & NEED_CAP_NET_RAW)
        value[values++] = CAP_NET_RAW;

    if (values < 1)
        return 0;

    if (cap_set_flag(caps, CAP_PERMITTED, values, value, CAP_SET) == -1)
        return errno;
    if (cap_set_flag(caps, CAP_EFFECTIVE, values, value, CAP_SET) == -1)
        return errno;

    return 0;
}

static int add_privileges(cap_t caps)
{
    cap_value_t  value[3] = { CAP_SETPCAP, CAP_SETUID, CAP_SETGID };

    if (cap_set_flag(caps, CAP_PERMITTED, sizeof value / sizeof value[0], value, CAP_SET) == -1)
        return errno;

    if (cap_set_flag(caps, CAP_EFFECTIVE, sizeof value / sizeof value[0], value, CAP_SET) == -1)
        return errno;

    return 0;
}

int drop_privileges(const char *const user, const unsigned int capabilities)
{
    uid_t uid;
    gid_t gid;
    cap_t caps;

    /* Make sure user is neither NULL nor empty. */
    if (!user || !user[0])
        return errno = EINVAL;

    /* Find the user. */
    {
        struct passwd *pw;

        pw = getpwnam(user);
        if (!pw
#ifdef UID_MIN
            || pw->pw_uid < (uid_t)UID_MIN
#endif
#ifdef UID_MAX
            || pw->pw_uid > (uid_t)UID_MAX
#endif
#ifdef GID_MIN
            || pw->pw_gid < (gid_t)GID_MIN
#endif
#ifdef GID_MAX
            || pw->pw_gid > (gid_t)GID_MAX
#endif
               )
            return errno = EINVAL;

        uid = pw->pw_uid;
        gid = pw->pw_gid;

        endpwent();
    }

    /* Install privileged capabilities. */
    caps = cap_init();
    if (!caps)
        return errno = ENOMEM;
    if (permit_effective(caps, capabilities)) {
        const int cause = errno;
        cap_free(caps);
        return errno = cause;
    }
    if (add_privileges(caps)) {
        const int cause = errno;
        cap_free(caps);
        return errno = cause;
    }
    if (cap_set_proc(caps) == -1) {
        const int cause = errno;
        cap_free(caps);
        return errno = cause;
    }
    cap_free(caps);

    /* Retain permitted capabilities over the identity change. */
    prctl(PR_SET_KEEPCAPS, 1UL, 0UL,0UL,0UL);

    if (setresgid(gid, gid, gid) == -1)
        return errno = EPERM;

    if (initgroups(user, gid) == -1)
        return errno = EPERM;

    if (setresuid(uid, uid, uid) == -1)
        return errno = EPERM;

    /* Install unprivileged capabilities. */
    caps = cap_init();
    if (!caps)
        return errno = ENOMEM;
    if (permit_effective(caps, capabilities)) {
        const int cause = errno;
        cap_free(caps);
        return errno = cause;
    }
    if (cap_set_proc(caps) == -1) {
        const int cause = errno;
        cap_free(caps);
        return errno = cause;
    }
    cap_free(caps);

    /* Reset standard KEEPCAPS behaviour. */
    prctl(PR_SET_KEEPCAPS, 0UL, 0UL,0UL,0UL);

    /* Done. */
    return 0;
}

udp-broadcast.h:

#ifndef   UDP_BROADCAST_H
#define   UDP_BROADCAST_H
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>

struct udp_socket {
    struct sockaddr_in  broadcast;  /* Broadcast address */
    unsigned int        if_index;   /* Interface index */
    int                 descriptor; /* Socket descriptor */
};

extern int open_udp_broadcast(struct udp_socket *const udpsocket,
                              const char        *const interface,
                              int                const port);

extern int udp_broadcast(const struct udp_socket *const udpsocket,
                         const void *const              data,
                         const size_t                   size,
                         const int                      flags);

extern size_t udp_receive(const struct udp_socket *const udpsocket,
                          void *const                    data,
                          const size_t                   size_max,
                          const int                      flags,
                          struct sockaddr_in      *const from_addr,
                          struct sockaddr_in      *const to_addr,
                          struct sockaddr_in      *const hdr_addr,
                          unsigned int            *const if_index);

#endif /* UDP_BROADCAST_H */

udp-broadcast.c:

#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <net/if.h>
#include <errno.h>
#include "udp-broadcast.h"


int udp_broadcast(const struct udp_socket *const udpsocket,
                  const void *const              data,
                  const size_t                   size,
                  const int                      flags)
{
    ssize_t  n;

    if (!udpsocket || udpsocket->broadcast.sin_family != AF_INET)
        return errno = EINVAL;

    if (!data || size < 1)
        return 0;

    n = sendto(udpsocket->descriptor, data, size, flags,
               (const struct sockaddr *)&(udpsocket->broadcast),
               sizeof (struct sockaddr_in));

    if (n == (ssize_t)-1)
        return errno;
    if (n == (ssize_t)size)
        return 0;
    return errno = EIO;    
}


size_t udp_receive(const struct udp_socket *const udpsocket,
                   void *const                    data,
                   const size_t                   size_max,
                   const int                      flags,
                   struct sockaddr_in      *const from_addr,
                   struct sockaddr_in      *const to_addr,
                   struct sockaddr_in      *const hdr_addr,
                   unsigned int            *const if_index)
{
    char            ancillary[512];
    struct msghdr   msg;
    struct iovec    iov[1];
    struct cmsghdr *cmsg;
    ssize_t         n;

    if (!data || size_max < 1 || !udpsocket) {
        errno = EINVAL;
        return (size_t)0;
    }

    /* Clear results, just in case. */
    if (from_addr) {
        memset(from_addr, 0, sizeof *from_addr);
        from_addr->sin_family = AF_UNSPEC;
    }
    if (to_addr) {
        memset(to_addr, 0, sizeof *to_addr);
        to_addr->sin_family = AF_UNSPEC;
    }
    if (hdr_addr) {
        memset(hdr_addr, 0, sizeof *hdr_addr);
        hdr_addr->sin_family = AF_UNSPEC;
    }
    if (if_index)
        *if_index = 0U;

    iov[0].iov_base = data;
    iov[0].iov_len  = size_max;

    if (from_addr) {
        msg.msg_name = from_addr;
        msg.msg_namelen = sizeof (struct sockaddr_in);
    } else {
        msg.msg_name = NULL;
        msg.msg_namelen = 0;
    }

    msg.msg_iov = iov;
    msg.msg_iovlen = 1;

    msg.msg_control = ancillary;
    msg.msg_controllen = sizeof ancillary;

    msg.msg_flags = 0;

    n = recvmsg(udpsocket->descriptor, &msg, flags);
    if (n == (ssize_t)-1)
        return (size_t)0; /* errno set by recvmsg(). */
    if (n < (ssize_t)1) {
        errno = EIO;
        return (size_t)0;
    }

    /* Populate data from ancillary message, if requested. */
    if (to_addr || hdr_addr || if_index)
        for (cmsg = CMSG_FIRSTHDR(&msg); cmsg != NULL; cmsg = CMSG_NXTHDR(&msg, cmsg))
            if (cmsg->cmsg_level == IPPROTO_IP && cmsg->cmsg_type == IP_PKTINFO) {
                const struct in_pktinfo *const info = CMSG_DATA(cmsg);
                if (!info)
                    continue;
                if (if_index)
                    *if_index = info->ipi_ifindex;
                if (to_addr) {
                    to_addr->sin_family = AF_INET;
                    to_addr->sin_port = udpsocket->broadcast.sin_port; /* This is a guess. */
                    to_addr->sin_addr = info->ipi_spec_dst;
                }
                if (hdr_addr) {
                    hdr_addr->sin_family = AF_INET;
                    hdr_addr->sin_port = udpsocket->broadcast.sin_port; /* A guess, again. */
                    hdr_addr->sin_addr = info->ipi_addr;
                }
            }

    errno = 0;
    return (size_t)n;
}

int open_udp_broadcast(struct udp_socket *const udpsocket,
                       const char        *const interface,
                       int                const port)
{
    const size_t interface_len = (interface) ? strlen(interface) : 0;
    const int    set_flag = 1;
    int          sockfd;

    if (udpsocket) {
        memset(udpsocket, 0, sizeof *udpsocket);
        udpsocket->broadcast.sin_family = AF_INET;
        udpsocket->broadcast.sin_addr.s_addr = INADDR_BROADCAST;
        if (port >= 1 && port <= 65535)
            udpsocket->broadcast.sin_port = htons(port);
        udpsocket->descriptor = -1;
    }

    if (!udpsocket || interface_len < 1 || port < 1 || port > 65535)
        return errno = EINVAL;

    /* Generic UDP socket. */
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1)
        return errno;

    /* Set SO_REUSEADDR if possible. */
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &set_flag, sizeof set_flag);

    /* Set IP_FREEBIND if possible. */
    setsockopt(sockfd, IPPROTO_IP, IP_FREEBIND, &set_flag, sizeof set_flag);

    /* We need broadcast capability. */
    if (setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &set_flag, sizeof set_flag) == -1) {
        const int real_errno = errno;
        close(sockfd);
        return errno = real_errno;
    }

    /* We want the IP_PKTINFO ancillary messages, to determine target address
     * and interface index. */ 
    if (setsockopt(sockfd, IPPROTO_IP, IP_PKTINFO, &set_flag, sizeof set_flag) == -1) {
        const int real_errno = errno;
        close(sockfd);
        return errno = real_errno;
    }

    /* We bind to the broadcast address. */
    if (bind(sockfd, (const struct sockaddr *)&(udpsocket->broadcast), sizeof udpsocket->broadcast) == -1) {
        const int real_errno = errno;
        close(sockfd);
        return errno = real_errno;
    }

    /* Finally, we bind to the specified interface. */
    if (setsockopt(sockfd, SOL_SOCKET, SO_BINDTODEVICE, interface, interface_len) == -1) {
        const int real_errno = errno;
        close(sockfd);
        return errno = real_errno;
    }

    udpsocket->descriptor = sockfd;

    udpsocket->if_index = if_nametoindex(interface);

    errno = 0;
    return 0;
}

main.c:

#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <stdio.h>
#include <netdb.h>
#include <errno.h>
#include "privileges.h"
#include "udp-broadcast.h"

static volatile sig_atomic_t    done_triggered = 0;
static volatile sig_atomic_t    reload_triggered = 0;

static void done_handler(int signum)
{
    __sync_bool_compare_and_swap(&done_triggered, (sig_atomic_t)0, (sig_atomic_t)signum);
}

static void reload_handler(int signum)
{
    __sync_bool_compare_and_swap(&reload_triggered, (sig_atomic_t)0, (sig_atomic_t)signum);
}

static int install_handler(const int signum, void (*handler)(int))
{
    struct sigaction act;
    memset(&act, 0, sizeof act);
    sigemptyset(&act.sa_mask);
    act.sa_handler = handler;
    act.sa_flags = 0;
    if (sigaction(signum, &act, NULL) == -1)
        return errno;
    return 0;
}

/* Return 0 if done_triggered or reload_triggered, nonzero otherwise.
 * Always clears reload_triggered.
*/
static inline int keep_running(void)
{
    if (done_triggered)
        return 0;
    return !__sync_fetch_and_and(&reload_triggered, (sig_atomic_t)0);
}

static const char *ipv4_address(const void *const addr)
{
    static char    buffer[16];
    char          *end = buffer + sizeof buffer;
    unsigned char  byte[4];

    if (!addr)
        return "(none)";

    memcpy(byte, addr, 4);

    *(--end) = '\0';
    do {
        *(--end) = '0' + (byte[3] % 10);
        byte[3] /= 10U;
    } while (byte[3]);
    *(--end) = '.';
    do {
        *(--end) = '0' + (byte[2] % 10);
        byte[2] /= 10U;
    } while (byte[2]);
    *(--end) = '.';
    do {
        *(--end) = '0' + (byte[1] % 10);
        byte[1] /= 10U;
    } while (byte[1]);
    *(--end) = '.';
    do {
        *(--end) = '0' + (byte[0] % 10);
        byte[0] /= 10U;
    } while (byte[0]);

    return (const char *)end;
}

int main(int argc, char *argv[])
{
    int  port;
    char dummy;

    /* Check usage. */
    if (argc != 4 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
        fprintf(stderr, "\n");
        fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
        fprintf(stderr, "       %s USERNAME INTERFACE PORT\n", argv[0]);
        fprintf(stderr, "Where:\n");
        fprintf(stderr, "       USERNAME    is the unprivileged user to run as,\n");
        fprintf(stderr, "       INTERFACE   is the interface to bind to, and\n");
        fprintf(stderr, "       PORT        is the UDP/IPv4 port number to use.\n");
        fprintf(stderr, "\n");
        return EXIT_FAILURE;
    }

    /* Parse the port into a number. */
    if (sscanf(argv[3], "%d %c", &port, &dummy) != 1 || port < 1 || port > 65535) {
        struct servent *serv = getservbyname(argv[3], "udp");
        if (serv && serv->s_port > 1 && serv->s_port < 65536) {
            port = serv->s_port;
            endservent();
        } else {
            endservent();
            fprintf(stderr, "%s: Invalid port.\n", argv[3]);
            return EXIT_FAILURE;
        }
    }

    /* Drop privileges. */
    if (drop_privileges(argv[1], NEED_CAP_NET_RAW)) {
        fprintf(stderr, "%s.\n", strerror(errno));
        return EXIT_FAILURE;
    }

    /* Install signal handlers. */
    if (install_handler(SIGINT, done_handler) ||
        install_handler(SIGTERM, done_handler) ||
        install_handler(SIGHUP, reload_handler) ||
        install_handler(SIGUSR1, reload_handler)) {
        fprintf(stderr, "Cannot install signal handlers: %s.\n", strerror(errno));
        return EXIT_FAILURE;
    }

    fprintf(stderr, "Send a SIGINT (Ctrl+C) or SIGTERM to stop the service:\n");
    fprintf(stderr, "\tkill -SIGTERM %ld\n", (long)getpid());
    fprintf(stderr, "Send a SIGHUP or SIGUSR1 to have the service reload and rebroadcast:\n");
    fprintf(stderr, "\tkill -SIGHUP %ld\n", (long)getpid());
    fprintf(stderr, "Privileges dropped successfully.\n\n");
    fflush(stderr);

    while (!done_triggered) {
        struct udp_socket  s;

        if (open_udp_broadcast(&s, argv[2], port)) {
            fprintf(stderr, "%s port %s: %s.\n", argv[2], argv[3], strerror(errno));
            return EXIT_FAILURE;
        }

        if (udp_broadcast(&s, "Hello?", 6, MSG_NOSIGNAL)) {
            fprintf(stderr, "%s port %s: Broadcast failed: %s.\n", argv[2], argv[3], strerror(errno));
            close(s.descriptor);
            return EXIT_FAILURE;
        }

        if (s.if_index)
            fprintf(stderr, "Broadcast sent using interface %s (index %u); waiting for responses.\n", argv[2], s.if_index);
        else
            fprintf(stderr, "Broadcast sent using interface %s; waiting for responses.\n", argv[2]);
        fflush(stderr);

        while (keep_running()) {
            struct sockaddr_in  from_addr, to_addr, hdr_addr;
            unsigned char       data[512];
            unsigned int        if_index;
            size_t              size, i;

            size = udp_receive(&s, data, sizeof data, 0, &from_addr, &to_addr, &hdr_addr, &if_index);
            if (size > 0) {
                printf("Received %zu bytes:", size);
                for (i = 0; i < size; i++)
                    if (i & 15)
                        printf(" %02x", data[i]);
                    else
                        printf("\n\t%02x", data[i]);
                if (if_index)
                    printf("\n\t Index: %u", if_index);
                printf("\n\t  From: %s", ipv4_address(&from_addr.sin_addr));
                printf("\n\t    To: %s", ipv4_address(&to_addr.sin_addr));
                printf("\n\tHeader: %s", ipv4_address(&hdr_addr.sin_addr));
                printf("\n");
                fflush(stdout);
            } else
            if (errno != EINTR) {
                fprintf(stderr, "%s\n", strerror(errno));
                break;
            }
        }

        close(s.descriptor);
    }

    fprintf(stderr, "Exiting.\n");
    return EXIT_SUCCESS;
}

使用编译

gcc -Wall -Wextra -O2 -c privileges.c
gcc -Wall -Wextra -O2 -c udp-broadcast.c
gcc -Wall -Wextra -O2 -c main.c
gcc -Wall -Wextra main.o udp-broadcast.o privileges.o -lcap -o example

以 root 用户身份运行 example,指定一个非特权用户名称作为运行用户,将接口绑定到 UDP 端口号作为参数:

sudo ./example yourdaemonuser eth0 4000

现在我只有一台笔记本电脑在使用,因此接收端基本上没有经过测试。我知道CAP_NET_RAW在这里是足够的(Linux内核4.2.0-27在x86-64上),并且UDP广播发送将显示为从以太网接口地址到255.255.255.255:port的出站流量,但我没有另一台机器向守护程序发送示例响应(可以使用例如NetCat轻松完成:printf 'Response!' | nc -u4 -q2y interface-address port)。

请注意,上面的代码质量仅为初始测试等级。由于我自己不需要这个东西,只想验证我没有胡说八道,所以我没有花费任何精力使代码更加清洁或可靠。

有问题?评论?


@TabascoEye:我会进行调查(编写一些示例代码并自行测试),然后回复您。 - Nominal Animal
嗯,我在内核源代码中找到了这个:http://lxr.free-electrons.com/source/net/core/sock.c#L569,所以看起来应该可以工作。但是在我的简单小测试中它并没有工作。我可能会把它放到一个单独的stackoverflow问题中。 - TabascoEye
顺便说一句:如果您能分享一些示例代码作为框架,那将是非常棒的。 - TabascoEye
@TabascoEye:添加了一些实际示例代码。由于只有一台测试机器(只有一个以太网口),在功能方面测试非常不充分(基本上只使用 tcpdump 进行流量转储),因此可能存在漏洞或思维错误。但总体设计应该是有效的。privileges.c 已经经过了相当全面的测试,但需要注意的是,如果它失败了,则可能会保留原始特权。(它还使用旧的 PR_SET_KEEPCAPS 而不是在内核 2.6.26 中引入的 securebits 接口,如果您使用较旧的内核,则需要注意这一点。) - Nominal Animal
我发现CAP_NET_RAW对我无效,因为我的二进制文件所在的分区被挂载了nosuid选项,这显然会阻止所有功能。我为此创建了一个单独的问答页面https://dev59.com/f5Pfa4cB1Zd3GeqPH8pM - TabascoEye
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接