如何创建 sends/receives UDP 在多个接口上广播的服务

How to create a service that sends/receives UDP broadcasts on multiple interfaces

我需要在 linux 上重新创建一个用于 运行 在嵌入式系统上 运行 连接 LwIP 堆栈(轻量级 IP)的服务。

该服务使用 UDP 广播到 INADDR_BROADCAST (255.255.255.255) 以查找和配置同一物理子网上的设备。 它发送 "scan" 并且 运行 此服务的所有设备都以其完整的网络设置(所有 NIC、所有 MAC 和 IP)进行回复。 然后用户获得这些设备的列表并可以更改 IP 设置(使用已经存在的协议)。
[是的,我知道人们为此使用 DHCP,但我们在这里谈论的是工业部门并且 protocol/service 已经存在,所以我别无选择,只能实现一些兼容的东西]

由于设备有多个 NIC,我需要能够接收此广播,知道哪个 NIC 接收到它并通过该 NIC 发送回复。 此外,该服务是可配置的,因此它不会在特定 NIC 上打开套接字。

LwIP 堆栈不如 Linux 堆栈复杂,因此绑定到 IP 的套接字仍会接收到 INADDR_BROADCAST 的所有数据包。因此,实现这一点非常简单。

在 Linux 上,我想我有几个选择可以做到这一点:

实现这个的正确方法是什么?

您可以使用 CAP_NET_RAW 安装二进制文件(如果使用端口 ≤ 1024,则使用 CAP_NET_BIND_SERVICE); setcap 'cap_net_raw=ep' yourdaemon 作为 root。对于 IP,SO_BROADCAST 不需要任何功能(特别是,CAP_NET_BROADCAST 不用于 IP)。

(有关所需的确切功能,请参阅 Linux 内核源代码中的 net/core/sock.c:sock_setbindtodevice(), net/core/sock.c:sock_setsockopt(), and include/net/sock.h:sock_set_flag() 以进行验证。)

但是,守护进程通常以 root 身份启动。在这里,以上内容还不够,因为更改进程的用户 ID(以删除权限)也 clears the effective capabilities。然而,我也更喜欢我的服务而不是 运行,但权限有限。

我会在两种基本方法中做出选择:

  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. 使用特权 "loader",打开所有必要的套接字,删除特权,并执行实际的守护进程。

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

    不幸的是,如果打开了新的接口,或者更改了配置,守护进程除了退出之外不能做太多事情。 (它甚至无法执行特权加载程序,因为特权已经被删除。)

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


这是一些示例代码。

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) = '[=14=]';
    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

和运行 example作为root,指定非特权用户名运行作为,要绑定的接口,UDP端口号作为参数:

sudo ./example yourdaemonuser eth0 4000

目前我只有一台笔记本电脑在使用,所以接收端基本没有测试过。我知道 CAP_NET_RAW 在这里就足够了(Linux x86-64 上的内核 4.2.0-27),并且 UDP 广播发送显示为从以太网接口地址传出到 255.255.255.255:port,但我没有另一台机器可以向守护进程发送示例响应(这很容易使用,例如 NetCat:printf 'Response!' | nc -u4 -q2y interface-address port)。

请注意,以上代码质量仅为初试成绩。因为我自己不需要这个,只是想验证我不是在胡说八道,所以我没有花任何精力使代码干净或可靠。

有问题吗?评论?