diff --git a/configure b/configure index a1a3216..f06d393 100755 --- a/configure +++ b/configure @@ -311,19 +311,19 @@ EOF esac if [ "$SANITIZEADDRESS" = yes ]; then - printf "Testing compiler supports address sanitisation ..." + printf "Testing compiler supports address sanitisation ... " cat <_test.c int main(void) { return 0; } EOF - if $CC -fsanitize=address _test.c -o _test 2>&3; then + if $CC -fsanitize=address,undefined _test.c -o _test 2>&3; then echo "yes" echo "SANITIZEADDRESS= yes" >>$CONFIG_MK echo "CPPFLAGS+= -DASAN" >>$CONFIG_MK - echo "CFLAGS+= -fsanitize=address" >>$CONFIG_MK + echo "CFLAGS+= -fsanitize=address,undefined" >>$CONFIG_MK echo "CFLAGS+= -fno-omit-frame-pointer" >>$CONFIG_MK - echo "LDFLAGS+= -fsanitize=address" >>$CONFIG_MK + echo "LDFLAGS+= -fsanitize=address,undefined" >>$CONFIG_MK else echo "no" fi @@ -591,6 +591,50 @@ fi rm -rf _inet_ntoa.* _inet_ntoa $abort && exit 1 +if [ -z "$LINK_SRC" ]; then + printf "Testing for route ... " + cat << EOF >_route.c +#include +#include +#include +int main(void) { + struct rt_msghdr rtm = { .rtm_type = RTM_IFINFO }; + return (int)rtm.rtm_type; +} +EOF + if $XCC _route.c -o _route 2>&3; then + LINK_SRC="route.c" + echo "yes" + else + echo "no" + fi + rm -rf _route.* _route +fi + +if [ -z "$LINK_SRC" ]; then + printf "Testing for netlink ... " + cat << EOF >_netlink.c +#include +#include +#include +int main(void) { + return 0; +} +EOF + if $XCC _netlink.c -o _netlink 2>&3; then + LINK_SRC="netlink.c" + echo "yes" + else + echo "no" + fi + rm -rf _netlink.* _netlink +fi +if [ -z "$LINK_SRC" ]; then + echo "no mechanism for detecting interface departures found" >&2 + exit 1 +fi +echo "LINK_SRC= $LINK_SRC" >>$CONFIG_MK + if [ -z "$SETPROCTITLE" ]; then printf "Testing for setproctitle ... " cat << EOF >_setproctitle.c @@ -742,7 +786,5 @@ echo " LIBDIR = $LIBDIR" echo " DATADIR = $DATADIR" echo " RUNDIR = $RUNDIR" echo " MANDIR = $MANDIR" -if [ "$PRIVSEP" = yes ]; then - echo " PRIVSEPUSER = $PRIVSEP_USER" -fi +echo " DHCPSD_USER = $DHCPSD_USER" echo diff --git a/src/Makefile b/src/Makefile index 554ab1e..4b9306a 100644 --- a/src/Makefile +++ b/src/Makefile @@ -4,15 +4,16 @@ PROG= dhcpsd SRCS= dhcpsd.c common.c eloop.c if.c logerr.c SRCS+= bpf.c dhcp.c dhcp_lease.c SRCS+= if_none.c if_ether.c -SRCS+= service.c unpriv.c +SRCS+= service.c priv.c unpriv.c SRCS+= plugin.c -OBJS+= ${SRCS:.c=.o} +SRCS+= ${LINK_SRC} +OBJS+= ${SRCS:.c=.o} PVENDOR_SRCS= ${VENDOR_SRCS:vendor/%=${TOP}/vendor/%} -OBJS+= ${PVENDOR_SRCS:.c=.o} +OBJS+= ${PVENDOR_SRCS:.c=.o} PCOMPAT_SRCS= ${COMPAT_SRCS:compat/%=${TOP}/compat/%} -OBJS+= ${PCOMPAT_SRCS:.c=.o} +OBJS+= ${PCOMPAT_SRCS:.c=.o} CFLAGS?= -O2 SUBDIRS= plugins diff --git a/src/bpf.c b/src/bpf.c index b023b6d..1fd922d 100644 --- a/src/bpf.c +++ b/src/bpf.c @@ -397,7 +397,7 @@ bpf_open(const struct interface *ifp, int (*filter)(const struct bpf *), #else struct bpf_version pv = { .bv_major = 0, .bv_minor = 0 }; struct ifreq ifr = { .ifr_flags = 0 }; -#if 0 +#if 1 int ibuf_len = 0; #endif unsigned int imm; @@ -506,7 +506,6 @@ bpf_open(const struct interface *ifp, int (*filter)(const struct bpf *), #ifdef __linux__ UNUSED(flags); #else -#if 0 if (flags & (O_RDONLY | O_RDWR)) { /* Get the required BPF buffer length from the kernel. */ if (ioctl(bpf->bpf_fd, BIOCGBLEN, &ibuf_len) == -1) @@ -517,7 +516,6 @@ bpf_open(const struct interface *ifp, int (*filter)(const struct bpf *), if (bpf->bpf_buffer == NULL) goto eexit; } -#endif #endif return bpf; @@ -568,7 +566,8 @@ bpf_read(struct bpf *bpf, void *data, size_t len) bytes = (ssize_t)len; else bytes = (ssize_t)packet.bh_caplen; - memcpy(data, payload, (size_t)bytes); + if (data) + memcpy(data, payload, (size_t)bytes); next: bpf->bpf_pos += BPF_WORDALIGN( packet.bh_hdrlen + packet.bh_caplen); diff --git a/src/common.c b/src/common.c index b60f0a9..e23557c 100644 --- a/src/common.c +++ b/src/common.c @@ -30,6 +30,10 @@ #include +#ifdef AF_PACKET +#include +#endif + #include #include #include @@ -414,13 +418,30 @@ inet_ntocidr(struct in_addr *addr) return cidr; } -size_t +int +sa_is_link(const struct sockaddr *sa) +{ +#ifdef AF_LINK + return sa->sa_family == AF_LINK ? 1 : 0; +#elif defined(AF_PACKET) + return sa->sa_family == AF_PACKET ? 1 : 0; +#else + errno = EAFNOSUPPORT; + return -1; +#endif +} + +socklen_t sa_len(const struct sockaddr *sa) { #ifdef BSD return sa->sa_len; #else switch (sa->sa_family) { +#ifdef AF_PACKET + case AF_PACKET: + return sizeof(struct sockaddr_ll); +#endif case AF_INET: return sizeof(struct sockaddr_in); case AF_INET6: diff --git a/src/common.h b/src/common.h index f500dc0..7fd528d 100644 --- a/src/common.h +++ b/src/common.h @@ -49,6 +49,13 @@ #define UNUSED(a) (void)(a) #define ARRAYCOUNT(a) (sizeof((a)) / sizeof((a)[0])) +#ifndef ALIGNBYTES +#define ALIGNBYTES (sizeof(int) - 1) +#endif +#ifndef ALIGN +#define ALIGN(p) (((unsigned int)(p) + ALIGNBYTES) & ~ALIGNBYTES) +#endif + /* Some systems don't define timespec macros */ #ifndef timespecclear #define timespecclear(tsp) (tsp)->tv_sec = (time_t)((tsp)->tv_nsec = 0L) @@ -100,7 +107,8 @@ uint8_t inet_ntocidr(struct in_addr *); struct sockaddr; -size_t sa_len(const struct sockaddr *); +int sa_is_link(const struct sockaddr *); +socklen_t sa_len(const struct sockaddr *); int sa_cmp(const struct sockaddr *, const struct sockaddr *); #define ss_len(ss) sa_len((const struct sockaddr *)(ss)) int sa_pton(struct sockaddr *, const char *restrict); diff --git a/src/dhcp.c b/src/dhcp.c index ace4354..8559d57 100644 --- a/src/dhcp.c +++ b/src/dhcp.c @@ -71,6 +71,7 @@ #include "if.h" #include "logerr.h" #include "plugin.h" +#include "priv.h" #ifdef HAVE_CASPER #include @@ -110,8 +111,8 @@ static void dhcp_set_expire_timeout(struct dhcp_ctx *ctx); static void dhcp_handlebootp(struct interface *ifp, struct bootp *bootp, size_t len, uint32_t flags); -static inline int -dhcp_cookiecmp(struct bootp *bootp) +int +dhcp_cookiecmp(const struct bootp *bootp) { uint32_t cookie = htonl(MAGIC_COOKIE); @@ -259,7 +260,7 @@ dhcp_findoption(const struct bootp *bootp, size_t len, uint8_t opt) return NULL; } -static void +static ssize_t dhcp_outputudp(const struct interface *ifp, const size_t len) { const struct dhcp_ctx *ctx = ifp->if_ctx->ctx_dhcp; @@ -272,22 +273,28 @@ dhcp_outputudp(const struct interface *ifp, const size_t len) .sin_len = sizeof(sin), #endif }; + ssize_t nbytes; #ifdef HAVE_CASPER if (cap_connect(ctx->dhcp_ctx->ctx_capnet, ctx->dhcp_capfd, - (struct sockaddr *)&sin, sizeof(sin)) == -1) + (struct sockaddr *)&sin, sizeof(sin)) == -1) { logerr("%s: cap_connect", __func__); - else if (send(ctx->dhcp_capfd, bootp, len, 0) == -1) + return -1; + } + nbytes = send(ctx->dhcp_capfd, bootp, len, 0); + if (nbytes == -1) logerr("%s: send", __func__); #else - if (sendto(ctx->dhcp_fd, bootp, len, 0, (struct sockaddr *)&sin, - sizeof(sin)) == -1) + nbytes = sendto(ctx->dhcp_fd, bootp, len, 0, (struct sockaddr *)&sin, + sizeof(sin)); + if (nbytes == -1) logerr("%s: sendto", __func__); #endif + return nbytes; } -static void -dhcp_outputipudp(const struct interface *ifp, const struct in_addr *src, +static ssize_t +dhcp_outputipudp(struct interface *ifp, const struct in_addr *src, const size_t len) { const struct dhcp_ctx *ctx = ifp->if_ctx->ctx_dhcp; @@ -328,6 +335,7 @@ dhcp_outputipudp(const struct interface *ifp, const struct in_addr *src, .iov_len = len, }, }; + ssize_t nbytes; if (dhcp_cookiecmp(bootp) == 0 && ((opt = dhcp_findoption(bootp, len, DHO_MESSAGETYPE)) != NULL) && @@ -360,9 +368,10 @@ dhcp_outputipudp(const struct interface *ifp, const struct in_addr *src, in_cksum(&udp, sizeof(udp), &sum); udp.uh_sum = in_cksum(bootp, len, &sum); - if (ifp->if_output(ifp, ifp->if_bpf->bpf_fd, iov, ARRAYCOUNT(iov)) == - -1) - logerr("%s: if_output: %s", __func__, ifp->if_name); + nbytes = priv_sendbpf(ifp, iov, ARRAYCOUNT(iov)); + if (nbytes == -1) + logerr("%s: priv_sendbpf: %s", __func__, ifp->if_name); + return nbytes; } static const char * @@ -837,12 +846,12 @@ dhcp_addoptions(struct bootp *bootp, uint8_t **p, const uint8_t *e, return 0; } -static void -dhcp_output(const struct interface *ifp, const struct dhcp_lease *lease, +static ssize_t +dhcp_output(struct interface *ifp, const struct dhcp_lease *lease, const uint8_t type, const char *msg, const struct bootp *req, size_t reqlen) { struct dhcp_ctx *ctx = ifp->if_ctx->ctx_dhcp; - struct bootp *bootp = ctx->dhcp_bootp; + struct bootp *bootp; const struct dhcp_pool *pool; struct in_addr addr; uint8_t *p, *e; @@ -852,6 +861,18 @@ dhcp_output(const struct interface *ifp, const struct dhcp_lease *lease, const uint8_t *opt; int err; + len = (size_t)ifp->if_mtu - sizeof(struct udphdr) - sizeof(struct ip); + if (len > ctx->dhcp_bootplen) { + void *n = realloc(ctx->dhcp_bootp, len); + if (n == NULL) { + logerr("%s: realloc", __func__); + return -1; + } + ctx->dhcp_bootp = n; + ctx->dhcp_bootplen = len; + } + + bootp = ctx->dhcp_bootp; memset(bootp, 0, sizeof(*bootp)); bootp->op = BOOTREPLY; bootp->flags = htons(ntohs(req->flags) & BROADCAST_FLAG); @@ -909,7 +930,7 @@ dhcp_output(const struct interface *ifp, const struct dhcp_lease *lease, ifp->if_name); else { logerr("%s: dhcp_addoptions", ifp->if_name); - return; + return -1; } } @@ -928,10 +949,9 @@ dhcp_output(const struct interface *ifp, const struct dhcp_lease *lease, bootp->xid, inet_ntoa(addr)); if (bootp->giaddr != INADDR_ANY || (bootp->ciaddr != INADDR_ANY && type != DHCP_NAK)) - dhcp_outputudp(ifp, len); - else - dhcp_outputipudp(ifp, &pool->dp_addr, len); - return; + return dhcp_outputudp(ifp, len); + + return dhcp_outputipudp(ifp, &pool->dp_addr, len); } static void @@ -1281,11 +1301,12 @@ dhcp_handlebootp(struct interface *ifp, struct bootp *bootp, size_t len, break; wanted = dhcp_lease_findaddr(ctx, &paddr); if (wanted != NULL && !dhcp_lease_avail(lease, wanted, &now)) { - logwarnx( - "%s: plugin assigned address unavailable: 0x%x %s", + logwarnx("%s: plugin assigned address in-use: 0x%x %s", ifp->if_name, bootp->xid, inet_ntoa(paddr)); +#if 0 paddr.s_addr = INADDR_ANY; wanted = NULL; +#endif } break; } @@ -1642,12 +1663,8 @@ dhcp_readudp0(struct dhcp_ctx *ctx, int fd, unsigned short events) bootp = ctx->dhcp_udp_buf; ifp = if_findifpfromcmsg(ctx->dhcp_ctx, &msg, NULL); if (ifp == NULL) { - /* This is a common situation for me when my tap - * interfaces come and go. */ -#if 0 logwarnx("dhcp: interface not found 0x%x %s", bootp->xid, inet_ntoa(from.sin_addr)); -#endif return; } @@ -1709,54 +1726,6 @@ dhcp_expire_leases(struct dhcp_ctx *ctx) dhcp_set_expire_timeout(ctx); } -int -dhcp_openbpf(struct interface *ifp) -{ - struct ctx *ctx = ifp->if_ctx; - struct dhcp_ctx *dhcp_ctx = ctx->ctx_dhcp; - struct ifreq ifr = { .ifr_mtu = 0 }; - size_t buflen; -#ifdef HAVE_CASPER - cap_rights_t rights; -#endif - - /* Ensure our bootp write buffer is as big as the MTU */ - strlcpy(ifr.ifr_name, ifp->if_name, sizeof(ifr.ifr_name)); - if (ioctl(ctx->ctx_pf_inet_fd, SIOCGIFMTU, &ifr, sizeof(ifr)) == -1) { - logerr("%s SIOCGIFMTU", __func__); - return -1; - } - buflen = (size_t)ifr.ifr_mtu - sizeof(struct udphdr) - - sizeof(struct ip); - if (buflen > dhcp_ctx->dhcp_bootplen) { - void *n = realloc(dhcp_ctx->dhcp_bootp, buflen); - if (n == NULL) { - logerr("%s: realloc", __func__); - return -1; - } - dhcp_ctx->dhcp_bootp = n; - dhcp_ctx->dhcp_bootplen = buflen; - } - - /* - * We only write to BPF, we don't read as we get the - * same data from the UDP socket even for unconfigured clients. - */ - ifp->if_bpf = bpf_open(ifp, bpf_bootp, O_WRONLY); - if (ifp->if_bpf == NULL) - return -1; - -#ifdef HAVE_CASPER - cap_rights_init(&rights, CAP_WRITE); - if (caph_rights_limit(ifp->if_bpf->bpf_fd, &rights) == -1) { - logerr("%s: caph_rights_limit", __func__); - return -1; - } -#endif - - return 0; -} - static int dhcp_open(void) { @@ -1814,6 +1783,7 @@ dhcp_open(void) goto errexit; } + logdebugx("dhcp: listening on port %d", BOOTPS); return s; errexit: diff --git a/src/dhcp.h b/src/dhcp.h index 0da0f65..cc6a3f1 100644 --- a/src/dhcp.h +++ b/src/dhcp.h @@ -152,12 +152,15 @@ struct bootp { #define DHCP_TTL 128 #define DHCP_LEASE_TIME 3600 +struct interface; + struct dhcp_pool { struct in_addr dp_addr; struct in_addr dp_mask; struct in_addr dp_from; struct in_addr dp_to; uint32_t dp_lease_time; + struct interface *dp_ifp; }; #define DHCP_CLIENTID_LEN 1 + 255 /* first byte is length */ @@ -213,11 +216,8 @@ struct dhcp_ctx { #endif }; -struct interface; - struct dhcp_ctx *dhcp_new(struct ctx *); void dhcp_free(struct dhcp_ctx *); -int dhcp_openbpf(struct interface *); void dhcp_expire_leases(struct dhcp_ctx *ctx); struct dhcp_lease *dhcp_alloclease(void); @@ -226,6 +226,7 @@ struct dhcp_lease *dhcp_newleaseaddr(struct dhcp_ctx *, const struct dhcp_lease *); /* For plugins to interogate the DHCP message */ +int dhcp_cookiecmp(const struct bootp *); const uint8_t *dhcp_findoption(const struct bootp *, size_t, uint8_t); const char *dhcp_ftoa(uint32_t); diff --git a/src/dhcpsd.8.in b/src/dhcpsd.8.in index d022123..039b306 100644 --- a/src/dhcpsd.8.in +++ b/src/dhcpsd.8.in @@ -24,7 +24,7 @@ .\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF .\" SUCH DAMAGE. .\" -.Dd July 25, 2025 +.Dd April 16, 2026 .Dt DHCPSD 8 .Os .Sh NAME @@ -32,7 +32,7 @@ .Nd a DHCP server .Sh SYNOPSIS .Nm -.Op Fl df +.Op Fl dfw .Op Fl p Ar plugin .Sh DESCRIPTION .Nm @@ -79,6 +79,14 @@ is appended and it is loaded from the plugin directory You may use the .Fl p option as many times as you have plugins. +.It Fl w +Waits until a message is received on an interface before configuring it. +This allows +.Nm +to work with dynamically created interfaces that are in the ready state. +If an interface is marked down or departs then +.Nm +will deactivate the interface. .El .Sh NOTES .Nm diff --git a/src/dhcpsd.c b/src/dhcpsd.c index 8b6c999..82385d8 100644 --- a/src/dhcpsd.c +++ b/src/dhcpsd.c @@ -53,6 +53,7 @@ #include "if.h" #include "logerr.h" #include "plugin.h" +#include "priv.h" #include "queue.h" #include "service.h" #include "unpriv.h" @@ -95,10 +96,12 @@ dhcpsd_signal_cb(int sig, void *arg) #define SIGMSG "received %s, %s" switch (sig) { case SIGINT: - loginfox(SIGMSG, "SIGINT", "stopping"); + if (ctx->ctx_options & DHCPSD_MAIN) + loginfox(SIGMSG, "SIGINT", "stopping"); break; case SIGTERM: - loginfox(SIGMSG, "SIGTERM", "stopping"); + if (ctx->ctx_options & DHCPSD_MAIN) + loginfox(SIGMSG, "SIGTERM", "stopping"); exit_code = EXIT_SUCCESS; break; default: @@ -106,12 +109,17 @@ dhcpsd_signal_cb(int sig, void *arg) return; } + ctx->ctx_options |= DHCPSD_EXITING; eloop_exit(ctx->ctx_eloop, exit_code); } int dhcpsd_dropperms(int do_chroot) { +#ifdef ASAN + logwarnx("not dropping permissions due to ASAN"); + UNUSED(do_chroot); +#else struct passwd *pw; pw = getpwnam(DHCPSD_USER); @@ -141,6 +149,7 @@ dhcpsd_dropperms(int do_chroot) logerr("%s: error dropping privileges", __func__); return -1; } +#endif return 0; } @@ -200,7 +209,7 @@ dhcpsd_fork(struct ctx *ctx) { int fork_fd[2]; pid_t pid; -#ifdef HAVE_CASPER +#ifdef HAVE_CASPERx cap_rights_t rights; #endif @@ -218,7 +227,8 @@ dhcpsd_fork(struct ctx *ctx) case 0: ctx->ctx_fork_fd = fork_fd[1]; close(fork_fd[0]); -#ifdef HAVE_CASPER +#ifdef HAVE_CASPERx + /* XXX This is failing on FreeBSD-15 */ cap_rights_init(&rights, CAP_WRITE); if (caph_rights_limit(ctx->ctx_fork_fd, &rights) == -1) { logerr("%s: caph_rights_limit", __func__); @@ -265,7 +275,7 @@ dhcpsd_fork(struct ctx *ctx) ctx->ctx_fork_fd = fork_fd[0]; close(fork_fd[1]); -#ifdef HAVE_CASPER +#ifdef HAVE_CASPERx cap_rights_init(&rights, CAP_READ); if (caph_rights_limit(ctx->ctx_fork_fd, &rights) == -1) { logerr("%s: caph_rights_limit", __func__); @@ -293,6 +303,37 @@ dhcpsd_send_launcher(struct ctx *ctx, int exit_code) ctx->ctx_fork_fd = -1; } +ssize_t +dhcpsd_configure_pools(struct interface *ifp) +{ + struct ctx *ctx = ifp->if_ctx; + ssize_t npools = 0; + struct plugin *p; + + if (!(ifp->if_flags & IF_ACTIVE)) + return 0; + + PLUGIN_FOREACH(ctx, p) + { + if (p->p_configure_pools == NULL) + continue; + ssize_t n = p->p_configure_pools(p, ifp); + if (n == -1 && ctx->ctx_argc != 0) + return -1; + if (n == -1 || n == 0) + continue; + /* XXX When we grow DHCPv6 only open BPF if we configure + * a DHCPv4 pool. */ + if (priv_openbpf(ifp) == -1) + return -1; + /* First plugin with config wins */ + npools += n; + break; + } + + return npools; +} + int main(int argc, char **argv) { @@ -306,7 +347,8 @@ main(int argc, char **argv) .ctx_pf_link_fd = -1, #endif }; - int ch, exit_code = EXIT_FAILURE, f_flag = 0; + int ch, exit_code = EXIT_FAILURE; + bool f_flag = false; size_t npools; struct interface *ifp; unsigned int logopts = LOGERR_LOG; @@ -321,17 +363,22 @@ main(int argc, char **argv) closefrom(STDERR_FILENO + 1); logopts |= LOGERR_ERR; // log to stderr -#define OPTS "dfp:" +#define OPTS "dfp:w" while ((ch = getopt(argc, argv, OPTS)) != -1) { switch (ch) { case 'd': logopts |= LOGERR_DEBUG; break; case 'f': - f_flag = 1; + f_flag = true; logopts |= LOGERR_ERR; // log to stderr logopts &= ~LOGERR_LOG; // don't syslog break; + case 'w': + ctx.ctx_options |= DHCPSD_WAITIF; + break; + case '?': + goto exit; } } @@ -379,6 +426,20 @@ main(int argc, char **argv) argc -= optind; argv += optind; + ctx.ctx_argc = argc; + ctx.ctx_argv = argv; + + if (priv_init(&ctx) == NULL) { + logerr("%s: priv_init", __func__); + goto exit; + } + if (ctx.ctx_options & DHCPSD_RUN) + goto open_pf_inet; + + if (unpriv_init(&ctx) == NULL) { + logerr("%s: unpriv_init", __func__); + goto exit; + } if (ctx.ctx_plugins == NULL) { loginfox("no plugins specified"); @@ -386,14 +447,12 @@ main(int argc, char **argv) goto exit; if (plugin_load(&ctx, "leasefile") == -1) goto exit; - } else { - if (unpriv_init(&ctx) == NULL) - goto exit; } PLUGIN_FOREACH(&ctx, p) { - if (ctx.ctx_options & DHCPSD_UNPRIV && !p->p_unpriv) { + if (ctx.ctx_options & DHCPSD_PRIV || + (ctx.ctx_options & DHCPSD_UNPRIV && !p->p_unpriv)) { plugin_unload(p); continue; } @@ -406,9 +465,7 @@ main(int argc, char **argv) } } - if (ctx.ctx_options & DHCPSD_RUN) - goto run; - +open_pf_inet: ctx.ctx_pf_inet_fd = xsocket(PF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0); if (ctx.ctx_pf_inet_fd == -1) { logerr("%s: PF_INET", __func__); @@ -423,31 +480,36 @@ main(int argc, char **argv) } #endif - if (getifaddrs(&ctx.ctx_ifa) == -1) { - logerr("%s: getifaddrs", __func__); + if (ctx.ctx_options & DHCPSD_RUN) + goto run; + + if (link_open(&ctx) == -1) { + logerr("%s: link_open", __func__); goto exit; } - if_learnifaces(&ctx); - npools = 0; - TAILQ_FOREACH(ifp, ctx.ctx_ifaces, if_next) { - if (argc == 0) - ifp->if_flags |= IF_ACTIVE; - else { - for (ch = 0; ch < argc; ch++) { - if (strcmp(argv[ch], ifp->if_name) == 0) { - ifp->if_flags |= IF_ACTIVE; - argv[ch][0] = '\0'; - break; + if (!(ctx.ctx_options & DHCPSD_WAITIF)) { + if_learnifaces(&ctx); + TAILQ_FOREACH(ifp, ctx.ctx_ifaces, if_next) { + if (argc == 0) + ifp->if_flags |= IF_ACTIVE; + else { + for (ch = 0; ch < argc; ch++) { + if (strcmp(argv[ch], ifp->if_name) == + 0) { + ifp->if_flags |= IF_ACTIVE; + argv[ch][0] = '\0'; + break; + } } } } - } - for (ch = 0; ch < argc; ch++) { - if (argv[ch][0] != '\0') { - logerrx("%s: no such interface", argv[ch]); - goto exit; + for (ch = 0; ch < argc; ch++) { + if (argv[ch][0] != '\0') { + logerrx("%s: no such interface", argv[ch]); + goto exit; + } } } @@ -461,36 +523,22 @@ main(int argc, char **argv) goto exit; } + npools = 0; TAILQ_FOREACH(ifp, ctx.ctx_ifaces, if_next) { if (!(ifp->if_flags & IF_ACTIVE)) continue; - PLUGIN_FOREACH(&ctx, p) - { - if (p->p_configure_pools == NULL) - continue; - ssize_t n = p->p_configure_pools(p, ifp); - if (n == -1 && argc != 0) - goto exit; - if (n == -1 || n == 0) - continue; - /* XXX When we grow DHCPv6 only open BPF if we configure - * a DHCPv4 pool. */ - if (dhcp_openbpf(ifp) == -1) - goto exit; - /* First plugin with config wins */ - npools += (size_t)n; - break; - } + ssize_t n = dhcpsd_configure_pools(ifp); + if (n == -1 && argc != 0) + goto exit; + if (n == -1 || n == 0) + continue; + npools += (size_t)n; } - if (npools == 0) { + if (!(ctx.ctx_options & DHCPSD_WAITIF) && npools == 0) { logerrx("no pools, nothing to serve"); goto exit; } - /* May as well free this now */ - freeifaddrs(ctx.ctx_ifa); - ctx.ctx_ifa = NULL; - loginfox("dropping to user: %s", DHCPSD_USER); if (dhcpsd_dropperms(1) == -1) goto exit; @@ -603,8 +651,6 @@ main(int argc, char **argv) exit: plugin_unloadall(&ctx); - if (ctx.ctx_ifa) - freeifaddrs(ctx.ctx_ifa); while ((ifp = TAILQ_FIRST(ctx.ctx_ifaces)) != NULL) { TAILQ_REMOVE(ctx.ctx_ifaces, ifp, if_next); if_free(ifp); @@ -613,12 +659,14 @@ main(int argc, char **argv) eloop_free(ctx.ctx_eloop); ctx.ctx_eloop = NULL; srv_free(ctx.ctx_unpriv); + srv_free(ctx.ctx_priv); + link_free(&ctx); #ifdef HAVE_CASPER if (ctx.ctx_capnet) cap_close(ctx.ctx_capnet); #endif if (ctx.ctx_options & DHCPSD_MAIN) { - logdebugx("dhcpsd exited"); + loginfox("dhcpsd exited"); dhcpsd_send_launcher(&ctx, exit_code); } diff --git a/src/dhcpsd.h b/src/dhcpsd.h index acea3ef..b79f950 100644 --- a/src/dhcpsd.h +++ b/src/dhcpsd.h @@ -31,7 +31,7 @@ #include -struct ifaddrs; +struct link_ctx; struct dhcp_ctx; struct eloop; struct if_head; @@ -42,21 +42,27 @@ typedef struct cap_channel cap_channel_t; #endif struct ctx { - struct ifaddrs *ctx_ifa; + int ctx_argc; + char **ctx_argv; struct dhcp_ctx *ctx_dhcp; struct eloop *ctx_eloop; struct if_head *ctx_ifaces; + struct srv_ctx *ctx_priv; struct srv_ctx *ctx_unpriv; struct plugin *ctx_plugins; size_t ctx_nplugins; unsigned int ctx_options; #define DHCPSD_RUN (1U << 0) /* Set by forked stuff */ -#define DHCPSD_MAIN (1U << 1) /* Main process */ -#define DHCPSD_UNPRIV (1U << 2) /* Unprivileged helper */ -#define DHCPSD_LAUNCHER (1U << 3) /* Launcher process */ +#define DHCPSD_EXITING (1U << 1) /* process will exit */ +#define DHCPSD_MAIN (1U << 2) /* Main process */ +#define DHCPSD_PRIV (1U << 3) /* Privileged helper */ +#define DHCPSD_UNPRIV (1U << 4) /* Unprivileged helper */ +#define DHCPSD_LAUNCHER (1U << 5) /* Launcher process */ +#define DHCPSD_WAITIF (1U << 6) /* wait for interfaces */ + struct link_ctx *ctx_link; int ctx_fork_fd; int ctx_pf_inet_fd; #ifdef IFLR_ACTIVE @@ -67,6 +73,16 @@ struct ctx { #endif }; +struct interface; + int dhcpsd_dropperms(int); +ssize_t dhcpsd_configure_pools(struct interface *); + +/* + * This is defined by the OS specific link mechanism: + * route(4) or rtnetlink(7) + */ +int link_open(struct ctx *); +void link_free(struct ctx *); #endif /* CTX_H */ diff --git a/src/if.c b/src/if.c index ed16ecb..1090ee4 100644 --- a/src/if.c +++ b/src/if.c @@ -33,6 +33,14 @@ #include #include #include + +#include + +#include "queue.h" +#include "src/common.h" +#include "src/priv.h" +#include "src/unpriv.h" + #ifdef AF_LINK #include #include @@ -43,6 +51,7 @@ #include #endif +#include #include #include #include @@ -58,121 +67,185 @@ #include "if_none.h" #include "logerr.h" -int -if_learnifaces(struct ctx *ctx) +void +if_update_output(struct interface *ifp) { - struct ifaddrs *ifa; - struct interface *ifp; -#ifdef AF_LINK - struct sockaddr_dl *sdl; -#ifdef IFLR_ACTIVE - struct if_laddrreq iflr = { .flags = IFLR_PREFIX }; -#endif -#elif defined(AF_PACKET) - struct sockaddr_ll *sll; -#endif + switch (ifp->if_hwtype) { + case ARPHRD_ETHER: + ifp->if_output = if_ether_output; + break; + default: + ifp->if_output = if_none_output; + break; + } +} - for (ifa = ctx->ctx_ifa; ifa; ifa = ifa->ifa_next) { - if (ifa->ifa_addr == NULL) - continue; +void +if_update(struct interface *ifp, struct sockaddr *sa) +{ #ifdef AF_LINK - if (ifa->ifa_addr->sa_family != AF_LINK) - continue; -#elif defined(AF_PACKET) - if (ifa->ifa_addr->sa_family != AF_PACKET) - continue; -#endif - -#ifdef IFLR_ACTIVE - sdl = (void *)ifa->ifa_addr; - - /* We need to check for active address */ - strlcpy(iflr.iflr_name, ifa->ifa_name, sizeof(iflr.iflr_name)); - memcpy(&iflr.addr, ifa->ifa_addr, - MIN(ifa->ifa_addr->sa_len, sizeof(iflr.addr))); - iflr.flags = IFLR_PREFIX; - iflr.prefixlen = (unsigned int)sdl->sdl_alen * NBBY; - if (ioctl(ctx->ctx_pf_link_fd, SIOCGLIFADDR, &iflr) == -1 || - !(iflr.flags & IFLR_ACTIVE)) - continue; -#endif - - ifp = calloc(1, sizeof(*ifp)); - if (ifp == NULL) { - logerr("%s: malloc", __func__); - return -1; - } - ifp->if_ctx = ctx; - strlcpy(ifp->if_name, ifa->ifa_name, sizeof(ifp->if_name)); + struct sockaddr_dl *sdl = (void *)sa; -#ifdef AF_LINK -#ifndef IFLR_ACTIVE - sdl = (void *)ifa->ifa_addr; -#endif + ifp->if_index = sdl->sdl_index; - switch (sdl->sdl_type) { + switch (sdl->sdl_type) { #ifdef IFT_BRIDGE - case IFT_BRIDGE: /* FALLTHROUGH */ + case IFT_BRIDGE: /* FALLTHROUGH */ #endif #ifdef IFT_PROPVIRTUAL - case IFT_PROPVIRTUAL: /* FALLTHROUGH */ + case IFT_PROPVIRTUAL: /* FALLTHROUGH */ #endif #ifdef IFT_TUNNEL - case IFT_TUNNEL: /* FALLTHROUGH */ + case IFT_TUNNEL: /* FALLTHROUGH */ #endif - case IFT_LOOP: /* FALLTHROUGH */ - case IFT_PPP: /* FALLTHROUGH */ + case IFT_LOOP: /* FALLTHROUGH */ + case IFT_PPP: /* FALLTHROUGH */ #ifdef IFT_L2VLAN - case IFT_L2VLAN: /* FALLTHROUGH */ + case IFT_L2VLAN: /* FALLTHROUGH */ #endif #ifdef IFT_L3IPVLAN - case IFT_L3IPVLAN: /* FALLTHROUGH */ + case IFT_L3IPVLAN: /* FALLTHROUGH */ #endif - case IFT_ETHER: - ifp->if_hwtype = ARPHRD_ETHER; - break; + case IFT_ETHER: + ifp->if_hwtype = ARPHRD_ETHER; + break; #ifdef notyet #ifdef IFT_IEEE1394 - case IFT_IEEE1394: - ifp->if_hwtype = ARPHRD_IEEE1394; - break; + case IFT_IEEE1394: + ifp->if_hwtype = ARPHRD_IEEE1394; + break; #endif #ifdef IFT_INFINIBAND - case IFT_INFINIBAND: - ifp->if_hwtype = ARPHRD_INFINIBAND; - break; + case IFT_INFINIBAND: + ifp->if_hwtype = ARPHRD_INFINIBAND; + break; #endif #endif - default: - logdebugx("%s: unsupported interface type 0x%.2x", - ifp->if_name, sdl->sdl_type); - break; - } - ifp->if_index = sdl->sdl_index; + default: + logdebugx("%s: unsupported interface type 0x%.2x", ifp->if_name, + sdl->sdl_type); + break; + } + + if (sdl->sdl_alen <= sizeof(ifp->if_hwaddr)) { ifp->if_hwlen = sdl->sdl_alen; - if (ifp->if_hwlen != 0) - memcpy(ifp->if_hwaddr, LLADDR(sdl), ifp->if_hwlen); + memcpy(ifp->if_hwaddr, LLADDR(sdl), sdl->sdl_alen); + } #elif defined(AF_PACKET) - sll = (void *)ifa->ifa_addr; - ifp->if_index = (unsigned int)sll->sll_ifindex; - ifp->if_hwtype = sll->sll_hatype; + struct sockaddr_ll *sll = (void *)sa; + ifp->if_index = (unsigned int)sll->sll_ifindex; + ifp->if_hwtype = sll->sll_hatype; + if (sll->sll_halen <= sizeof(ifp->if_hwaddr)) { ifp->if_hwlen = sll->sll_halen; - if (ifp->if_hwlen != 0) - memcpy(ifp->if_hwaddr, sll->sll_addr, ifp->if_hwlen); + memcpy(ifp->if_hwaddr, sll->sll_addr, sll->sll_halen); + } #endif - switch (ifp->if_hwtype) { - case ARPHRD_ETHER: - ifp->if_output = if_ether_output; - break; - default: - ifp->if_output = if_none_output; - break; +} + +int +if_update_mtu(struct interface *ifp) +{ + struct ctx *ctx = ifp->if_ctx; + struct ifreq ifr = { .ifr_mtu = 0 }; + + strlcpy(ifr.ifr_name, ifp->if_name, sizeof(ifr.ifr_name)); + if (ioctl(ctx->ctx_pf_inet_fd, SIOCGIFMTU, &ifr, sizeof(ifr)) == -1) { + logerr("%s SIOCGIFMTU", __func__); + return -1; + } + + ifp->if_mtu = ifr.ifr_mtu; + return 0; +} + +int +if_sockaddr_active(struct ctx *ctx, const char *if_name, + const struct sockaddr *sa) +{ +#ifdef IFLR_ACTIVE + const struct sockaddr_dl *sdl = (const void *)sa; + struct if_laddrreq iflr = { + .flags = IFLR_PREFIX, + .prefixlen = (unsigned int)sdl->sdl_alen * NBBY, + }; + + strlcpy(iflr.iflr_name, if_name, sizeof(iflr.iflr_name)); + memcpy(&iflr.addr, sa, MIN(sa->sa_len, sizeof(iflr.addr))); + + if (ioctl(ctx->ctx_pf_link_fd, SIOCGLIFADDR, &iflr) == -1) + return 0; + if (!(iflr.flags & IFLR_ACTIVE)) + return 0; +#else + UNUSED(ctx); + UNUSED(if_name); + UNUSED(sa); +#endif + + return 1; +} + +int +if_link_match_index(const struct sockaddr *sa, unsigned int if_index) +{ + if (sa_is_link(sa) != 1) { + errno = EINVAL; + return -1; + } + +#ifdef AF_LINK + const struct sockaddr_dl *sdl = (const void *)sa; + return sdl->sdl_index == if_index ? 1 : 0; +#elif defined(AF_PACKET) + const struct sockaddr_ll *sll = (const void *)sa; + return (unsigned int)sll->sll_ifindex == if_index ? 1 : 0; +#else +#error undefined platform +#endif +} + +int +if_learnifaces(struct ctx *ctx) +{ + struct ifaddrs *ifaddrs, *ifa; + struct interface *ifp; + int err = -1; + + if (unpriv_getifaddrs(ctx->ctx_unpriv, &ifaddrs, NULL) == -1) { + logerr("%s: unpriv_getifaddrs", __func__); + return -1; + } + for (ifa = ifaddrs; ifa; ifa = ifa->ifa_next) { + if (!sa_is_link(ifa->ifa_addr)) + continue; + + if (if_sockaddr_active(ctx, ifa->ifa_name, ifa->ifa_addr) != 1) + continue; + + ifp = calloc(1, sizeof(*ifp)); + if (ifp == NULL) { + logerr("%s: malloc", __func__); + goto err; + } + ifp->if_ctx = ctx; + strlcpy(ifp->if_name, ifa->ifa_name, sizeof(ifp->if_name)); + + if_update(ifp, ifa->ifa_addr); + if_update_output(ifp); + if (if_update_mtu(ifp) == -1) { + logerr("%s: if_update_mtu: %s", __func__, ifa->ifa_name); + free(ifp); + continue; } TAILQ_INSERT_TAIL(ctx->ctx_ifaces, ifp, if_next); } - return 0; + err = 0; + +err: + free(ifaddrs); + return err; } struct interface * @@ -182,7 +255,7 @@ if_findifpfromcmsg(struct ctx *ctx, struct msghdr *msg, void *to) unsigned int if_index = 0; struct interface *ifp; #ifdef IP_RECVIF - struct sockaddr_dl sdl; + struct sockaddr_dl sdl = { .sdl_len = 0 }; #else struct in_pktinfo ipi; #endif @@ -246,21 +319,82 @@ if_findifpfromcmsg(struct ctx *ctx, struct msghdr *msg, void *to) /* Find the receiving interface */ TAILQ_FOREACH(ifp, ctx->ctx_ifaces, if_next) { if (ifp->if_index == if_index) - return ifp; + break; + } + + if (ifp == NULL) { + int i; + + if (!(ctx->ctx_options & DHCPSD_WAITIF)) { + errno = ESRCH; + return NULL; + } + + ifp = calloc(1, sizeof(*ifp)); + if (ifp == NULL) + return NULL; + ifp->if_ctx = ctx; + ifp->if_index = if_index; + + if (unpriv_learnif(ifp) == -1) { + logerr("%s: unpriv_learnif", __func__); + /* Avoid neededless checks */ + free(ifp); + return NULL; + } + + if (ctx->ctx_argc == 0) + ifp->if_flags |= IF_ACTIVE; + else { + for (i = 0; i < ctx->ctx_argc; i++) { + if (strcmp(ifp->if_name, ctx->ctx_argv[i]) == + 0) { + ifp->if_flags |= IF_ACTIVE; + } + } + } + if (ifp->if_flags & IF_ACTIVE) { + logdebugx("%s: activated interface (%d)", ifp->if_name, + ifp->if_index); + if (dhcpsd_configure_pools(ifp) == -1) { + logerr("%s: dhcpsd_configure_pools", + ifp->if_name); + if_free(ifp); + return NULL; + } + } + TAILQ_INSERT_TAIL(ctx->ctx_ifaces, ifp, if_next); } - /* No support for learning new interfaces after we have loaded. */ - errno = ESRCH; return ifp; } void if_free(struct interface *ifp) { + bool srv_if_free; + unsigned int options; + if (ifp == NULL) return; + + /* If we are exiting there is no need to notify our services + * that we are freeing an interface. */ + options = ifp->if_ctx->ctx_options; + srv_if_free = options & DHCPSD_MAIN && !(options & DHCPSD_EXITING); + + if (srv_if_free) { + if (options & DHCPSD_WAITIF) + loginfox("%s: deactiving interface", ifp->if_name); + if (priv_freeif(ifp) == -1 && errno != ESRCH) + logerr("%s: priv_freeif: %s", __func__, ifp->if_name); + if (unpriv_freeif(ifp) == -1 && errno != ESRCH) + logerr("%s: unpriv_freeif: %s", __func__, ifp->if_name); + } if (ifp->if_bpf != NULL) bpf_close(ifp->if_bpf); free(ifp->if_pools); + if (srv_if_free && options & DHCPSD_WAITIF) + logdebugx("%s: deactivated interface", ifp->if_name); free(ifp); } diff --git a/src/if.h b/src/if.h index 5bcf762..efd8de8 100644 --- a/src/if.h +++ b/src/if.h @@ -72,6 +72,11 @@ TAILQ_HEAD(if_head, interface); struct ifaddrs; +int if_sockaddr_active(struct ctx *, const char *, const struct sockaddr *); +int if_link_match_index(const struct sockaddr *, unsigned int); +void if_update(struct interface *, struct sockaddr *); +int if_update_mtu(struct interface *); +void if_update_output(struct interface *); int if_learnifaces(struct ctx *); void if_free(struct interface *); struct interface *if_findifpfromcmsg(struct ctx *, struct msghdr *, void *); diff --git a/src/if_ether.c b/src/if_ether.c index 0b80250..359f3ff 100644 --- a/src/if_ether.c +++ b/src/if_ether.c @@ -57,6 +57,7 @@ if_ether_output(const struct interface *ifp, int fd, const struct iovec *_iov, errno = EINVAL; return -1; } + memcpy(eh.ether_shost, ifp->if_hwaddr, ifp->if_hwlen); if (ip->ip_p != IPPROTO_UDP || ntohs(bootp->flags) & BROADCAST_FLAG) memset(eh.ether_dhost, 0xff, sizeof(eh.ether_dhost)); diff --git a/src/netlink.c b/src/netlink.c new file mode 100644 index 0000000..59a2f28 --- /dev/null +++ b/src/netlink.c @@ -0,0 +1,249 @@ +/* SPDX-License-Identifier: BSD-2-Clause */ +/* + * dhcpsd - netlink(7) support + * Copyright (c) 2025 Roy Marples + * All rights reserved + + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.h" +#include "dhcpsd.h" +#include "eloop.h" +#include "if.h" +#include "logerr.h" +#include "queue.h" + +struct link_ctx { + struct ctx *link_ctx; + int link_fd; +}; + +static int +netlink_get(struct link_ctx *lctx, struct iovec *iov, int fd, int flags, + int (*cb)(struct link_ctx *, struct nlmsghdr *)) +{ + struct sockaddr_nl nladdr = { .nl_pid = 0 }; + struct msghdr msg = { + .msg_name = &nladdr, + .msg_namelen = sizeof(nladdr), + .msg_iov = iov, + .msg_iovlen = 1, + }; + ssize_t len; + struct nlmsghdr *nlm; + int r = 0; + unsigned int again; + bool terminated; + +recv_again: + len = recvmsg(fd, &msg, flags); + if (len == 0 || len == -1) + return (int)len; + + /* Check sender */ + if (msg.msg_namelen != sizeof(nladdr)) { + errno = EINVAL; + return -1; + } + + /* Ignore message if it is not from kernel */ + if (nladdr.nl_pid != 0) + return 0; + + again = 0; + terminated = false; + for (nlm = iov->iov_base; nlm && NLMSG_OK(nlm, len); + nlm = NLMSG_NEXT(nlm, len)) { + again = (nlm->nlmsg_flags & NLM_F_MULTI); + if (nlm->nlmsg_type == NLMSG_NOOP) + continue; + + if (nlm->nlmsg_type == NLMSG_ERROR) { + struct nlmsgerr *err; + + if (nlm->nlmsg_len - sizeof(*nlm) < sizeof(*err)) { + errno = EBADMSG; + return -1; + } + err = (struct nlmsgerr *)NLMSG_DATA(nlm); + if (err->error != 0) { + errno = -err->error; + return -1; + } + again = 0; + terminated = true; + break; + } + if (nlm->nlmsg_type == NLMSG_DONE) { + again = 0; + terminated = true; + break; + } + if (cb == NULL) + continue; + r = cb(lctx, nlm); + } + + if (again || !terminated) + goto recv_again; + + return r; +} + +static int +netlink_link(struct link_ctx *lctx, struct nlmsghdr *nlm) +{ + struct ctx *ctx = lctx->link_ctx; + size_t len; + struct ifinfomsg *ifi; + struct interface *ifp; + + len = nlm->nlmsg_len - sizeof(*nlm); + if ((size_t)len < sizeof(*ifi)) { + errno = EBADMSG; + return -1; + } + + ifi = NLMSG_DATA(nlm); + if (nlm->nlmsg_type != RTM_DELLINK && ifi->ifi_flags & IFF_UP) + return 0; + + TAILQ_FOREACH(ifp, ctx->ctx_ifaces, if_next) { + if (ifp->if_index == (unsigned int)ifi->ifi_index) + break; + } + if (ifp == NULL) + return 0; + + if (nlm->nlmsg_type == RTM_DELLINK) + loginfox("%s: interface has departed", ifp->if_name); + else if (!(ifi->ifi_flags & IFF_UP)) + loginfox("%s: interface is down", ifp->if_name); + + TAILQ_REMOVE(ctx->ctx_ifaces, ifp, if_next); + if_free(ifp); + return 0; +} + +static int +netlink_dispatch(struct link_ctx *lctx, struct nlmsghdr *nlm) +{ + switch (nlm->nlmsg_type) { + case RTM_DELLINK: + case RTM_NEWLINK: + return netlink_link(lctx, nlm); + } + return 0; +} + +static void +netlink_handle(void *arg, unsigned short e) +{ + struct link_ctx *lctx = arg; + unsigned char buf[16 * 1024]; + struct iovec iov = { + .iov_base = buf, + .iov_len = sizeof(buf), + }; + +#ifdef NDEBUG + UNUSED(e); +#else + assert(e == ELE_READ); +#endif + + if (netlink_get(lctx, &iov, lctx->link_fd, MSG_DONTWAIT, + netlink_dispatch) == -1) + logerr("%s: netlink_get", __func__); +} + +int +link_open(struct ctx *ctx) +{ + struct link_ctx *lctx = malloc(sizeof(*lctx)); + struct sockaddr_nl nl = { .nl_family = AF_NETLINK, + .nl_groups = RTMGRP_LINK }; +#ifdef NETLINK_BROADCAST_ERROR + int on = 1; +#endif + + if (lctx == NULL) + return -1; + + /* lctx will be freed at exit regardless if any error below + * so there is no need to free it on an error path. */ + lctx->link_ctx = ctx; + ctx->ctx_link = lctx; + + lctx->link_fd = xsocket(AF_NETLINK, SOCK_RAW | SOCK_CXNB, + NETLINK_ROUTE); + if (lctx->link_fd == -1) + return -1; + + if (bind(lctx->link_fd, (struct sockaddr *)&nl, sizeof(nl)) == -1) { + logerr("%s: bind", __func__); + return -1; + } + + /* netlink socket can overflow if the kernel sends too many messages. + * We need to reliably track state and if we can't we need to know. */ +#ifdef NETLINK_BROADCAST_ERROR + if (setsockopt(lctx->link_fd, SOL_NETLINK, NETLINK_BROADCAST_ERROR, &on, + sizeof(on)) == -1) + logerr("%s: NETLINK_BROADCAST_ERROR", __func__); +#endif + + if (eloop_event_add(ctx->ctx_eloop, lctx->link_fd, ELE_READ, + netlink_handle, lctx) == -1) { + logerr("%s: eloop_event_add", __func__); + return -1; + } + + return 0; +} + +void +link_free(struct ctx *ctx) +{ + struct link_ctx *lctx = ctx->ctx_link; + + if (lctx == NULL) + return; + if (lctx->link_fd != -1) + close(lctx->link_fd); + free(lctx); + ctx->ctx_link = NULL; +} diff --git a/src/plugins/auto.c b/src/plugins/auto.c index b5af136..074d059 100644 --- a/src/plugins/auto.c +++ b/src/plugins/auto.c @@ -44,6 +44,7 @@ #include "if.h" #include "logerr.h" #include "plugin.h" +#include "unpriv.h" static const char auto_name[] = "auto"; static const char auto_description[] = "Automatic DHCP configuration"; @@ -65,7 +66,8 @@ inet_private_address(struct in_addr *addr) static ssize_t auto_configure_pools(__unused struct plugin *p, struct interface *ifp) { - struct ifaddrs *ifa; + struct srv_ctx *sctx = ifp->if_ctx->ctx_unpriv; + struct ifaddrs *ifaddrs, *ifa; struct sockaddr_in *sin_addr, *sin_mask; uint8_t *ip, cidr; struct dhcp_pool *pool; @@ -73,7 +75,11 @@ auto_configure_pools(__unused struct plugin *p, struct interface *ifp) const char *fromp, *top; ssize_t npools = 0; - for (ifa = ifp->if_ctx->ctx_ifa; ifa; ifa = ifa->ifa_next) { + if (unpriv_getifaddrs(sctx, &ifaddrs, &ifp->if_index) == -1) { + logerr("%s: unpriv_getifaddrs %d:", __func__, ifp->if_index); + return -1; + } + for (ifa = ifaddrs; ifa; ifa = ifa->ifa_next) { if (ifa->ifa_addr == NULL) continue; if (ifa->ifa_flags & IFF_LOOPBACK) @@ -113,6 +119,7 @@ auto_configure_pools(__unused struct plugin *p, struct interface *ifp) sizeof(*pool) * (ifp->if_npools + 1)); if (pool == NULL) { logerr("%s: realloc", __func__); + free(ifaddrs); return -1; } ifp->if_pools = pool; @@ -146,6 +153,7 @@ auto_configure_pools(__unused struct plugin *p, struct interface *ifp) npools++; } + free(ifaddrs); return npools; } diff --git a/src/plugins/lua.c b/src/plugins/lua.c index f3a7479..9276615 100644 --- a/src/plugins/lua.c +++ b/src/plugins/lua.c @@ -95,7 +95,6 @@ lua_run_configure_pools(struct plugin *p, struct srv_ctx *sctx, ssize_t err = -1; struct dhcp_pool *pool, *pools = NULL; int laddress, lnetmask, lfrom, lto, llease_time; - uint8_t cidr; const char *saddress, *snetmask, *sfrom, *sto; in_addr_t address, netmask, from, to; uint32_t lease_time; @@ -147,6 +146,7 @@ lua_run_configure_pools(struct plugin *p, struct srv_ctx *sctx, lnetmask = lua_getfield(L, -2, "netmask"); lfrom = lua_getfield(L, -3, "from"); lto = lua_getfield(L, -4, "to"); + llease_time = lua_getfield(L, -5, "lease_time"); if (laddress != LUA_TSTRING) { logerrx("%s: no address in pool", ifname); goto skip; @@ -163,10 +163,10 @@ lua_run_configure_pools(struct plugin *p, struct srv_ctx *sctx, logerrx("%s: no too in pool", ifname); goto skip; } - saddress = lua_tostring(L, -4); - snetmask = lua_tostring(L, -3); - sfrom = lua_tostring(L, -2); - sto = lua_tostring(L, -1); + saddress = lua_tostring(L, -5); + snetmask = lua_tostring(L, -4); + sfrom = lua_tostring(L, -3); + sto = lua_tostring(L, -2); if (inet_pton(AF_INET, saddress, &address) != 1) { logerrx("%s: not an ip address %s", lua_name, saddress); goto skip; @@ -183,8 +183,6 @@ lua_run_configure_pools(struct plugin *p, struct srv_ctx *sctx, logerrx("%s: not an ip address %s", lua_name, sto); goto skip; } - - llease_time = lua_getfield(L, -5, "lease_time"); if (llease_time == LUA_TNUMBER) lease_time = (uint32_t)lua_tonumber(L, -1); else @@ -202,19 +200,10 @@ lua_run_configure_pools(struct plugin *p, struct srv_ctx *sctx, pool->dp_mask.s_addr = netmask; pool->dp_from.s_addr = from; pool->dp_to.s_addr = to; - cidr = inet_ntocidr(&pool->dp_mask); pool->dp_lease_time = lease_time; - if (lease_time != 0) - loginfox("%s: %s: pool for %s/%d: %s - %s (%u secs)", - ifname, lua_name, saddress, cidr, sfrom, sto, - lease_time); - else - loginfox("%s: %s: pool for %s/%d: %s - %s", ifname, - lua_name, saddress, cidr, sfrom, sto); skip: - lua_pop(L, 4); - + lua_pop(L, 5); lua_gettable(L, -1); lua_pop(L, 1); if (!array) @@ -322,8 +311,9 @@ lua_run_lookup_addr(struct plugin *p, struct srv_ctx *sctx, const void *data, /* Aligns bootp */ memmove(UNCONST(data), datap, len); - if (len < sizeof(*l->l_req)) { - /* Allow for a truncated vendor area if DHCP */ + /* Allow for a truncated vendor area if it can at least + * contain a DHCP cookie. */ + if (len < offsetof(struct bootp, vend) + sizeof(uint32_t)) { if (len < offsetof(struct bootp, vend) + sizeof(uint32_t) || dhcp_cookiecmp(l->l_req) != 0) { errno = EINVAL; @@ -966,7 +956,9 @@ lua_configure_pools(struct plugin *p, struct interface *ifp) ssize_t err, result; void *_pools; size_t poolslen, npools; - struct dhcp_pool *pool; + struct dhcp_pool *pool, *epool; + uint32_t cidr; + char addr[INET_ADDRSTRLEN], from[INET_ADDRSTRLEN], to[INET_ADDRSTRLEN]; err = srv_run(p->p_ctx->ctx_unpriv, p, L_CONFIGUREPOOLS, ifp->if_name, strlen(ifp->if_name) + 1, &result, &_pools, &poolslen); @@ -984,6 +976,22 @@ lua_configure_pools(struct plugin *p, struct interface *ifp) pool += ifp->if_npools; memcpy(pool, _pools, poolslen); ifp->if_npools += npools; + + for (pool = _pools, epool = pool + npools; pool != epool; pool++) { + inet_ntop(AF_INET, &pool->dp_addr, addr, sizeof(addr)); + cidr = inet_ntocidr(&pool->dp_mask); + inet_ntop(AF_INET, &pool->dp_from, from, sizeof(from)); + inet_ntop(AF_INET, &pool->dp_to, to, sizeof(to)); + + if (pool->dp_lease_time != 0) + loginfox("%s: %s: pool for %s/%d: %s - %s (%u secs)", + ifp->if_name, lua_name, addr, cidr, from, to, + pool->dp_lease_time); + else + loginfox("%s: %s: pool for %s/%d: %s - %s", + ifp->if_name, lua_name, addr, cidr, from, to); + } + return (ssize_t)npools; } diff --git a/src/priv.c b/src/priv.c new file mode 100644 index 0000000..e74a561 --- /dev/null +++ b/src/priv.c @@ -0,0 +1,257 @@ +/* SPDX-License-Identifier: BSD-2-Clause */ +/* + * dhcpsd - privileged service helper + * Copyright (c) 2025 Roy Marples + * All rights reserved + + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "bpf.h" +#include "common.h" +#include "dhcpsd.h" +#include "if.h" +#include "logerr.h" +#include "plugin.h" +#include "priv.h" +#include "queue.h" +#include "service.h" + +#define P_OPENBPF 1 +#define P_SENDBPF 2 +#define P_FREEIF 3 + +int +priv_openbpf(struct interface *ifp) +{ + struct srv_ctx *sctx = ifp->if_ctx->ctx_priv; + ssize_t result; + int err; + + err = srv_run(sctx, NULL, P_OPENBPF, ifp, sizeof(*ifp), &result, NULL, + 0); + return err == -1 ? -1 : (int)result; +} + +ssize_t +priv_sendbpf(struct interface *ifp, const struct iovec *iov, size_t iov_len) +{ + assert(iov_len == 3); + struct srv_ctx *sctx = ifp->if_ctx->ctx_priv; + struct iovec iov0[] = { + { .iov_base = UNCONST(&ifp->if_index), sizeof(ifp->if_index) }, + { .iov_base = iov[0].iov_base, .iov_len = iov[0].iov_len }, + { .iov_base = iov[1].iov_base, .iov_len = iov[1].iov_len }, + { .iov_base = iov[2].iov_base, .iov_len = iov[2].iov_len }, + }; + ssize_t result; + int err; + + err = srv_runv(sctx, NULL, P_SENDBPF, iov0, ARRAYCOUNT(iov0), &result, + NULL, 0); + return err == -1 ? -1 : (int)result; +} + +int +priv_freeif(struct interface *ifp) +{ + struct srv_ctx *sctx = ifp->if_ctx->ctx_priv; + ssize_t result; + int err; + + err = srv_run(sctx, NULL, P_FREEIF, &ifp->if_index, + sizeof(ifp->if_index), &result, NULL, 0); + return err == -1 ? -1 : (int)result; +} + +static ssize_t +priv_dispatch_openbpf(struct srv_ctx *sctx, const void *data, size_t len) +{ + struct ctx *ctx = sctx->srv_ctx; + const struct interface *iff = data; + struct interface *ifp; + ssize_t err = -1; + + if (len != sizeof(*iff)) { + errno = EINVAL; + goto err; + } + + TAILQ_FOREACH(ifp, ctx->ctx_ifaces, if_next) { + if (ifp->if_index == iff->if_index) + break; + } + if (ifp == NULL) { + ifp = calloc(1, sizeof(*ifp)); + ifp->if_ctx = ctx; + ifp->if_index = iff->if_index; + if (strlcpy(ifp->if_name, iff->if_name, sizeof(ifp->if_name)) > + sizeof(iff->if_name)) { + free(ifp); + errno = EINVAL; + goto err; + } + ifp->if_flags = iff->if_flags; + ifp->if_hwtype = iff->if_hwtype; + if (iff->if_hwlen > sizeof(ifp->if_hwaddr)) { + free(ifp); + errno = EINVAL; + goto err; + } + ifp->if_hwlen = iff->if_hwlen; + memcpy(ifp->if_hwaddr, iff->if_hwaddr, sizeof(iff->if_hwaddr)); + if_update_output(ifp); + TAILQ_INSERT_TAIL(ctx->ctx_ifaces, ifp, if_next); + } + + if (ifp->if_bpf == NULL) { + /* + * We only write to BPF, we don't read as we get the + * same data from the UDP socket even for unconfigured clients. + */ + ifp->if_bpf = bpf_open(ifp, bpf_bootp, O_WRONLY); + if (ifp->if_bpf != NULL) + err = 0; + } + +err: + return srv_send(sctx, NULL, P_OPENBPF, err, NULL, 0); +} + +static ssize_t +priv_dispatch_sendbpf(struct srv_ctx *sctx, const void *data, size_t len) +{ + struct ctx *ctx = sctx->srv_ctx; + unsigned int if_index; + struct interface *ifp; + struct iovec iov[] = { { .iov_base = (uint8_t *)UNCONST(data) + + sizeof(if_index), + .iov_len = len - sizeof(if_index) } }; + ssize_t err = -1; + + if (len <= sizeof(if_index)) { + errno = EINVAL; + goto err; + } + memcpy(&if_index, data, sizeof(if_index)); + len -= sizeof(if_index); + + TAILQ_FOREACH(ifp, ctx->ctx_ifaces, if_next) { + if (ifp->if_index == if_index) + break; + } + if (ifp == NULL) { + errno = ESRCH; + goto err; + } + + err = ifp->if_output(ifp, ifp->if_bpf->bpf_fd, iov, ARRAYCOUNT(iov)); + +err: + return srv_send(sctx, NULL, P_SENDBPF, err, NULL, 0); +} + +static ssize_t +priv_dispatch_freeif(struct srv_ctx *sctx, const void *data, size_t len) +{ + struct ctx *ctx = sctx->srv_ctx; + unsigned int if_index; + struct interface *ifp; + ssize_t err = -1; + + if (len != sizeof(if_index)) { + errno = EINVAL; + goto err; + } + memcpy(&if_index, data, sizeof(if_index)); + + TAILQ_FOREACH(ifp, ctx->ctx_ifaces, if_next) { + if (ifp->if_index == if_index) + break; + } + if (ifp == NULL) { + errno = ESRCH; + goto err; + } + + TAILQ_REMOVE(ctx->ctx_ifaces, ifp, if_next); + if_free(ifp); + err = 0; + +err: + return srv_send(sctx, NULL, P_FREEIF, err, NULL, 0); +} + +static ssize_t +priv_dispatch(struct srv_ctx *sctx, struct plugin *p, unsigned int cmd, + const void *data, size_t len) +{ + if (p != NULL) { + if (p->p_dispatch != NULL) + return p->p_dispatch(p, sctx, cmd, data, len); + errno = ENOSYS; + logerr(__func__); + return srv_send(sctx, NULL, cmd, -1, NULL, 0); + } + + switch (cmd) { + case P_OPENBPF: + return priv_dispatch_openbpf(sctx, data, len); + case P_SENDBPF: + return priv_dispatch_sendbpf(sctx, data, len); + case P_FREEIF: + return priv_dispatch_freeif(sctx, data, len); + default: + errno = EINVAL; + logerr(__func__); + return srv_send(sctx, NULL, cmd, -1, NULL, 0); + } +} + +struct srv_ctx * +priv_init(struct ctx *ctx) +{ + if (ctx->ctx_priv != NULL) + goto out; + ctx->ctx_priv = srv_init(ctx, "privileged helper", priv_dispatch); + + if (ctx->ctx_priv == NULL) + return NULL; + + if (ctx->ctx_options & DHCPSD_RUN) { + ctx->ctx_options |= DHCPSD_PRIV; +#ifdef HAVE_SETPROCTITLE + setproctitle("privileged helper"); +#endif + } + +out: + return ctx->ctx_priv; +} diff --git a/src/priv.h b/src/priv.h new file mode 100644 index 0000000..c2e8b15 --- /dev/null +++ b/src/priv.h @@ -0,0 +1,41 @@ +/* SPDX-License-Identifier: BSD-2-Clause */ +/* + * dhcpsd - privileged service helper + * Copyright (c) 2025 Roy Marples + * All rights reserved + + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#ifndef PRIV_H +#define PRIV_H + +struct ctx; +struct srv_ctx; +struct interface; +struct iovec; + +struct srv_ctx *priv_init(struct ctx *); +int priv_openbpf(struct interface *); +ssize_t priv_sendbpf(struct interface *, const struct iovec *, size_t); +int priv_freeif(struct interface *); +#endif diff --git a/src/route.c b/src/route.c new file mode 100644 index 0000000..822215a --- /dev/null +++ b/src/route.c @@ -0,0 +1,229 @@ +/* SPDX-License-Identifier: BSD-2-Clause */ +/* + * dhcpsd - route(4) support + * Copyright (c) 2025 Roy Marples + * All rights reserved + + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include "common.h" +#include "dhcpsd.h" +#include "eloop.h" +#include "if.h" +#include "logerr.h" +#include "queue.h" + +struct link_ctx { + struct ctx *link_ctx; + int link_fd; +}; + +struct rtm { + struct rt_msghdr hdr; + char buffer[sizeof(struct sockaddr_storage) * RTAX_MAX]; +}; + +static void +route_dispatch_ifinfo(struct link_ctx *lctx, struct rt_msghdr *rtm) +{ + struct ctx *ctx = lctx->link_ctx; + struct if_msghdr *ifi = (struct if_msghdr *)rtm; + struct interface *ifp; + + if (rtm->rtm_msglen < sizeof(*ifi)) + return; + + if (ifi->ifm_flags & IFF_UP) + return; + + /* Interface is down, remove it from consideration. */ + TAILQ_FOREACH(ifp, ctx->ctx_ifaces, if_next) { + if (ifp->if_index == ifi->ifm_index) + break; + } + if (ifp == NULL) + return; + + loginfox("%s: interface is down", ifp->if_name); + TAILQ_REMOVE(ctx->ctx_ifaces, ifp, if_next); + if_free(ifp); +} + +#ifdef RTM_IFANNOUNCE +static void +route_dispatch_ifannounce(struct link_ctx *lctx, const struct rt_msghdr *rtm) +{ + struct ctx *ctx = lctx->link_ctx; + const struct if_announcemsghdr *ifan = + (const struct if_announcemsghdr *)rtm; + struct interface *ifp; + + if (rtm->rtm_msglen < sizeof(*ifan)) + return; + + if (ifan->ifan_what != IFAN_DEPARTURE) + return; + + /* Interface has departed, remove it from consideration. */ + TAILQ_FOREACH(ifp, ctx->ctx_ifaces, if_next) { + if (ifp->if_index == ifan->ifan_index) + break; + } + if (ifp == NULL) + return; + + loginfox("%s: interface has departed", ifp->if_name); + TAILQ_REMOVE(ctx->ctx_ifaces, ifp, if_next); + if_free(ifp); +} +#endif + +static void +route_dispatch(void *arg, unsigned short e) +{ + struct link_ctx *lctx = arg; + struct rtm rtm; + ssize_t len; + +#ifdef NDEBUG + UNUSED(e); +#else + assert(e == ELE_READ); +#endif + + len = read(lctx->link_fd, &rtm, sizeof(rtm)); + if (len == -1) { + logerr("%s: read", __func__); + return; + } + + if ((size_t)len < offsetof(struct rt_msghdr, rtm_type) + + sizeof(rtm.hdr.rtm_type) || + len < rtm.hdr.rtm_msglen) { + logerrx("%s: truncated route message %zd %zu %d", __func__, len, + sizeof(rtm.hdr), rtm.hdr.rtm_msglen); + return; + } + + switch (rtm.hdr.rtm_type) { + case RTM_IFINFO: + route_dispatch_ifinfo(lctx, &rtm.hdr); + break; +#ifdef RTM_IFANNOUNCE + case RTM_IFANNOUNCE: + route_dispatch_ifannounce(lctx, &rtm.hdr); + break; +#endif + default: + /* Ignore other messages */ + break; + } +} + +int +link_open(struct ctx *ctx) +{ + struct link_ctx *lctx = malloc(sizeof(*lctx)); +#ifdef SO_RERROR + int n; +#endif +#if defined(RO_MSGFILTER) || defined(ROUTE_MSGFILTER) + unsigned char msgfilter[] = { + RTM_IFINFO, +#ifdef RTM_IFANNOUNCE + RTM_IFANNOUNCE, +#endif + }; +#ifdef ROUTE_MSGFILTER + unsigned int i, msgfilter_mask; +#endif +#endif + + if (lctx == NULL) + return -1; + + /* lctx will be freed at exit regardless if any error below + * so there is no need to free it on an error path. */ + lctx->link_ctx = ctx; + ctx->ctx_link = lctx; + + lctx->link_fd = xsocket(PF_ROUTE, SOCK_RAW | SOCK_CXNB, AF_UNSPEC); + if (lctx->link_fd == -1) + return -1; + +#ifdef SO_RERROR + /* Routing socket can overflow if the kernel sends too many messages. + * We need to reliably track state and if we can't we need to know. */ + n = 1; + if (setsockopt(lctx->link_fd, SOL_SOCKET, SO_RERROR, &n, sizeof(n)) == + -1) + logerr("%s: SO_RERROR", __func__); +#endif + +#if defined(RO_MSGFILTER) + if (setsockopt(lctx->link_fd, PF_ROUTE, RO_MSGFILTER, &msgfilter, + sizeof(msgfilter)) == -1) + logerr(__func__); +#elif defined(ROUTE_MSGFILTER) + /* Convert the array into a bitmask. */ + msgfilter_mask = 0; + for (i = 0; i < ARRAYCOUNT(msgfilter); i++) + msgfilter_mask |= ROUTE_FILTER(msgfilter[i]); + if (setsockopt(lctx->link_fd, PF_ROUTE, ROUTE_MSGFILTER, + &msgfilter_mask, sizeof(msgfilter_mask)) == -1) + logerr(__func__); +#else +#warning kernel does not support route message filtering +#endif + + if (eloop_event_add(ctx->ctx_eloop, lctx->link_fd, ELE_READ, + route_dispatch, lctx) == -1) { + logerr("%s: eloop_event_add", __func__); + return -1; + } + + return 0; +} + +void +link_free(struct ctx *ctx) +{ + struct link_ctx *lctx = ctx->ctx_link; + + if (lctx == NULL) + return; + if (lctx->link_fd != -1) + close(lctx->link_fd); + free(lctx); + ctx->ctx_link = NULL; +} diff --git a/src/seccomp.c b/src/seccomp.c index d65226a..bd8cceb 100644 --- a/src/seccomp.c +++ b/src/seccomp.c @@ -398,12 +398,15 @@ static struct sock_filter seccomp_filter[] = { SECCOMP_ALLOW(__NR_rt_sigtimedwait), #endif #ifdef VALGRIND +/* This is dangerous, and also pointless as in privsep + * we are no longer root and thus cannot unlink the valgrind + * pipes anyway. */ #ifdef __NR_unlink - /* This is dangerous, and also pointless as in privsep - * we are no longer root and thus cannot unlink the valgrind - * pipes anyway. */ SECCOMP_ALLOW(__NR_unlink), #endif +#ifdef __NR_unlinkat + SECCOMP_ALLOW(__NR_unlinkat), +#endif #endif /* hardened-malloc */ diff --git a/src/service.c b/src/service.c index 6d47145..4802c9f 100644 --- a/src/service.c +++ b/src/service.c @@ -54,6 +54,7 @@ struct srv_cmd { static ssize_t srv_recv(struct srv_ctx *sctx, unsigned short e) { + unsigned int options = sctx->srv_ctx->ctx_options; struct srv_result *sr = &sctx->srv_result; struct srv_cmd cmd; struct iovec iov[] = { @@ -67,7 +68,13 @@ srv_recv(struct srv_ctx *sctx, unsigned short e) if (e & ELE_HANGUP) { hangup: - eloop_exit(sctx->srv_ctx->ctx_eloop, EXIT_SUCCESS); + if (options & DHCPSD_MAIN) + logdebugx("%s(%d): fd %d hungup", __func__, getpid(), + sctx->srv_fd); + close(sctx->srv_fd); + sctx->srv_fd = -1; + eloop_exit(sctx->srv_ctx->ctx_eloop, + options & DHCPSD_EXITING ? EXIT_SUCCESS : EXIT_FAILURE); return -1; } if (e != ELE_READ) { @@ -132,8 +139,11 @@ srv_recvl(void *arg, unsigned short e) { struct srv_ctx *sctx = arg; - if (srv_recv(sctx, e) == -1 && !(e & ELE_HANGUP)) - eloop_exit(sctx->srv_ctx->ctx_eloop, EXIT_FAILURE); + if (srv_recv(sctx, e) == -1 && !(e & ELE_HANGUP)) { + unsigned int options = sctx->srv_ctx->ctx_options; + eloop_exit(sctx->srv_ctx->ctx_eloop, + options & DHCPSD_EXITING ? EXIT_SUCCESS : EXIT_FAILURE); + } } ssize_t @@ -204,8 +214,10 @@ srv_runv(struct srv_ctx *sctx, struct plugin *p, unsigned int cmd, } events = eloop_waitfd(sctx->srv_fd); - if (events == -1) + if (events == -1) { + logerr("%s: eloop_waitfd: %d", __func__, sctx->srv_fd); return -1; + } if (srv_recv(sctx, (unsigned short)events) == -1) return -1; @@ -277,7 +289,8 @@ srv_init(struct ctx *ctx, const char *name, default: sctx->srv_fd = fdset[0]; close(fdset[1]); - logdebugx("service: spawned %s on pid %ld", name, (long)pid); + logdebugx("service: spawned %s on pid %ld fd %d", name, + (long)pid, sctx->srv_fd); return sctx; } diff --git a/src/unpriv.c b/src/unpriv.c index b3e1d39..d970557 100644 --- a/src/unpriv.c +++ b/src/unpriv.c @@ -30,8 +30,26 @@ #include #include +#include + +#include "queue.h" + +#ifdef AF_LINK +#include +#include +#include +#include +#undef AF_PACKET /* Newer Illumos defines this */ +#endif +#ifdef AF_PACKET +#include +#endif + #include +#include #include +#include +#include #include #include #include @@ -39,12 +57,18 @@ #include "common.h" #include "dhcpsd.h" +#include "if.h" #include "logerr.h" #include "plugin.h" #include "service.h" #include "unpriv.h" -#define U_GETADDRINFO 1 +#define U_GETIFADDRS 1 +#define U_LEARNIF 2 +#define U_FREEIF 3 +#define U_GETADDRINFO 4 + +#define IFA_NADDRS 4 struct unpriv_addrinfo { int u_ai_flags; @@ -62,6 +86,147 @@ struct unpriv_getaddrinfo { struct unpriv_addrinfo u_gai_hints; }; +int +unpriv_getifaddrs(struct srv_ctx *ctx, struct ifaddrs **ifahead, + const unsigned int *match_if_index) +{ + struct ifaddrs *ifa; + char *bp, *sap; + socklen_t salen; + ssize_t result; + void *rdata, *buf; + size_t rdata_len, len; + int err; + + err = srv_run(ctx, NULL, U_GETIFADDRS, match_if_index, + match_if_index != NULL ? sizeof(*match_if_index) : 0, &result, + &rdata, &rdata_len); + if (err == -1 || result == -1) + return -1; + + /* Should be impossible - lo0 will always exist. */ + if (rdata_len == 0) { + *ifahead = NULL; + return 0; + } + + /* Take our own copy of the data. */ + buf = malloc(rdata_len); + if (buf == NULL) + return -1; + memcpy(buf, rdata, rdata_len); + len = rdata_len; + + bp = buf; + *ifahead = (struct ifaddrs *)(void *)bp; + for (ifa = *ifahead; ifa != NULL; ifa = ifa->ifa_next) { + if (len < ALIGN(sizeof(*ifa)) + ALIGN(IFNAMSIZ) + + ALIGN(sizeof(salen) * IFA_NADDRS)) + goto err; + bp += ALIGN(sizeof(*ifa)); + ifa->ifa_name = bp; + bp += ALIGN(IFNAMSIZ); + sap = bp; + bp += ALIGN(sizeof(salen) * IFA_NADDRS); + len -= ALIGN(sizeof(*ifa)) + ALIGN(IFNAMSIZ) + + ALIGN(sizeof(salen) * IFA_NADDRS); + +#define COPYOUTSA(addr) \ + do { \ + memcpy(&salen, sap, sizeof(salen)); \ + if (len < salen) \ + goto err; \ + if (salen != 0) { \ + (addr) = (struct sockaddr *)(void *)bp; \ + bp += ALIGN(salen); \ + len -= ALIGN(salen); \ + } \ + sap += sizeof(salen); \ + } while (0 /* CONSTCOND */) + + COPYOUTSA(ifa->ifa_addr); + COPYOUTSA(ifa->ifa_netmask); + COPYOUTSA(ifa->ifa_broadaddr); + + memcpy(&salen, sap, sizeof(salen)); + if (len < salen) + goto err; + if (salen != 0) { + ifa->ifa_data = bp; + bp += ALIGN(salen); + len -= ALIGN(salen); + } else + ifa->ifa_data = NULL; + + if (len != 0) + ifa->ifa_next = (struct ifaddrs *)(void *)bp; + else + ifa->ifa_next = NULL; + } + return 0; + +err: + free(buf); + *ifahead = NULL; + errno = EINVAL; + return -1; +} + +int +unpriv_learnif(struct interface *ifp) +{ + struct srv_ctx *sctx = ifp->if_ctx->ctx_unpriv; + ssize_t result; + void *rdata; + size_t rdata_len, len; + int err; + struct interface *ifp0; + + err = srv_run(sctx, NULL, U_LEARNIF, &ifp->if_index, + sizeof(ifp->if_index), &result, &rdata, &rdata_len); + if (err == -1 || result == -1) + return -1; + + if (rdata_len != sizeof(*ifp)) { + errno = EINVAL; + return -1; + } + + ifp0 = (struct interface *)rdata; + len = strlcpy(ifp->if_name, ifp0->if_name, sizeof((ifp->if_name))); + if (len >= sizeof(ifp->if_name)) { + errno = EINVAL; + return -1; + } + + ifp->if_hwtype = ifp0->if_hwtype; + ifp->if_hwlen = ifp0->if_hwlen; + + if (ifp0->if_hwlen > sizeof(ifp->if_hwaddr)) { + errno = EINVAL; + return -1; + } + memcpy(ifp->if_hwaddr, ifp0->if_hwaddr, ifp0->if_hwlen); + + ifp->if_mtu = ifp0->if_mtu; + + return 0; +} + +int +unpriv_freeif(struct interface *ifp) +{ + struct srv_ctx *sctx = ifp->if_ctx->ctx_unpriv; + ssize_t result; + int err; + + err = srv_run(sctx, NULL, U_FREEIF, &ifp->if_index, + sizeof(ifp->if_index), &result, NULL, 0); + if (err == -1) + return -1; + return (int)result; +} + int unpriv_getaddrinfo(struct srv_ctx *ctx, const char *hostname, const char *servname, struct addrinfo *hints, @@ -93,11 +258,14 @@ unpriv_getaddrinfo(struct srv_ctx *ctx, const char *hostname, u_gai.u_gai_hints.u_ai_protocol = hints->ai_protocol; } - err = srv_run(ctx, 0, U_GETADDRINFO, &u_gai, sizeof(u_gai), &result, + err = srv_run(ctx, NULL, U_GETADDRINFO, &u_gai, sizeof(u_gai), &result, &rdata, &rdata_len); if (err == -1) return -1; + if (result != 0) + return (int)result; + for (u_ai = rdata; rdata_len != 0; rdata_len -= sizeof(*u_ai), u_ai++) { if (rdata_len < sizeof(*u_ai)) { logerrx("%s: ai_addrinfo truncated", __func__); @@ -154,8 +322,254 @@ unpriv_getaddrinfo(struct srv_ctx *ctx, const char *hostname, } static ssize_t -unpriv_dispatch(struct srv_ctx *sctx, struct plugin *p, unsigned int cmd, - const void *data, size_t len) +unpriv_dispatch_getifaddrs(struct srv_ctx *sctx, const void *data, size_t len) +{ + struct ifaddrs *ifaddrs = NULL, *ifa; + ssize_t err = -1; + void *buf = NULL; + uint8_t *bp, *sap; + socklen_t salen; + bool match_index, matching = false; + unsigned int if_index; + + if (len == sizeof(if_index)) { + memcpy(&if_index, data, sizeof(if_index)); + match_index = true; + } else + match_index = false; + + len = 0; + if (getifaddrs(&ifaddrs) == -1) + goto err; + if (ifaddrs == NULL) { + err = 0; + goto err; + } + + /* Work out the buffer length required. + * Ensure everything is aligned correctly, which does + * create a larger buffer than what is needed to send, + * but makes creating the same structure in the client + * much easier. */ + for (ifa = ifaddrs; ifa != NULL; ifa = ifa->ifa_next) { + if (match_index) { + switch (if_link_match_index(ifa->ifa_addr, if_index)) { + case -1: + if (!matching) + continue; + break; + case 0: + matching = false; + continue; + case 1: + matching = true; + break; + } + } + len += ALIGN(sizeof(*ifa)); + len += ALIGN(IFNAMSIZ); + len += ALIGN(sizeof(salen) * IFA_NADDRS); + if (ifa->ifa_addr != NULL) + len += ALIGN(sa_len(ifa->ifa_addr)); + if (ifa->ifa_netmask != NULL) + len += ALIGN(sa_len(ifa->ifa_netmask)); + if (ifa->ifa_broadaddr != NULL) + len += ALIGN(sa_len(ifa->ifa_broadaddr)); +#ifdef BSD + /* + * On BSD we need to carry ifa_data so we can access + * if_data->ifi_link_state + */ + if (ifa->ifa_addr != NULL && + ifa->ifa_addr->sa_family == AF_LINK) + len += ALIGN(sizeof(struct if_data)); +#endif + } + + /* Use calloc to set everything to zero. + * This satisfies memory sanitizers because we don't write + * where we don't need to. */ + buf = calloc(1, len); + if (buf == NULL) { + freeifaddrs(ifaddrs); + return -1; + } + + bp = buf; + matching = false; + for (ifa = ifaddrs; ifa != NULL; ifa = ifa->ifa_next) { + if (match_index) { + switch (if_link_match_index(ifa->ifa_addr, if_index)) { + case -1: + if (!matching) + continue; + break; + case 0: + matching = false; + continue; + case 1: + matching = true; + break; + } + } + if (if_sockaddr_active(sctx->srv_ctx, ifa->ifa_name, + ifa->ifa_addr) != 1) + continue; + + memcpy(bp, ifa, sizeof(*ifa)); + bp += ALIGN(sizeof(*ifa)); + + strlcpy((char *)bp, ifa->ifa_name, IFNAMSIZ); + bp += ALIGN(IFNAMSIZ); + sap = bp; + bp += ALIGN(sizeof(salen) * IFA_NADDRS); + +#define COPYINSA(addr) \ + do { \ + if ((addr) != NULL) \ + salen = sa_len((addr)); \ + else \ + salen = 0; \ + if (salen != 0) { \ + memcpy(sap, &salen, sizeof(salen)); \ + memcpy(bp, (addr), salen); \ + bp += ALIGN(salen); \ + } \ + sap += sizeof(salen); \ + } while (0 /*CONSTCOND */) + + COPYINSA(ifa->ifa_addr); + COPYINSA(ifa->ifa_netmask); + COPYINSA(ifa->ifa_broadaddr); + +#ifdef BSD + if (ifa->ifa_addr != NULL && + ifa->ifa_addr->sa_family == AF_LINK) { + salen = (socklen_t)sizeof(struct if_data); + memcpy(bp, ifa->ifa_data, salen); + bp += ALIGN(salen); + } else +#endif + salen = 0; + memcpy(sap, &salen, sizeof(salen)); + } + len = (size_t)(bp - (uint8_t *)buf); + + err = 0; +err: + freeifaddrs(ifaddrs); + err = srv_send(sctx, NULL, U_GETIFADDRS, err, buf, len); + free(buf); + return err; +} + +static ssize_t +unpriv_dispatch_learnif(struct srv_ctx *sctx, const void *data, size_t len) +{ + struct ctx *ctx = sctx->srv_ctx; + struct interface *ifp = NULL; + unsigned int if_index; + ssize_t err = -1; + size_t nlen; + void *buf = NULL; + struct ifaddrs *ifaddrs = NULL, *ifa; + bool found_if_index = false; + + if (len != sizeof(if_index)) { + errno = EINVAL; + len = 0; + goto err; + } + memcpy(&if_index, data, len); + len = 0; + + TAILQ_FOREACH(ifp, ctx->ctx_ifaces, if_next) { + if (ifp->if_index == if_index) { + found_if_index = true; + break; + } + } + if (ifp == NULL) { + ifp = calloc(1, sizeof(*ifp)); + if (ifp == NULL) + goto err; + ifp->if_ctx = ctx; + ifp->if_index = if_index; + } + + if (getifaddrs(&ifaddrs) != 0) + goto err; + for (ifa = ifaddrs; ifa != NULL; ifa = ifa->ifa_next) { + if (sa_is_link(ifa->ifa_addr) == 1 && + if_link_match_index(ifa->ifa_addr, if_index) == 1 && + if_sockaddr_active(sctx->srv_ctx, ifa->ifa_name, + ifa->ifa_addr) == 1) + break; + } + + if (ifa == NULL) { + errno = ESRCH; + goto err; + } + nlen = strlcpy(ifp->if_name, ifa->ifa_name, sizeof(ifp->if_name)); + if (nlen >= sizeof(ifp->if_name)) { + errno = EINVAL; + goto err; + } + if_update(ifp, ifa->ifa_addr); + /* Every interface must have an MTU */ + if (if_update_mtu(ifp) == -1) + goto err; + + err = 0; + buf = ifp; + len = sizeof(*ifp); + if (!found_if_index) { + TAILQ_INSERT_TAIL(ctx->ctx_ifaces, ifp, if_next); + /* stop ifp being freed below */ + found_if_index = true; + } + +err: + freeifaddrs(ifaddrs); + if (!found_if_index) + if_free(ifp); + return srv_send(sctx, NULL, U_LEARNIF, err, buf, len); +} + +static ssize_t +unpriv_dispatch_freeif(struct srv_ctx *sctx, const void *data, size_t len) +{ + struct ctx *ctx = sctx->srv_ctx; + unsigned int if_index; + struct interface *ifp; + ssize_t err = -1; + + if (len != sizeof(if_index)) { + errno = EINVAL; + goto err; + } + memcpy(&if_index, data, sizeof(if_index)); + + TAILQ_FOREACH(ifp, ctx->ctx_ifaces, if_next) { + if (ifp->if_index == if_index) + break; + } + if (ifp == NULL) { + errno = ESRCH; + goto err; + } + + TAILQ_REMOVE(ctx->ctx_ifaces, ifp, if_next); + if_free(ifp); + err = 0; + +err: + return srv_send(sctx, NULL, U_FREEIF, err, NULL, 0); +} + +static ssize_t +unpriv_dispatch_getaddrinfo(struct srv_ctx *sctx, const void *data, size_t len) { const struct unpriv_getaddrinfo *u_gai = data; const char *hostname, *servname; @@ -166,15 +580,7 @@ unpriv_dispatch(struct srv_ctx *sctx, struct plugin *p, unsigned int cmd, size_t n; ssize_t res; - if (p != NULL) { - if (p->p_dispatch != NULL) - return p->p_dispatch(p, sctx, cmd, data, len); - errno = ENOSYS; - logerr(__func__); - goto err; - } - - if (cmd != U_GETADDRINFO || len != sizeof(*u_gai)) { + if (len != sizeof(*u_gai)) { errno = EINVAL; logerr(__func__); goto err; @@ -234,7 +640,35 @@ unpriv_dispatch(struct srv_ctx *sctx, struct plugin *p, unsigned int cmd, err: freeaddrinfo(ai_result); free(reply); - return srv_send(sctx, NULL, cmd, err, NULL, 0); + return srv_send(sctx, NULL, U_GETADDRINFO, err, NULL, 0); +} + +static ssize_t +unpriv_dispatch(struct srv_ctx *sctx, struct plugin *p, unsigned int cmd, + const void *data, size_t len) +{ + if (p != NULL) { + if (p->p_dispatch != NULL) + return p->p_dispatch(p, sctx, cmd, data, len); + errno = ENOSYS; + logerr(__func__); + return srv_send(sctx, NULL, cmd, -1, NULL, 0); + } + + switch (cmd) { + case U_GETIFADDRS: + return unpriv_dispatch_getifaddrs(sctx, data, len); + case U_LEARNIF: + return unpriv_dispatch_learnif(sctx, data, len); + case U_FREEIF: + return unpriv_dispatch_freeif(sctx, data, len); + case U_GETADDRINFO: + return unpriv_dispatch_getaddrinfo(sctx, data, len); + default: + errno = EINVAL; + logerr(__func__); + return srv_send(sctx, NULL, cmd, -1, NULL, 0); + } } struct srv_ctx * diff --git a/src/unpriv.h b/src/unpriv.h index 121116d..3e2d7fd 100644 --- a/src/unpriv.h +++ b/src/unpriv.h @@ -29,10 +29,17 @@ #ifndef UNPRIV_H #define UNPRIV_H +struct ctx; struct srv_ctx; struct addrinfo; +struct ifaddrs; +struct interface; struct srv_ctx *unpriv_init(struct ctx *); +int unpriv_getifaddrs(struct srv_ctx *ctx, struct ifaddrs **ifahead, + const unsigned int *match_if_index); +int unpriv_learnif(struct interface *ifp); +int unpriv_freeif(struct interface *ifp); int unpriv_getaddrinfo(struct srv_ctx *, const char *, const char *, struct addrinfo *, struct addrinfo **restrict); void unpriv_freeaddrinfo(struct addrinfo *);