pax_global_header00006660000000000000000000000064131756102710014515gustar00rootroot0000000000000052 comment=59e836232816b89fbc7071ee5d6caa0c4038b5f8 resolvconf-admin-0.3/000077500000000000000000000000001317561027100146255ustar00rootroot00000000000000resolvconf-admin-0.3/.dir-locals.el000066400000000000000000000001311317561027100172510ustar00rootroot00000000000000((c-mode (indent-tabs-mode . nil) (c-basic-offset . 2) (c-file-style . "linux")) ) resolvconf-admin-0.3/.gitignore000066400000000000000000000001651317561027100166170ustar00rootroot00000000000000resolvconf-admin resolvconf-admin.1 resolvconf-admin.1.md *~ tests/resolv.conf tests/getifname resolvconf-admin-test resolvconf-admin-0.3/CHANGES000066400000000000000000000004431317561027100156210ustar00rootroot00000000000000Version 0.3 ----------- 2017-10-30 - clear the PATH before exec (avoids trivial privilege escalation) Version 0.2 ----------- 2017-09-18 - support "make check" on platforms that don't have an interface named "lo" Version 0.1 ----------- 2017-09-16 - initial release, inspired by dhcpcanon resolvconf-admin-0.3/LICENSE.md000066400000000000000000000017771317561027100162450ustar00rootroot00000000000000Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. resolvconf-admin-0.3/Makefile000066400000000000000000000033701317561027100162700ustar00rootroot00000000000000#!/usr/bin/make -f OBJECTS = resolvconf-admin resolvconf-admin.1 CFLAGS += -Wall -Werror -pedantic -g SBINRESOLVCONF ?= /sbin/resolvconf ETCRESOLVCONF ?= /etc/resolv.conf FILENAMES = -DSBINRESOLVCONF=\"$(SBINRESOLVCONF)\" -DETCRESOLVCONF=\"$(ETCRESOLVCONF)\" PREFIX ?= /usr MANPATH ?= $(PREFIX)/share/man VERSION ?= $(shell head -n1 < CHANGES | cut -f2 -d\ ) all: $(OBJECTS) %: %.c $(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) $(FILENAMES) -o $@ $< resolvconf-admin.1.md: resolvconf-admin.1.md.in sed -e 's!@SBINRESOLVCONF@!$(SBINRESOLVCONF)!g' \ -e 's!@ETCRESOLVCONF@!$(ETCRESOLVCONF)!g' \ < $< > $@ resolvconf-admin.1: resolvconf-admin.1.md pandoc -s -f markdown -t man -o $@ $< resolvconf-admin-test: resolvconf-admin.c $(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) -DSBINRESOLVCONF=\"$(CURDIR)/tests/dummy-resolvconf2\" -DETCRESOLVCONF=\"$(CURDIR)/tests/resolv.conf\" -o $@ $< check: resolvconf-admin-test tests/run tests/dummy-resolvconf tests/getifname tests/run install: resolvconf-admin resolvconf-admin.1 install -D -m 4754 resolvconf-admin $(DESTDIR)$(PREFIX)/bin/resolvconf-admin install -D -m 0644 resolvconf-admin.1 $(DESTDIR)$(MANPATH)/man1/resolvconf-admin.1 clean: rm -f $(OBJECTS) resolvconf-admin-test tests/resolv.conf tests/dummy-resolvconf2 resolvconf-admin.1.md tests/getifname # for upstream maintainer working from git only: release: git tag -d resolvconf-admin-$(VERSION) || true git tag -s resolvconf-admin-$(VERSION) -m 'tagging release of resolvconf-admin $(VERSION)' master git archive --format=tar --prefix=resolvconf-admin-$(VERSION)/ resolvconf-admin-$(VERSION) | gzip -9n > ../resolvconf-admin_$(VERSION).orig.tar.gz gpg --armor --detach-sign ../resolvconf-admin_$(VERSION).orig.tar.gz .PHONY: all clean check install release resolvconf-admin-0.3/README.md000066400000000000000000000042771317561027100161160ustar00rootroot00000000000000resolvconf-admin ================ `resolvconf-admin` is a setuid helper program for tools that need to be able to set up the local DNS resolver configuration. This program deals with setting the local DNS resolver configuration (i.e. `/etc/resolv.conf`), which needs to be done as root on some systems. One example use case is to run a DHCP client without giving that DHCP client full superuser privileges. Theory of Operation ------------------- If `/sbin/resolvconf` is present and executable, it is invoked as root with the specified configuration. If `/sbin/resolvconf` is not present (or is present but not executable), then `/etc/resolv.conf` is updated directly. WARNING!!! ---------- A better approach for setting up the DNS in a non-privileged way is to make an authenticated IPC call to some [running daemon that already manages `/etc/resolv.conf`](https://www.freedesktop.org/wiki/Software/systemd/resolved/). However, some systems do not run such a daemon, so we offer this setuid approach instead, for those limited systems only. This setuid program *should not* be installed on systems that already run such a daemon, because every setuid program increases the attack surface of the operating system. *DO NOT INSTALL THIS TOOL IF YOU HAVE BETTER OPTIONS AVAILABLE TO YOU!* Installation ------------ It should probably be installed as `/usr/bin/resolvconf-admin` something like this: getent group resolvconf-admins >/dev/null || addgroup --system resolvconf-admins chown root:resolvconf-admins /usr/bin/resolvconf-admin chmod 4754 /usr/bin/resolvconf-admin and then make sure the user that you care about has access, by adding them to this group: adduser my-nonpriv-dhcp-daemon resolvconf-admins Usage ----- When the non-privileged user wants to set local DNS resolvers due to information it learned from interface NETIF, it should invoke: resolvconf-admin add NETIF [-s SEARCH] [-d DOMAIN] NAMESERVER [...] Note that DNS search path and domain name are optional. However, at least one nameserver is required. When the non-privileged user wants to tear down the DNS resolver information that it had previously set for interface NETIF, it should invoke: resolvconf-admin del NETIF resolvconf-admin-0.3/resolvconf-admin.1.md.in000066400000000000000000000051101317561027100211560ustar00rootroot00000000000000--- title: RESOLVCONF-ADMIN section: 1 author: Daniel Kahn Gillmor date: 2017 September --- NAME ==== resolvconf-admin - a setuid program for setting up DNS resolution SYNOPSIS ======== resolvconf-admin add NETIF [-s SEARCH] [-d DOMAIN] NAMESERVER [...] resolvconf-admin del NETIF DESCRIPTION =========== This setuid program allows specific non-privileged users to invoke `@SBINRESOLVCONF@` (if it is present) with a constrained argument to add or remove DNS resolvers; or, if `@SBINRESOLVCONF@` is not executable, it can replace `@ETCRESOLVCONF@`. This is useful, for example, for running a DHCP client as a non-privileged user. When the non-privileged user wants to set up the DNS resolvers due to information it learned from interface NETIF, it should invoke: resolvconf-admin add NETIF [-s SEARCH] [-d DOMAIN] NAMESERVER [...] Note that DNS search path and domain name are optional. However, at least one nameserver is required. When the non-privileged user wants to tear down the DNS resolver information that it had previously set for interface NETIF, it should invoke: resolvconf-admin del NETIF WARNING ======= A better (non-suid) approach for setting up the DNS in a non-privileged way is to make an authenticated IPC call to some running daemon that already manages the local DNS resolution configuration (e.g., `systemd-resolved(8)`). However, some systems do not run such a daemon, so we offer this setuid approach instead, for those limited systems only. This setuid program *should not* be installed on systems that already run such a daemon, because every setuid program increases the attack surface of the operating system. *DO NOT INSTALL THIS TOOL IF YOU HAVE BETTER OPTIONS AVAILABLE TO YOU!* INTERLEAVED OPERATION WITHOUT RESOLVCONF(8) =========================================== On a system where `resolvconf(8)` is not installed, the behavior is not very sophisticated. On these systems: * The first time `resolvconf-admin add` is invoked, the old `@ETCRESOLVCONF@` is backed up to `@ETCRESOLVCONF@.bak.resolvconf-admin`. * The first time `resolvconf-admin del` is invoked, the backed up file is restored. If multiple daemons (or a single daemon monitoring multiple sources of DNS resolver information) invokes `resolvconf-admin` in an interleaved fashion (e.g. two `add`s before a `del`), this will almost certainly not be the behavior that you want. If your system is likely to have this kind of interleaved operation, it should also have `resolvconf(8)` installed. SEE ALSO ======== resolvconf(8), resolv.conf(5), systemd-resolved(8) resolvconf-admin-0.3/resolvconf-admin.c000066400000000000000000000267601317561027100202520ustar00rootroot00000000000000/* setuid helper program for tools that need to be able to set up the local DNS. * * Author: Daniel Kahn Gillmor #include #include #include #include #include #include #include #include #include #include #include typedef struct { char data[INET_ADDRSTRLEN]; } addrstr; #ifndef PROGNAME #define PROGNAME "resolvconf-admin" #endif #ifndef PREAMBLE #define PREAMBLE "# Generated by " PROGNAME #endif #ifndef BACKUPNAME #define BACKUPNAME ETCRESOLVCONF ".bak." PROGNAME #endif #ifndef EXECPATH #define EXECPATH "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" #endif char * const envp[] = { "PATH=" EXECPATH, NULL }; /* return 0 on success */ int canonicalize_ip(const char *in, addrstr* out) { struct in_addr a; if (inet_pton(AF_INET, in, &a) != 1) return 1; if (NULL == inet_ntop(AF_INET, &a, out->data, sizeof(out->data))) { perror("inet_ntop failed"); return 2; } return 0; } /* returns 1 if ifname is the name of a legitimate network interface on the current system, 0 if not */ int is_valid_interface(const char *ifname) { struct ifaddrs *ifa, *cur; if (getifaddrs(&ifa)) { perror("Failed to getifaddrs()"); return 0; } int found = 0; for (cur = ifa; cur; cur = cur->ifa_next) { if (strcmp(ifname, cur->ifa_name) == 0) { found = 1; break; } } freeifaddrs(ifa); return found; } /* returns 1 if we think domain is a valid argument for "search" or "domain" paths, 0 if not. allowblank should be 1 for "search", 0 for "domain". see resolv.conf(5) for more details. */ int is_valid_domain(const char *domain, int allowblank) { if (domain == NULL || *domain == 0) return 0; while (*domain) { if ((!(allowblank && isblank(*domain))) && (isspace(*domain) || (!isgraph(*domain)) || (ispunct(*domain) && *domain != '.'))) return 0; domain++; } return 1; } void usage(FILE* f, const char *arg0) { if (!arg0) arg0 = PROGNAME; fprintf(stderr, "Usage:\t%s add IFNAME [-d DOMAIN] [-s SEARCH] NAMESERVERIP [...]\n" "\t%s del IFNAME\n" "\t%s help\n" "\n" "If " SBINRESOLVCONF " is present, invoke it appropriately.\n" "If " SBINRESOLVCONF " is not present, update " ETCRESOLVCONF " directly.\n", arg0, arg0, arg0); } int write_new_resolvconf(FILE* writer, int argc, const char **argv, const char *preamble) { int i; addrstr canonical; const char *search = NULL, *domain = NULL; fprintf(writer, "%s", preamble); int total_nameservers = 0; for (i = 0; i < argc; i++) { if ((strcmp(argv[i], "-d") == 0) || (strcmp(argv[i], "-s") == 0)) { i++; if (i >= argc) { fprintf(stderr, "Need DOMAIN argument for %s\n", argv[i-1]); return 1; } if (argv[i-1][1] == 'd') { if (domain) { fprintf(stderr, "only one -d DOMAIN option allowed\n"); return 1; } if (!is_valid_domain(argv[i], 0)) { fprintf(stderr, "Non-domain-name argument for -d: \"%s\", skipping..\n", argv[i]); continue; } domain = argv[i]; } else if (argv[i-1][1] == 's') { if (search) { fprintf(stderr, "only one -s SEARCH option allowed\n"); return 1; } if (!is_valid_domain(argv[i], 1)) { fprintf(stderr, "bad argument for domain search list -s: \"%s\", skipping...\n", argv[i]); continue; } search = argv[i]; } else { fprintf(stderr, "something went wrong with argument parsing\n"); return 1; } } else { /* this should be a nameserver */ if (canonicalize_ip(argv[i], &canonical)) { fprintf(stderr, "Failed to canonicalize IP address '%s'\n", argv[i]); return 1; } fprintf(writer, "nameserver %s\n", canonical.data); total_nameservers++; } } if (total_nameservers < 1) { fprintf(stderr, "not enough nameservers\n"); return 1; } if (domain) fprintf(writer, "domain %s\n", domain); if (search) fprintf(writer, "search %s\n", search); return 0; } /* return 1 if it starts with the preamble; 0 if it exists and does not start with the preamble; -1 if there was any weirdness that prevented us from checking. */ int resolvconf_starts_with(const char *preamble) { int ret = -1; int oldfd = open(ETCRESOLVCONF, O_RDONLY); if (oldfd == -1) { perror("Failed to open " ETCRESOLVCONF " for reading"); } else { struct stat statbuf; size_t sz = strlen(preamble); if (fstat(oldfd, &statbuf)) { perror("failed to stat " ETCRESOLVCONF " after opening"); return -1; } if (statbuf.st_size < sz) return 0; char* oldcontents = (char *)mmap(NULL, sz, PROT_READ, MAP_PRIVATE, oldfd, 0); if (oldcontents == MAP_FAILED) { perror("Failed to mmap " ETCRESOLVCONF); return -1; } else { ret = (strncmp(preamble, oldcontents, sz) == 0); if (munmap(oldcontents, sz)) perror("failed to mumap " ETCRESOLVCONF); } if (close(oldfd)) perror("failed to close " ETCRESOLVCONF); } return ret; } int main(int argc, const char **argv) { int pipes[2] = { -1, -1 }; int use_sbin_resolvconf = 0; char tmpname[] = ETCRESOLVCONF "." PROGNAME ".XXXXXX"; int ret; const char *ifname = NULL; char label[1024]; char preamble[1024]; int do_add = 0; label[sizeof(label)-1] = 0; preamble[sizeof(preamble)-1] = 0; preamble[sizeof(preamble)-2] = '\n'; /* ensure that there is a trailing newline if ifname happens to be enormous */ if (argc < 2) { usage(stderr, argv[0]); return 1; } if (strcmp(argv[1], "help") == 0) { usage(stdout, argv[0]); return 0; } if (strcmp(argv[1], "add") == 0) { do_add = 1; } else if (strcmp(argv[1], "del") == 0) { } else { usage(stderr, argv[0]); return 1; } ifname = argv[2]; if (!is_valid_interface(ifname)) { fprintf(stderr, "'%s' is not a valid network interface on this host\n", ifname); return 1; } if (access(SBINRESOLVCONF, X_OK)) { if (errno != ENOENT) { perror("cannot execute " SBINRESOLVCONF); fprintf(stderr, "falling back to overwriting " ETCRESOLVCONF " manually...\n"); } } else { use_sbin_resolvconf = 1; } snprintf(preamble, sizeof(preamble)-2, "%s\n# (%s)\n", PREAMBLE, ifname); if (use_sbin_resolvconf) { snprintf(label, sizeof(label)-1, "%s.%d." PROGNAME, ifname, getuid()); if (do_add) { if (pipe(pipes)) { perror("Failed to open a pipe"); return 1; } } else { execle(SBINRESOLVCONF, "resolvconf", "-d", label, NULL, envp); perror(SBINRESOLVCONF " -d failed."); return 1; } } else { if (do_add) { pipes[1] = mkstemp(tmpname); if (pipes[1] == -1) { perror("Failed to make a temporary file for overwriting " ETCRESOLVCONF); return 1; } } else { if (resolvconf_starts_with(preamble) == 1) { if (access(BACKUPNAME, F_OK) == 0) { if (rename(BACKUPNAME, ETCRESOLVCONF)) perror("Failed to restore " BACKUPNAME " to " ETCRESOLVCONF); } } else { fprintf(stderr, "%s is not created by %s for interface %s, not destroying.\n", ETCRESOLVCONF, PROGNAME, ifname); } return 0; } } FILE *writer = fdopen(pipes[1], "w"); if (writer == NULL) { perror("Failed to set up buffered writer"); return 1; } ret = write_new_resolvconf(writer, argc-3, argv+3, preamble); if (ret) { if (writer) fclose(writer); if (!use_sbin_resolvconf) { if (unlink(tmpname)) fprintf(stderr, "failed to unlink tempfile '%s': (%d) %s\n", tmpname, errno, strerror(errno)); } return ret; } if (use_sbin_resolvconf) { if (fclose(writer)) { perror("Failed to close write side of pipe"); return 1; } if (-1 == dup2(pipes[0], 0)) { fprintf(stderr, "dup2(%d, 0) failed (%d) %s\n", pipes[0], errno, strerror(errno)); return 1; } execle(SBINRESOLVCONF, "resolvconf", "-a", label, NULL, envp); /* should never get here! */ perror(SBINRESOLVCONF " -a failed"); return 1; } else { struct stat statbuf; if (stat(ETCRESOLVCONF, &statbuf)) { perror("failed to stat " ETCRESOLVCONF); fprintf(stderr, "Not copying ownership and permissions\n"); } else { /* if possible, copy ownership and permissions */ if (fchown(fileno(writer), statbuf.st_uid, statbuf.st_gid)) perror("failed to copy ownership for " ETCRESOLVCONF); if (fchmod(fileno(writer), statbuf.st_mode)) perror("failed to copy permissions for " ETCRESOLVCONF); /* TODO: if possible, copy over ACL as well? would need to link to libacl, which expands attack surface */ } if (fclose(writer)) { perror("Failed to close write side of pipe"); return 1; } /* back up old /etc/resolv.conf which we are clobbering here, if the old file doesn't start with PREAMBLE. note that we only check PREAMBLE here (that is, without the ifname), because we don't want two running instances (e.g. on different interfaces) to be backing up each other's data. */ if (0 == access(ETCRESOLVCONF, F_OK)) { if (resolvconf_starts_with(PREAMBLE) != 1) { if (0 == access(BACKUPNAME, F_OK)) { if (unlink(BACKUPNAME)) { if (errno != ENOENT) perror("unlink(\"" BACKUPNAME "\") failed while backing up " ETCRESOLVCONF); } } if (link(ETCRESOLVCONF, BACKUPNAME)) perror("failed to back up " ETCRESOLVCONF " to " BACKUPNAME); } } if (rename(tmpname, ETCRESOLVCONF)) { fprintf(stderr, "failed to rename \"%s\" to \"%s\": (%d) %s\n", tmpname, ETCRESOLVCONF, errno, strerror(errno)); } } } resolvconf-admin-0.3/tests/000077500000000000000000000000001317561027100157675ustar00rootroot00000000000000resolvconf-admin-0.3/tests/dummy-resolvconf000077500000000000000000000000771317561027100212320ustar00rootroot00000000000000#!/bin/sh echo invoked as "$0 $*" exec cat > tests/resolv.conf resolvconf-admin-0.3/tests/getifname.c000066400000000000000000000010551317561027100200730ustar00rootroot00000000000000#include #include #include /* print the name of the first discovered network interface on stdout */ int main(int argc, const char **argv) { struct ifaddrs *ifa, *cur; if (getifaddrs(&ifa)) { perror("Failed to getifaddrs()"); return 1; } for (cur = ifa; cur; cur = cur->ifa_next) { if (cur->ifa_name && cur->ifa_name[0] != 0) { printf("%s\n", cur->ifa_name); freeifaddrs(ifa); return 0; } } freeifaddrs(ifa); perror("no interfaces found via getifaddrs()"); return 2; } resolvconf-admin-0.3/tests/run000077500000000000000000000016041317561027100165220ustar00rootroot00000000000000#!/bin/sh # Author: Daniel Kahn Gillmor set -e set -x NETIF=$(tests/getifname) cleanup() { rm -f tests/dummy-resolvconf2 } trap cleanup EXIT # TODO: fill in the tests here! ./resolvconf-admin-test add "$NETIF" 4.2.2.1 [ "$(grep '^[^#]' < tests/resolv.conf)" = "nameserver 4.2.2.1" ] ./resolvconf-admin-test add "$NETIF" 4.2.2.2 4.2.2.1 [ "$(grep '^[^#]' < tests/resolv.conf)" = "$(printf "nameserver 4.2.2.2\nnameserver 4.2.2.1")" ] ln -s dummy-resolvconf tests/dummy-resolvconf2 ./resolvconf-admin-test add "$NETIF" 8.8.8.8 [ "$(grep '^[^#]' < tests/resolv.conf)" = "nameserver 8.8.8.8" ] ./resolvconf-admin-test add "$NETIF" 1.2.3.4 5.6.7.8 [ "$(grep '^[^#]' < tests/resolv.conf)" = "$(printf "nameserver 1.2.3.4\nnameserver 5.6.7.8")" ] # TODO: test with non-existent interface # TODO: test with DOMAIN and SEARCH # TODO: test "resolvconf-admin del"