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

I need to recreate a service on linux that was used to work on an embedded system that runs the LwIP (lightweight IP) stack.

The service uses UDP broadcasts for INADDR_BROADCAST (255.255.255.255) to search for and configure devices on the same physical subnet. It sends a β€œscan” and all devices that run this service response, with their full network setup (all network cards, all their MAC addresses and IP addresses). The user then receives a list of these devices and can change the IP setting (using an existing protocol).
[yes, I know that people use DHCP for this, but we are talking about the industrial sector here, and the protocol / service already exists, so I have no choice but to implement something compatible]

Since the device has several network adapters, I need to be able to receive this broadcast, know which network adapter received it and send a response through this network card. Also, the service is configured so that it does not open the socket for specific network adapters.

The LwIP stack is not as complex as the Linux stack, so the IP-related socket still receives all packets up to INADDR_BROADCAST . So it was pretty simple to implement this.

On Linux, I decided that I have several options:

  • open separate sockets for each network card with SO_BROADCAST and SO_BINDTODEVICE , so I can bind() them up to INADDR_ANY and receive broadcasts. When I send a response through this socket, Linux routing is ignored and sent through the correct network adapter.
    BUT : I would like the service to not start as root ..
  • You have the only associated INADDR_ANY socket (possibly with IP_PKTINFO , to easily find out which network card the packet came to), have one socket on the NIC bound to a valid address with SO_BROADCAST and send responses through them. If I go this way, I would like to make sure that sending sockets can never receive anything (because I never call recv () on them. Resource voice?).
    Maybe SO_RCVBUFSIZE = 0 will be enough?

What would be the right way to implement this?

+5
source share
1 answer

You can set the binary using CAP_NET_RAW (and CAP_NET_BIND_SERVICE if ports ≀ 1024 are used); setcap 'cap_net_raw=ep' yourdaemon as root. For IP, SO_BROADCAST does not require any capabilities (in particular, CAP_NET_BROADCAST not used for an IP address).

(For exact capabilities, see, for example, net / core / sock.c: sock_setbindtodevice () , net / core / sock.c: sock_setsockopt () and enable /net/sock.h: sock_set_flag () in Linux kernel sources for checks.)

However, daemons usually run as root. This is not enough here, since changing the user ID for the process (to remove privileges) also cleans up the effective features . However, I also prefer that my services run with limited privileges.

I would choose two main approaches:

  • Require that the daemon be run as root or with the capabilities of CAP_NET_RAW (and optionally CAP_NET_BIND_SERVICE ).

    Use prctl() , setgroups() or initgroups() , setresuid() , setresgid() and from libcap, cap_init() , cap_set_flag() and cap_set_proc() to refuse privileges by switching to the selected user and still preserving the features CAP_NET_RAW (and optionally CAP_NET_BIND_SERVICE ) and only them.

    This allows daemons to respond to, for example, HUP without a full restart, since it has the necessary privileges to list interfaces and read its own configuration files to open sockets for new interfaces.

  • Use a privileged "bootloader" that opens all the necessary sockets, reduces privileges and runs the actual daemon.

    The daemon should receive information about the socket and interface as command line parameters or, possibly, through standard input. The daemon is completely unprivileged.

    Unfortunately, if new interfaces open or the configuration is changed, the daemon cannot do much but exit. (It cannot even execute the privileged bootloader because privileges have already been removed.)

The first approach is more common and easier to put into practice; especially if the daemon should only run root. (Remember that the daemon can respond to configuration changes because it has the necessary capabilities, but does not have root privileges at all.) I used only the second approach for black box binaries, which I do not trust.


Here is a sample code.

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

Compile with

 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 

and run example as root, specifying an unprivileged username to run as, an interface for binding, and a UDP port number as parameters:

 sudo ./example yourdaemonuser eth0 4000 

Right now I only have one laptop to use, so the receiving side is mostly untested. I know that CAP_NET_RAW is enough here (Linux kernel 4.2.0-27 on x86-64), and that UDP broadcasts appear as outgoing messages from the ethernet interface address at 255.255.255.255:port , but I do not have another computer to send sample answers daemon (which would be easy to use, for example, NetCat: printf 'Response!' | nc -u4 -q2y interface-address port ).

Please note that the quality of the code above is only an initial assessment. Since I do not need this for anything, and I just wanted to verify that I was not speaking from my butt, I did not make any effort to make the code clean or reliable.

Questions? Comments?

+2
source

Source: https://habr.com/ru/post/1241996/


All Articles