diff --git a/BUILDING.md b/BUILDING.md index 58ad8e8c..1b3fc700 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -139,6 +139,11 @@ are disabled by default. For Valgrind, it needs to unlink the pipe files which it can't do anyway as it's dropped permissions. Otherwise it works fine. +Enable libpcap support with --with-libpcap. +This should only be done on systems that lack the needed kernel hooks +as libpcap does not support a write filter and is vulnerable +if the application is exploited. + ## Init systems We try and detect how dhcpcd should interact with system services at runtime. If we cannot auto-detect how do to this, or it is wrong then diff --git a/configure b/configure index 0cb6f288..a1776e72 100755 --- a/configure +++ b/configure @@ -42,6 +42,7 @@ SMALL= SANITIZE=no STATUSARG= OPENSSL= +LIBPCAP= DHCPCD_DEFS=dhcpcd-definitions.conf @@ -119,6 +120,8 @@ for x do --with-poll) POLL="$var";; --with-openssl) OPENSSL=yes;; --without-openssl) OPENSSL=no;; + --with-libpcap) LIBPCAP=yes;; + --without-libpcap) LIBPCAP=no;; --sanitise|--sanitize) SANITIZEADDRESS="yes";; --serviceexists) SERVICEEXISTS=$var;; --servicecmd) SERVICECMD=$var;; @@ -465,6 +468,7 @@ if [ "$SMALL" = yes ]; then echo "DHCPCD_DEFS= $DHCPCD_DEFS" >>$CONFIG_MK fi +BPF_OS= case "$OS" in freebsd*|kfreebsd*) # FreeBSD hide some newer POSIX APIs behind _GNU_SOURCE ... @@ -497,6 +501,7 @@ linux*) echo "#include /* fix broken headers */" >>$CONFIG_H echo "#include /* fix broken headers */" >>$CONFIG_H echo "#include " >>$CONFIG_H + BPF_OS="bpf-linux.c" # cksum does't support -a and netpgp is rare echo "CKSUM= sha256sum --tag" >>$CONFIG_MK echo "PGP= gpg2" >>$CONFIG_MK @@ -516,6 +521,13 @@ solaris*|sunos*) ;; esac +if [ -z "$BPF_OS" ]; then + BPF_OS="bpf-bsd.c" +fi +if [ "$LIBPCAP" = yes ]; then + BPF_OS="bpf-pcap.c" +fi + if [ -n "${_DEFAULT_HOSTNAME+x}" ]; then DEFAULT_HOSTNAME="${_DEFAULT_HOSTNAME}" else @@ -529,7 +541,7 @@ echo "DEFAULT_HOSTNAME= $DEFAULT_HOSTNAME" >>$CONFIG_MK if [ -z "$INET" ] || [ "$INET" = yes ]; then echo "Enabling INET support" echo "CPPFLAGS+= -DINET" >>$CONFIG_MK - echo "DHCPCD_SRCS+= dhcp.c ipv4.c bpf.c" >>$CONFIG_MK + echo "DHCPCD_SRCS+= dhcp.c ipv4.c bpf.c $BPF_OS" >>$CONFIG_MK if [ -z "$ARP" ] || [ "$ARP" = yes ]; then echo "Enabling ARP support" echo "CPPFLAGS+= -DARP" >>$CONFIG_MK @@ -1506,6 +1518,36 @@ EOF rm -f _openssl_sha.c _openssl_sha fi +if [ "$LIBPCAP" = yes ]; then + printf "Testing for libpcap ... " + if $PKG_CONFIG --exists libpcap 2>/dev/null; then + LIBPCAP_CFLAGS=$($PKG_CONFIG --cflags libpcap 2>/dev/null) + LIBPCAP_LIBS=$($PKG_CONFIG --libs libpcap 2>/dev/null) + echo "yes" + echo "CFLAGS+= $LIBPCAP_CFLAGS" >>$CONFIG_MK + echo "LDADD+= $LIBPCAP_LIBS" >>$CONFIG_MK + else + cat <_libpcap.c +#include + +int main(void) { + return pcap_activate(NULL); +} +EOF + if $XCC _libpcap.c -o libpcap -lpcap 2>&3; then + echo "yes" + echo "LDADD+= -lpcap" >>$CONFIG_MK + abort=false + else + echo "no" + abort=true + fi + rm -f _libpcap.c libpcap + $abort && exit 1 + fi + echo "#define USE_LIBPCAP" >>$CONFIG_H +fi + # Workaround for DragonFlyBSD import if [ "$OS" = dragonfly ]; then echo "#ifdef USE_PRIVATECRYPTO" >>$CONFIG_H diff --git a/src/arp.c b/src/arp.c index 9c9a6d3a..7d4f1e78 100644 --- a/src/arp.c +++ b/src/arp.c @@ -562,7 +562,7 @@ arp_new(struct interface *ifp, const struct in_addr *addr) } else #endif { - astate->bpf = bpf_open(ifp, bpf_arp, addr); + astate->bpf = bpf_open(ifp, bpf_filter_arp, addr); if (astate->bpf == NULL) { logerr(__func__); free(astate); diff --git a/src/bpf-bsd.c b/src/bpf-bsd.c new file mode 100644 index 00000000..c449e705 --- /dev/null +++ b/src/bpf-bsd.c @@ -0,0 +1,261 @@ +/* + * BPF BSD interface + * SPDX-License-Identifier: BSD-2-Clause + * Copyright (c) 2006-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 "bpf.h" +#include "logerr.h" + +const char *bpf_name = "Berkeley Packet Filter"; + +struct bpf * +bpf_open(const struct interface *ifp, + int (*filter)(const struct bpf *, const struct in_addr *), + const struct in_addr *ia) +{ + struct bpf *bpf; + struct bpf_version pv = { .bv_major = 0, .bv_minor = 0 }; + struct ifreq ifr = { .ifr_flags = 0 }; + int ibuf_len = 0; +#ifdef O_CLOEXEC +#define BPF_OPEN_FLAGS O_RDWR | O_NONBLOCK | O_CLOEXEC +#else +#define BPF_OPEN_FLAGS O_RDWR | O_NONBLOCK +#endif +#ifdef BIOCIMMEDIATE + unsigned int flags; +#endif +#ifndef O_CLOEXEC + int fd_opts; +#endif + + bpf = calloc(1, sizeof(*bpf)); + if (bpf == NULL) + return NULL; + bpf->bpf_ifp = ifp; + bpf->bpf_flags = BPF_EOF; + + /* /dev/bpf is a cloner on modern kernels */ + bpf->bpf_fd = open("/dev/bpf", BPF_OPEN_FLAGS); + + /* Support older kernels where /dev/bpf is not a cloner */ + if (bpf->bpf_fd == -1) { + char device[32]; + int n = 0; + + do { + snprintf(device, sizeof(device), "/dev/bpf%d", n++); + bpf->bpf_fd = open(device, BPF_OPEN_FLAGS); + } while (bpf->bpf_fd == -1 && errno == EBUSY); + } + + if (bpf->bpf_fd == -1) + goto eexit; + +#ifndef O_CLOEXEC + if ((fd_opts = fcntl(bpf->bpf_fd, F_GETFD)) == -1 || + fcntl(bpf->bpf_fd, F_SETFD, fd_opts | FD_CLOEXEC) == -1) + goto eexit; +#endif + + if (ioctl(bpf->bpf_fd, BIOCVERSION, &pv) == -1) + goto eexit; + if (pv.bv_major != BPF_MAJOR_VERSION || + pv.bv_minor < BPF_MINOR_VERSION) { + logerrx("BPF version mismatch - recompile"); + goto eexit; + } + + strlcpy(ifr.ifr_name, ifp->name, sizeof(ifr.ifr_name)); + if (ioctl(bpf->bpf_fd, BIOCSETIF, &ifr) == -1) + goto eexit; + +#ifdef BIOCIMMEDIATE + flags = 1; + if (ioctl(bpf->bpf_fd, BIOCIMMEDIATE, &flags) == -1) + goto eexit; +#endif + + if (filter(bpf, ia) != 0) + goto eexit; + + /* Get the required BPF buffer length from the kernel. */ + if (ioctl(bpf->bpf_fd, BIOCGBLEN, &ibuf_len) == -1) + goto eexit; + + bpf->bpf_size = (size_t)ibuf_len; + bpf->bpf_buffer = malloc(bpf->bpf_size); + if (bpf->bpf_buffer == NULL) + goto eexit; + + return bpf; + +eexit: + if (bpf->bpf_fd != -1) + close(bpf->bpf_fd); + free(bpf); + return NULL; +} + +/* BPF requires that we read the entire buffer. + * So we pass the buffer in the API so we can loop on >1 packet. */ +ssize_t +bpf_read(struct bpf *bpf, void *data, size_t len) +{ + ssize_t bytes; + struct bpf_hdr packet; + size_t hdr_max; + const uint8_t *payload; + + bpf->bpf_flags &= ~BPF_EOF; + for (;;) { + if (bpf->bpf_len == 0) { + bytes = read(bpf->bpf_fd, bpf->bpf_buffer, + bpf->bpf_size); +#if defined(__sun) + /* After 2^31 bytes, the kernel offset overflows. + * To work around this bug, lseek 0. */ + if (bytes == -1 && errno == EINVAL) { + lseek(bpf->bpf_fd, 0, SEEK_SET); + continue; + } +#endif + if (bytes == -1 || bytes == 0) + return bytes; + bpf->bpf_len = (size_t)bytes; + bpf->bpf_pos = 0; + } + bytes = -1; + if (bpf->bpf_pos + sizeof(packet) > bpf->bpf_len) { + errno = EINVAL; + goto err; + } + payload = (const uint8_t *)bpf->bpf_buffer + bpf->bpf_pos; + memcpy(&packet, payload, sizeof(packet)); + hdr_max = SIZE_MAX - packet.bh_caplen; + if (packet.bh_hdrlen > hdr_max) { + errno = EOVERFLOW; + goto err; + } + if (packet.bh_hdrlen + packet.bh_caplen > + bpf->bpf_len - bpf->bpf_pos) { + errno = EBADMSG; + goto err; + } + payload += packet.bh_hdrlen; + if (packet.bh_caplen > len) + bytes = (ssize_t)len; + else + bytes = (ssize_t)packet.bh_caplen; + if (bpf_frame_bcast(bpf->bpf_ifp, payload) == 0) + bpf->bpf_flags |= BPF_BCAST; + else + bpf->bpf_flags &= ~BPF_BCAST; + memcpy(data, payload, (size_t)bytes); + bpf->bpf_pos += BPF_WORDALIGN( + packet.bh_hdrlen + packet.bh_caplen); + if (bpf->bpf_pos >= bpf->bpf_len) { + bpf->bpf_len = bpf->bpf_pos = 0; + bpf->bpf_flags |= BPF_EOF; + } + if (bytes != -1) + return bytes; + } + + /* NOTREACHED */ + +err: + bpf->bpf_len = bpf->bpf_pos = 0; + bpf->bpf_flags |= BPF_EOF; + return -1; +} + +int +bpf_setfilter(const struct bpf *bpf, void *filter, unsigned int filter_len) +{ + struct bpf_program pf = { .bf_insns = filter, .bf_len = filter_len }; + + /* Install the filter. */ + return ioctl(bpf->bpf_fd, BIOCSETF, &pf); +} + +int +bpf_setwfilter(const struct bpf *bpf, void *filter, unsigned int filter_len) +{ +#ifdef BIOCSETWF + struct bpf_program pf = { .bf_insns = filter, .bf_len = filter_len }; + + /* Install the filter. */ + return ioctl(bpf->bpf_fd, BIOCSETWF, &pf); +#else +#warning No BIOCSETWF support - a compromised BPF can be used as a raw socket + UNUSED(bpf); + UNUSED(filter); + UNUSED(filter_len); + errno = ENOSYS; + return -1; +#endif +} + +int +bpf_lock(const struct bpf *bpf) +{ +#ifdef BIOCLOCK + return ioctl(bpf->bpf_fd, BIOCLOCK); +#else + UNUSED(bpf); + errno = ENOSYS; + return -1; +#endif +} + +#if !defined(__sun) +/* SunOS is special too - sending via BPF goes nowhere. */ +ssize_t +bpf_writev(const struct bpf *bpf, struct iovec *iov, int iovcnt) +{ + return writev(bpf->bpf_fd, iov, iovcnt); +} +#endif + +void +bpf_close(struct bpf *bpf) +{ + close(bpf->bpf_fd); + free(bpf->bpf_buffer); + free(bpf); +} diff --git a/src/bpf-linux.c b/src/bpf-linux.c new file mode 100644 index 00000000..5d7968eb --- /dev/null +++ b/src/bpf-linux.c @@ -0,0 +1,219 @@ +/* + * BPF Linux interface + * SPDX-License-Identifier: BSD-2-Clause + * Copyright (c) 2006-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 "bpf.h" + +const char *bpf_name = "Packet Socket"; + +struct bpf * +bpf_open(const struct interface *ifp, + int (*filter)(const struct bpf *, const struct in_addr *), + const struct in_addr *ia) +{ + struct bpf *bpf; + union sockunion { + struct sockaddr sa; + struct sockaddr_ll sll; + struct sockaddr_storage ss; + } su = { .sll = { + .sll_family = PF_PACKET, + .sll_protocol = htons(ETH_P_ALL), + .sll_ifindex = (int)ifp->index, + } }; +#ifdef PACKET_AUXDATA + int n; +#endif + + bpf = calloc(1, sizeof(*bpf)); + if (bpf == NULL) + return NULL; + bpf->bpf_ifp = ifp; + bpf->bpf_flags = BPF_EOF; + bpf->bpf_fd = -1; + + /* Allocate a suitably large buffer for a single packet. */ + bpf->bpf_size = ETH_FRAME_LEN; + bpf->bpf_buffer = malloc(bpf->bpf_size); + if (bpf->bpf_buffer == NULL) + goto eexit; + + bpf->bpf_fd = xsocket(PF_PACKET, SOCK_RAW | SOCK_CXNB, + htons(ETH_P_ALL)); + if (bpf->bpf_fd == -1) + goto eexit; + + /* We cannot validate the correct interface, + * so we MUST set this first. */ + if (bind(bpf->bpf_fd, &su.sa, sizeof(su.sll)) == -1) + goto eexit; + + if (filter(bpf, ia) != 0) + goto eexit; + + /* In the ideal world, this would be set before the bind and filter. */ +#ifdef PACKET_AUXDATA + n = 1; + if (setsockopt(bpf->bpf_fd, SOL_PACKET, PACKET_AUXDATA, &n, + sizeof(n)) != 0) { + if (errno != ENOPROTOOPT) + goto eexit; + } +#endif + + /* + * At this point we could have received packets for the wrong + * interface or which don't pass the filter. + * Linux should flush upon setting the filter like every other OS. + * There is no way of flushing them from userland. + * As such, consumers need to inspect each packet to ensure it's valid. + * Or to put it another way, don't trust the Linux BPF filter. + */ + + return bpf; + +eexit: + if (bpf->bpf_fd != -1) + close(bpf->bpf_fd); + free(bpf->bpf_buffer); + free(bpf); + return NULL; +} + +/* BPF requires that we read the entire buffer. + * So we pass the buffer in the API so we can loop on >1 packet. */ +ssize_t +bpf_read(struct bpf *bpf, void *data, size_t len) +{ + ssize_t bytes; + struct iovec iov = { + .iov_base = bpf->bpf_buffer, + .iov_len = bpf->bpf_size, + }; + struct msghdr msg = { .msg_iov = &iov, .msg_iovlen = 1 }; +#ifdef PACKET_AUXDATA + union { + struct cmsghdr hdr; + uint8_t buf[CMSG_SPACE(sizeof(struct tpacket_auxdata))]; + } cmsgbuf = { .buf = { 0 } }; + struct cmsghdr *cmsg; + struct tpacket_auxdata *aux; +#endif + +#ifdef PACKET_AUXDATA + msg.msg_control = cmsgbuf.buf; + msg.msg_controllen = sizeof(cmsgbuf.buf); +#endif + + bytes = recvmsg(bpf->bpf_fd, &msg, 0); + if (bytes == -1) + return -1; + bpf->bpf_flags |= BPF_EOF; /* We only ever read one packet. */ + bpf->bpf_flags &= ~BPF_PARTIALCSUM; + if (bytes) { + if (bpf_frame_bcast(bpf->bpf_ifp, bpf->bpf_buffer) == 0) + bpf->bpf_flags |= BPF_BCAST; + else + bpf->bpf_flags &= ~BPF_BCAST; + if ((size_t)bytes > len) + bytes = (ssize_t)len; + memcpy(data, bpf->bpf_buffer, (size_t)bytes); +#ifdef PACKET_AUXDATA + for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; + cmsg = CMSG_NXTHDR(&msg, cmsg)) { + if (cmsg->cmsg_level == SOL_PACKET && + cmsg->cmsg_type == PACKET_AUXDATA) { + aux = (void *)CMSG_DATA(cmsg); + if (aux->tp_status & TP_STATUS_CSUMNOTREADY) + bpf->bpf_flags |= BPF_PARTIALCSUM; + } + } +#endif + } + return bytes; +} + +int +bpf_setfilter(const struct bpf *bpf, void *filter, unsigned int filter_len) +{ + struct sock_fprog pf = { + .filter = filter, + .len = (unsigned short)filter_len, + }; + int s = bpf->bpf_fd; + + /* Install the filter. */ + return setsockopt(s, SOL_SOCKET, SO_ATTACH_FILTER, &pf, sizeof(pf)); +} + +int +bpf_setwfilter(__unused const struct bpf *bpf, __unused void *filter, __unused unsigned int filter_len) +{ +#warning A compromised PF_PACKET socket can be used as a raw socket + + errno = ENOSYS; + return -1; +} + +int +bpf_lock(const struct bpf *bpf) +{ +#ifdef SO_LOCK_FILTER + int fd = bpf->bpf_fd, on = 1; + + return setsockopt(fd, SOL_SOCKET, SO_LOCK_FILTER, &on, sizeof(on)); +#else + UNUSED(bpf); + errno = ENOSYS; + return -1; +#endif +} + +ssize_t +bpf_writev(const struct bpf *bpf, struct iovec *iov, int iovcnt) +{ + return writev(bpf->bpf_fd, iov, iovcnt); +} + +void +bpf_close(struct bpf *bpf) +{ + close(bpf->bpf_fd); + free(bpf->bpf_buffer); + free(bpf); +} + diff --git a/src/bpf-pcap.c b/src/bpf-pcap.c new file mode 100644 index 00000000..2c77d300 --- /dev/null +++ b/src/bpf-pcap.c @@ -0,0 +1,200 @@ +/* + * BPF libpcap interface + * SPDX-License-Identifier: BSD-2-Clause + * Copyright (c) 2025 Joan Lledó + * 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 "bpf.h" +#include "logerr.h" + +#define PCAP_CHECK(call, name) \ + do { \ + int status = (call); \ + if (status < 0) \ + logerrx("%s: %s failed: %s", __func__, name, \ + pcap_statustostr(status)); \ + else if (status > 0) \ + logwarnx("%s: %s warning: %s", __func__, name, \ + pcap_statustostr(status)); \ + } while (0) + +#define ETH_MTU 1500 + +const char *bpf_name = "Berkeley Packet Filter (libpcap)"; + +struct bpf * +bpf_open(const struct interface *ifp, + int (*filter)(const struct bpf *, const struct in_addr *), + __unused const struct in_addr *ia) +{ + int err; + struct bpf *bpf; + pcap_t *handle; + char errbuf[PCAP_ERRBUF_SIZE]; + int snaplen; + + bpf = calloc(1, sizeof(*bpf)); + if (bpf == NULL) + return NULL; + + snaplen = ifp->mtu ? ifp->mtu : ETH_MTU; + bpf->bpf_ifp = ifp; + bpf->bpf_size = bpf_frame_header_len(ifp) + (size_t)snaplen; + bpf->bpf_buffer = malloc(bpf->bpf_size); + if (bpf->bpf_buffer == NULL) + goto eexit; + bpf->bpf_len = 0; + bpf->bpf_pos = 0; + bpf->bpf_flags = BPF_EOF; + + bpf->bpf_handle = handle = pcap_create(ifp->name, errbuf); + if (handle == NULL) { + logerrx("%s: pcap_create: %s", __func__, errbuf); + goto eexit; + } + + PCAP_CHECK(pcap_set_snaplen(handle, snaplen), "pcap_set_snaplen"); + PCAP_CHECK(pcap_set_promisc(handle, 0), "pcap_set_promisc"); + PCAP_CHECK(pcap_set_immediate_mode(handle, 1), + "pcap_set_immediate_mode"); + + err = pcap_activate(handle); + if (err != 0) { + if (err < 0) { + logerrx("%s: pcap_activate failed: %s", __func__, + pcap_statustostr(err)); + goto eexit; + } + logwarnx("%s: pcap_activate warning: %s", __func__, + pcap_statustostr(err)); + } + + bpf->bpf_fd = pcap_get_selectable_fd(handle); + if (bpf->bpf_fd < 0) { + logerrx("%s: pcap_get_selectable_fd failed", __func__); + goto eexit; + } + + if (filter(bpf, ia) != 0) + goto eexit; + + return bpf; + +eexit: + bpf_close(bpf); + return NULL; +} + +ssize_t +bpf_read(struct bpf *bpf, void *data, size_t len) +{ + struct pcap_pkthdr *pkt_header; + const u_char *pkt_data; + size_t cap_len; + int err; + + bpf->bpf_flags |= BPF_EOF; /* We only read one packet per call */ + + err = pcap_next_ex(bpf->bpf_handle, &pkt_header, &pkt_data); + + if (err == 0) + return 0; + if (err < 0) + return -1; + + /* Packet read successfully */ + cap_len = pkt_header->caplen; + if (cap_len > len) + cap_len = len; + memcpy(data, pkt_data, cap_len); + + if (bpf_frame_bcast(bpf->bpf_ifp, pkt_data) == 0) + bpf->bpf_flags |= BPF_BCAST; + else + bpf->bpf_flags &= ~BPF_BCAST; + + return (ssize_t)cap_len; +} + +ssize_t +bpf_writev(const struct bpf *bpf, struct iovec *iov, int iovcnt) +{ + int i; + size_t len = 0; + uint8_t *bp = bpf->bpf_buffer; + + for (i = 0; i < iovcnt; i++) { + len += iov[i].iov_len; + /* This should be impossible. */ + if (bpf->bpf_size < len) { + errno = ENOBUFS; + return -1; + } + memcpy(bp, iov[i].iov_base, iov[i].iov_len); + bp += iov[i].iov_len; + } + + return pcap_inject(bpf->bpf_handle, bpf->bpf_buffer, len); +} + +int +bpf_setfilter(const struct bpf *bpf, void *filter, unsigned int filter_len) +{ + struct bpf_program pf = { .bf_insns = filter, .bf_len = filter_len }; + + /* Install the filter. */ + return pcap_setfilter(bpf->bpf_handle, &pf); +} + +int +bpf_setwfilter(__unused const struct bpf *bpf, __unused void *filter, + __unused unsigned int filter_len) +{ +#warning A compromised libpcap socket can be used as a raw socket + + errno = ENOSYS; + return -1; +} + +int +bpf_lock(__unused const struct bpf *bpf) +{ + errno = ENOSYS; + return -1; +} + +void +bpf_close(struct bpf *bpf) +{ + if (bpf->bpf_handle != NULL) + pcap_close(bpf->bpf_handle); + free(bpf->bpf_buffer); + free(bpf); +} diff --git a/src/bpf.c b/src/bpf.c index a644a42c..20032c9d 100644 --- a/src/bpf.c +++ b/src/bpf.c @@ -138,181 +138,7 @@ bpf_frame_bcast(const struct interface *ifp, const void *frame) } } -#ifndef __linux__ -/* Linux is a special snowflake for opening, attaching and reading BPF. - * See if-linux.c for the Linux specific BPF functions. */ - -const char *bpf_name = "Berkley Packet Filter"; - -struct bpf * -bpf_open(const struct interface *ifp, - int (*filter)(const struct bpf *, const struct in_addr *), - const struct in_addr *ia) -{ - struct bpf *bpf; - struct bpf_version pv = { .bv_major = 0, .bv_minor = 0 }; - struct ifreq ifr = { .ifr_flags = 0 }; - int ibuf_len = 0; -#ifdef O_CLOEXEC -#define BPF_OPEN_FLAGS O_RDWR | O_NONBLOCK | O_CLOEXEC -#else -#define BPF_OPEN_FLAGS O_RDWR | O_NONBLOCK -#endif -#ifdef BIOCIMMEDIATE - unsigned int flags; -#endif -#ifndef O_CLOEXEC - int fd_opts; -#endif - - bpf = calloc(1, sizeof(*bpf)); - if (bpf == NULL) - return NULL; - bpf->bpf_ifp = ifp; - bpf->bpf_flags = BPF_EOF; - - /* /dev/bpf is a cloner on modern kernels */ - bpf->bpf_fd = open("/dev/bpf", BPF_OPEN_FLAGS); - - /* Support older kernels where /dev/bpf is not a cloner */ - if (bpf->bpf_fd == -1) { - char device[32]; - int n = 0; - - do { - snprintf(device, sizeof(device), "/dev/bpf%d", n++); - bpf->bpf_fd = open(device, BPF_OPEN_FLAGS); - } while (bpf->bpf_fd == -1 && errno == EBUSY); - } - - if (bpf->bpf_fd == -1) - goto eexit; - -#ifndef O_CLOEXEC - if ((fd_opts = fcntl(bpf->bpf_fd, F_GETFD)) == -1 || - fcntl(bpf->bpf_fd, F_SETFD, fd_opts | FD_CLOEXEC) == -1) - goto eexit; -#endif - - if (ioctl(bpf->bpf_fd, BIOCVERSION, &pv) == -1) - goto eexit; - if (pv.bv_major != BPF_MAJOR_VERSION || - pv.bv_minor < BPF_MINOR_VERSION) { - logerrx("BPF version mismatch - recompile"); - goto eexit; - } - - strlcpy(ifr.ifr_name, ifp->name, sizeof(ifr.ifr_name)); - if (ioctl(bpf->bpf_fd, BIOCSETIF, &ifr) == -1) - goto eexit; - -#ifdef BIOCIMMEDIATE - flags = 1; - if (ioctl(bpf->bpf_fd, BIOCIMMEDIATE, &flags) == -1) - goto eexit; -#endif - - if (filter(bpf, ia) != 0) - goto eexit; - - /* Get the required BPF buffer length from the kernel. */ - if (ioctl(bpf->bpf_fd, BIOCGBLEN, &ibuf_len) == -1) - goto eexit; - - bpf->bpf_size = (size_t)ibuf_len; - bpf->bpf_buffer = malloc(bpf->bpf_size); - if (bpf->bpf_buffer == NULL) - goto eexit; - - return bpf; - -eexit: - if (bpf->bpf_fd != -1) - close(bpf->bpf_fd); - free(bpf); - return NULL; -} - -/* BPF requires that we read the entire buffer. - * So we pass the buffer in the API so we can loop on >1 packet. */ -ssize_t -bpf_read(struct bpf *bpf, void *data, size_t len) -{ - ssize_t bytes; - struct bpf_hdr packet; - const char *payload; - - bpf->bpf_flags &= ~BPF_EOF; - for (;;) { - if (bpf->bpf_len == 0) { - bytes = read(bpf->bpf_fd, bpf->bpf_buffer, - bpf->bpf_size); -#if defined(__sun) - /* After 2^31 bytes, the kernel offset overflows. - * To work around this bug, lseek 0. */ - if (bytes == -1 && errno == EINVAL) { - lseek(bpf->bpf_fd, 0, SEEK_SET); - continue; - } -#endif - if (bytes == -1 || bytes == 0) - return bytes; - bpf->bpf_len = (size_t)bytes; - bpf->bpf_pos = 0; - } - bytes = -1; - payload = (const char *)bpf->bpf_buffer + bpf->bpf_pos; - memcpy(&packet, payload, sizeof(packet)); - if (bpf->bpf_pos + packet.bh_caplen + packet.bh_hdrlen > - bpf->bpf_len) - goto next; /* Packet beyond buffer, drop. */ - payload += packet.bh_hdrlen; - if (packet.bh_caplen > len) - bytes = (ssize_t)len; - else - bytes = (ssize_t)packet.bh_caplen; - if (bpf_frame_bcast(bpf->bpf_ifp, payload) == 0) - bpf->bpf_flags |= BPF_BCAST; - else - bpf->bpf_flags &= ~BPF_BCAST; - memcpy(data, payload, (size_t)bytes); - next: - bpf->bpf_pos += BPF_WORDALIGN( - packet.bh_hdrlen + packet.bh_caplen); - if (bpf->bpf_pos >= bpf->bpf_len) { - bpf->bpf_len = bpf->bpf_pos = 0; - bpf->bpf_flags |= BPF_EOF; - } - if (bytes != -1) - return bytes; - } - - /* NOTREACHED */ -} - -int -bpf_attach(int fd, void *filter, unsigned int filter_len) -{ - struct bpf_program pf = { .bf_insns = filter, .bf_len = filter_len }; - - /* Install the filter. */ - return ioctl(fd, BIOCSETF, &pf); -} - -#ifdef BIOCSETWF -static int -bpf_wattach(int fd, void *filter, unsigned int filter_len) -{ - struct bpf_program pf = { .bf_insns = filter, .bf_len = filter_len }; - - /* Install the filter. */ - return ioctl(fd, BIOCSETWF, &pf); -} -#endif -#endif - #ifndef __sun -/* SunOS is special too - sending via BPF goes nowhere. */ ssize_t bpf_send(const struct bpf *bpf, uint16_t protocol, const void *data, size_t len) { @@ -335,17 +161,10 @@ bpf_send(const struct bpf *bpf, uint16_t protocol, const void *data, size_t len) } iov[1].iov_base = UNCONST(data); iov[1].iov_len = len; - return writev(bpf->bpf_fd, iov, 2); -} -#endif -void -bpf_close(struct bpf *bpf) -{ - close(bpf->bpf_fd); - free(bpf->bpf_buffer); - free(bpf); + return bpf_writev(bpf, iov, __arraycount(iov)); } +#endif #ifdef ARP #define BPF_CMP_HWADDR_LEN ((((HWADDR_LEN / 4) + 2) * 2) + 1) @@ -489,6 +308,7 @@ bpf_arp_rw(const struct bpf *bpf, const struct in_addr *ia, bool recv) struct bpf_insn buf[BPF_ARP_LEN + 1]; struct bpf_insn *bp; uint16_t arp_len; + unsigned int len; bp = buf; /* Check frame header. */ @@ -542,26 +362,22 @@ bpf_arp_rw(const struct bpf *bpf, const struct in_addr *ia, bool recv) BPF_SET_STMT(bp, BPF_RET + BPF_K, 0); bp++; -#ifdef BIOCSETWF - if (!recv) - return bpf_wattach(bpf->bpf_fd, buf, (unsigned int)(bp - buf)); -#endif - - return bpf_attach(bpf->bpf_fd, buf, (unsigned int)(bp - buf)); + len = (unsigned int)(bp - buf); + if (recv) + return bpf_setfilter(bpf, buf, len); + return bpf_setwfilter(bpf, buf, len); } int -bpf_arp(const struct bpf *bpf, const struct in_addr *ia) +bpf_filter_arp(const struct bpf *bpf, const struct in_addr *ia) { -#ifdef BIOCSETWF - if (bpf_arp_rw(bpf, ia, true) == -1 || - bpf_arp_rw(bpf, ia, false) == -1 || - ioctl(bpf->bpf_fd, BIOCLOCK) == -1) + if (bpf_arp_rw(bpf, ia, true) == -1) + return -1; + if (bpf_arp_rw(bpf, ia, false) == -1 && errno != ENOSYS) + return -1; + if (bpf_lock(bpf) == -1 && errno != ENOSYS) return -1; return 0; -#else - return bpf_arp_rw(bpf, ia, true); -#endif } #endif @@ -617,7 +433,6 @@ static const struct bpf_insn bpf_bootp_read[] = { }; #define BPF_BOOTP_READ_LEN __arraycount(bpf_bootp_read) -#ifdef BIOCSETWF static const struct bpf_insn bpf_bootp_write[] = { /* Make sure it's from and to the right port. * RFC2131 makes no mention of encforcing a source port, @@ -626,8 +441,7 @@ static const struct bpf_insn bpf_bootp_write[] = { BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, (BOOTPC << 16) + BOOTPS, 1, 0), BPF_STMT(BPF_RET + BPF_K, 0), }; -#define BPF_BOOTP_WRITE_LEN __arraycount(bpf_bootp_write) -#endif +#define BPF_BOOTP_WRITE_LEN __arraycount(bpf_bootp_write) #define BPF_BOOTP_CHADDR_LEN ((BOOTP_CHADDR_LEN / 4) * 3) #define BPF_BOOTP_XID_LEN 4 /* BOUND check is 4 instructions */ @@ -664,7 +478,6 @@ bpf_bootp_rw(const struct bpf *bpf, bool read) memcpy(bp, bpf_bootp_base, sizeof(bpf_bootp_base)); bp += BPF_BOOTP_BASE_LEN; -#ifdef BIOCSETWF if (!read) { memcpy(bp, bpf_bootp_write, sizeof(bpf_bootp_write)); bp += BPF_BOOTP_WRITE_LEN; @@ -673,11 +486,8 @@ bpf_bootp_rw(const struct bpf *bpf, bool read) BPF_SET_STMT(bp, BPF_RET + BPF_K, BPF_WHOLEPACKET); bp++; - return bpf_wattach(bpf->bpf_fd, buf, (unsigned int)(bp - buf)); + return bpf_setwfilter(bpf, buf, (unsigned int)(bp - buf)); } -#else - UNUSED(read); -#endif memcpy(bp, bpf_bootp_read, sizeof(bpf_bootp_read)); bp += BPF_BOOTP_READ_LEN; @@ -686,26 +496,17 @@ bpf_bootp_rw(const struct bpf *bpf, bool read) BPF_SET_STMT(bp, BPF_RET + BPF_K, BPF_WHOLEPACKET); bp++; - return bpf_attach(bpf->bpf_fd, buf, (unsigned int)(bp - buf)); + return bpf_setfilter(bpf, buf, (unsigned int)(bp - buf)); } int -bpf_bootp(const struct bpf *bpf, __unused const struct in_addr *ia) +bpf_filter_bootp(const struct bpf *bpf, __unused const struct in_addr *ia) { -#ifdef BIOCSETWF - if (bpf_bootp_rw(bpf, true) == -1 || bpf_bootp_rw(bpf, false) == -1 || - ioctl(bpf->bpf_fd, BIOCLOCK) == -1) + if (bpf_bootp_rw(bpf, true) == -1) + return -1; + if (bpf_bootp_rw(bpf, false) == -1 && errno != ENOSYS) + return -1; + if (bpf_lock(bpf) == -1 && errno != ENOSYS) return -1; return 0; -#else -#ifdef PRIVSEP -#if defined(__sun) /* Solaris cannot send via BPF. */ -#elif defined(BIOCSETF) -#warning No BIOCSETWF support - a compromised BPF can be used as a raw socket -#else -#warning A compromised PF_PACKET socket can be used as a raw socket -#endif -#endif - return bpf_bootp_rw(bpf, true); -#endif } diff --git a/src/bpf.h b/src/bpf.h index f15379b0..97248ab9 100644 --- a/src/bpf.h +++ b/src/bpf.h @@ -56,6 +56,7 @@ struct bpf { const struct interface *bpf_ifp; + void *bpf_handle; int bpf_fd; unsigned int bpf_flags; void *bpf_buffer; @@ -63,6 +64,7 @@ struct bpf { size_t bpf_len; size_t bpf_pos; }; +struct iovec; extern const char *bpf_name; size_t bpf_frame_header_len(const struct interface *); @@ -73,9 +75,13 @@ struct bpf *bpf_open(const struct interface *, int (*)(const struct bpf *, const struct in_addr *), const struct in_addr *); void bpf_close(struct bpf *); -int bpf_attach(int, void *, unsigned int); +int bpf_setfilter(const struct bpf *, void *, unsigned int); +int bpf_setwfilter(const struct bpf *, void *, unsigned int); +int bpf_lock(const struct bpf *); ssize_t bpf_send(const struct bpf *, uint16_t, const void *, size_t); +ssize_t bpf_writev(const struct bpf *, struct iovec *, int); ssize_t bpf_read(struct bpf *, void *, size_t); -int bpf_arp(const struct bpf *, const struct in_addr *); -int bpf_bootp(const struct bpf *, const struct in_addr *); + +int bpf_filter_arp(const struct bpf *, const struct in_addr *); +int bpf_filter_bootp(const struct bpf *, const struct in_addr *); #endif diff --git a/src/dhcp.c b/src/dhcp.c index 733e6ffd..f7195bc7 100644 --- a/src/dhcp.c +++ b/src/dhcp.c @@ -3859,7 +3859,7 @@ dhcp_openbpf(struct interface *ifp) if (state->bpf != NULL) return 0; - state->bpf = bpf_open(ifp, bpf_bootp, NULL); + state->bpf = bpf_open(ifp, bpf_filter_bootp, NULL); if (state->bpf == NULL) { if (errno == ENOENT) { logerrx("%s not found", bpf_name); diff --git a/src/if-linux.c b/src/if-linux.c index 0b7d18b0..3f678d29 100644 --- a/src/if-linux.c +++ b/src/if-linux.c @@ -1807,158 +1807,6 @@ if_initrt(struct dhcpcd_ctx *ctx, rb_tree_t *kroutes, int af) } #ifdef INET -/* Linux is a special snowflake when it comes to BPF. */ -const char *bpf_name = "Packet Socket"; - -/* Linux is a special snowflake for opening BPF. */ -struct bpf * -bpf_open(const struct interface *ifp, - int (*filter)(const struct bpf *, const struct in_addr *), - const struct in_addr *ia) -{ - struct bpf *bpf; - union sockunion { - struct sockaddr sa; - struct sockaddr_ll sll; - struct sockaddr_storage ss; - } su = { .sll = { - .sll_family = PF_PACKET, - .sll_protocol = htons(ETH_P_ALL), - .sll_ifindex = (int)ifp->index, - } }; -#ifdef PACKET_AUXDATA - int n; -#endif - - bpf = calloc(1, sizeof(*bpf)); - if (bpf == NULL) - return NULL; - bpf->bpf_ifp = ifp; - bpf->bpf_flags = BPF_EOF; - - /* Allocate a suitably large buffer for a single packet. */ - bpf->bpf_size = ETH_FRAME_LEN; - bpf->bpf_buffer = malloc(bpf->bpf_size); - if (bpf->bpf_buffer == NULL) - goto eexit; - - bpf->bpf_fd = xsocket(PF_PACKET, SOCK_RAW | SOCK_CXNB, - htons(ETH_P_ALL)); - if (bpf->bpf_fd == -1) - goto eexit; - - /* We cannot validate the correct interface, - * so we MUST set this first. */ - if (bind(bpf->bpf_fd, &su.sa, sizeof(su.sll)) == -1) - goto eexit; - - if (filter(bpf, ia) != 0) - goto eexit; - - /* In the ideal world, this would be set before the bind and filter. */ -#ifdef PACKET_AUXDATA - n = 1; - if (setsockopt(bpf->bpf_fd, SOL_PACKET, PACKET_AUXDATA, &n, - sizeof(n)) != 0) { - if (errno != ENOPROTOOPT) - goto eexit; - } -#endif - - /* - * At this point we could have received packets for the wrong - * interface or which don't pass the filter. - * Linux should flush upon setting the filter like every other OS. - * There is no way of flushing them from userland. - * As such, consumers need to inspect each packet to ensure it's valid. - * Or to put it another way, don't trust the Linux BPF filter. - */ - - return bpf; - -eexit: - if (bpf->bpf_fd != -1) - close(bpf->bpf_fd); - free(bpf->bpf_buffer); - free(bpf); - return NULL; -} - -/* BPF requires that we read the entire buffer. - * So we pass the buffer in the API so we can loop on >1 packet. */ -ssize_t -bpf_read(struct bpf *bpf, void *data, size_t len) -{ - ssize_t bytes; - struct iovec iov = { - .iov_base = bpf->bpf_buffer, - .iov_len = bpf->bpf_size, - }; - struct msghdr msg = { .msg_iov = &iov, .msg_iovlen = 1 }; -#ifdef PACKET_AUXDATA - union { - struct cmsghdr hdr; - uint8_t buf[CMSG_SPACE(sizeof(struct tpacket_auxdata))]; - } cmsgbuf = { .buf = { 0 } }; - struct cmsghdr *cmsg; - struct tpacket_auxdata *aux; -#endif - -#ifdef PACKET_AUXDATA - msg.msg_control = cmsgbuf.buf; - msg.msg_controllen = sizeof(cmsgbuf.buf); -#endif - - bytes = recvmsg(bpf->bpf_fd, &msg, 0); - if (bytes == -1) - return -1; - bpf->bpf_flags |= BPF_EOF; /* We only ever read one packet. */ - bpf->bpf_flags &= ~BPF_PARTIALCSUM; - if (bytes) { - if (bpf_frame_bcast(bpf->bpf_ifp, bpf->bpf_buffer) == 0) - bpf->bpf_flags |= BPF_BCAST; - else - bpf->bpf_flags &= ~BPF_BCAST; - if ((size_t)bytes > len) - bytes = (ssize_t)len; - memcpy(data, bpf->bpf_buffer, (size_t)bytes); -#ifdef PACKET_AUXDATA - for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; - cmsg = CMSG_NXTHDR(&msg, cmsg)) { - if (cmsg->cmsg_level == SOL_PACKET && - cmsg->cmsg_type == PACKET_AUXDATA) { - aux = (void *)CMSG_DATA(cmsg); - if (aux->tp_status & TP_STATUS_CSUMNOTREADY) - bpf->bpf_flags |= BPF_PARTIALCSUM; - } - } -#endif - } - return bytes; -} - -int -bpf_attach(int s, void *filter, unsigned int filter_len) -{ - struct sock_fprog pf = { - .filter = filter, - .len = (unsigned short)filter_len, - }; - - /* Install the filter. */ - if (setsockopt(s, SOL_SOCKET, SO_ATTACH_FILTER, &pf, sizeof(pf)) == -1) - return -1; - -#ifdef SO_LOCK_FILTER - int on = 1; - - if (setsockopt(s, SOL_SOCKET, SO_LOCK_FILTER, &on, sizeof(on)) == -1) - return -1; -#endif - - return 0; -} - int if_address(unsigned char cmd, const struct ipv4_addr *ia) { diff --git a/src/privsep-bpf.c b/src/privsep-bpf.c index dab7b802..940c576e 100644 --- a/src/privsep-bpf.c +++ b/src/privsep-bpf.c @@ -240,13 +240,13 @@ ps_bpf_cmd(struct dhcpcd_ctx *ctx, struct ps_msghdr *psm, struct msghdr *msg) case PS_BPF_ARP: psp->psp_proto = ETHERTYPE_ARP; psp->psp_protostr = "ARP"; - psp->psp_filter = bpf_arp; + psp->psp_filter = bpf_filter_arp; break; #endif case PS_BPF_BOOTP: psp->psp_proto = ETHERTYPE_IP; psp->psp_protostr = "BOOTP"; - psp->psp_filter = bpf_bootp; + psp->psp_filter = bpf_filter_bootp; break; }