#define _GNU_SOURCE #include /* secure_getenv */ #include #include #include #include #include #include #include #include #include /* open, O_NONBLOCK, &c */ #include #include #include #include #include #define __UAPI_DEF_IF_IFNAMSIZ 1 #include #include #include "sendfd.h" // From `man unix` #define SUN_PATH_SZ 108 #define N_CONNS 16 char *TEMP_PATHS[1024] = { 0 }; int LAST_TEMP_PATH = -1; #define IFR_FLAGS_ALLOWED (IFF_NO_PI | IFF_TAP | IFF_TUN | IFF_VNET_HDR | IFF_MULTI_QUEUE | IFF_PERSIST) #define IFR_FLAGS_DEFAULT (IFF_NO_PI | IFF_TAP | IFF_VNET_HDR | IFF_PERSIST) #define PTR_OR_DIE(expr) TRUE_OR_DIE((expr) != NULL) #define DO_OR_DIE(expr) TRUE_OR_DIE((expr) != -1) #define TRUE_OR_DIE(expr, ...) TRUE_OR_(EXIT_FAILURE, expr, __VA_ARGS__) #define TRUE_OR_WARN(expr, ...) TRUE_OR_(0, expr, __VA_ARGS__) #define TRUE_OR_(status, expr, ...) \ do if (!(expr)) { \ error(status, errno, "Failed assertion: " #expr "." __VA_ARGS__); \ } while(false) struct allow_pattern { // enum { USER = 1, GROUP = 2 } type; // union { uid_t uid, gid_t gid } xid; char *name; }; struct allow_patterns { size_t n; struct allow_pattern *patterns; }; /* Running on the same host, not caring for alignment */ struct tap_request { short ifrFlags; /* 0 to use defaults: IFF_TAP | IFF_NO_PI | IFF_VNET_HDR */ char name[IFNAMSIZ]; }; struct tap_reply { enum { OK = 0, AUTH_ERROR = 1 } status; char name[IFNAMSIZ]; }; int tuntap_alloc(char *dev, short openFlags, short ifrFlags, int *out_fd); bool match_mask(const char *test_addr, const char *expected_addr, const char *mask, int n) { for (int octet = 0; octet < n; ++octet) { if ((test_addr[octet] & mask[octet]) != expected_addr[octet]) { return false; } } return true; } /* * Adapted from spectrum's `mktuntap.c` (2019 Alyssa Ross * GPL-2.0-only), which in turn adapts `tun_alloc` from * `linux/Documentation/networking/tuntap.rst`. * * ifrFlags: IFF_TUN - TUN device (no Ethernet headers) * IFF_TAP - TAP device * * IFF_NO_PI - Do not provide packet information */ int tuntap_alloc(char *dev, short openFlags, short ifrFlags, int *out_fd) { struct ifreq ifr = { 0 }; int fd = -1, err = 0; DO_OR_DIE(fd = open("/dev/net/tun", openFlags)); if (dev != NULL) { int devLen = strlen(dev); if (devLen >= IFNAMSIZ) { /* If client requests a name, we do want the entire name to fit */ errno = EINVAL; return EINVAL; } strncpy(ifr.ifr_name, dev, IFNAMSIZ - 1); } ifr.ifr_flags = ifrFlags; TRUE_OR_WARN((err = ioctl(fd, TUNSETIFF, (void *)&ifr)) == 0); if (err != 0) { close(fd); return err; } strncpy(dev, ifr.ifr_name, IFNAMSIZ); *out_fd = fd; return 0; } int acceptRequests(const char *requestsPath, const struct allow_patterns *patterns) { int listener; struct sockaddr_un addr; const int t = 1; DO_OR_DIE(listener = socket(AF_UNIX, SOCK_SEQPACKET, 0)); DO_OR_DIE(setsockopt(listener, SOL_SOCKET, SO_PASSCRED, &t, sizeof(t))); addr.sun_family = AF_UNIX; strncpy(addr.sun_path, requestsPath, SUN_PATH_SZ - 1); DO_OR_DIE (bind(listener, &addr, sizeof(addr))); PTR_OR_DIE(TEMP_PATHS[++LAST_TEMP_PATH] = strdup(requestsPath)); DO_OR_DIE(listen(listener, N_CONNS)); for (;;) { /* Already changed my mind about looking at ucred, but keeping the code around for now */ int sock = -1; struct ucred cred = { 0 }; struct msghdr msg = { 0 }; struct cmsghdr *cmsg = NULL; struct iovec iov = { 0 }; struct tap_request req = { 0 }; msg.msg_iov = &iov; msg.msg_iovlen = 1; iov.iov_base = &req; iov.iov_len = sizeof(struct tap_request); DO_OR_DIE((sock = accept(listener, NULL, NULL))); TRUE_OR_DIE(recvmsg(sock, &msg, 0) > 0); req.name[IFNAMSIZ] = 0; for (cmsg = CMSG_FIRSTHDR(&msg); cmsg != NULL; cmsg = CMSG_NXTHDR(&msg, cmsg)) { if (cmsg->cmsg_level != SOL_SOCKET) { continue; } if (cmsg->cmsg_type != SCM_CREDENTIALS) { continue; } if (CMSG_LEN(cmsg) < sizeof(struct ucred)) { continue; } memcpy(&cred, CMSG_DATA(cmsg), sizeof(struct ucred)); break; } if (req.ifrFlags == 0) { req.ifrFlags = IFR_FLAGS_DEFAULT; } bool allowed = false; for (int i = 0; !allowed && i < patterns->n; ++i) { bool ifnameOk = fnmatch(patterns->patterns[i].name, req.name, 0) == 0; bool flagsOk = (req.ifrFlags & IFR_FLAGS_ALLOWED) == req.ifrFlags; allowed = ifnameOk && flagsOk; } struct tap_reply reply = { 0 }; if (!allowed) { reply.status = AUTH_ERROR; } if (allowed) { /* O_CLOEXEC? */ int fd = -1; TRUE_OR_DIE(tuntap_alloc(req.name, O_RDWR | O_NONBLOCK, req.ifrFlags, &fd) == 0); struct iovec iov = { 0 }; iov.iov_base = &reply; iov.iov_len = sizeof(struct tap_reply); TRUE_OR_DIE(send_fd(sock, fd, &iov) > 0); close(fd); } close(sock); } close(listener); } struct allow_patterns parsePatterns(const char *raw) { const size_t rawLen = strlen(raw); size_t nPatterns = 0; for (int i = 0; i < rawLen; ++i) { const int start = i; if (isspace(raw[i])) { continue; } for (; i < rawLen && !isspace(raw[i]); ++i) { } if (start < i) { ++nPatterns; } } struct allow_pattern *patterns = NULL; PTR_OR_DIE(patterns = calloc(nPatterns, sizeof(struct allow_pattern))); int iPattern = 0; for (int i = 0; i < rawLen; ++i) { if (isspace(raw[i])) { continue; } /* used to have per-group/per-user patterns, "u:$username:$pattern", &c - gone */ { const int start = i; for (; i < rawLen && !isspace(raw[i]); ++i) { } if (start < i) { PTR_OR_DIE(patterns[iPattern].name = strndup(&raw[start], i - start)); iPattern += 1; } } } struct allow_patterns out = { .n = nPatterns, .patterns = patterns }; return out; } int get(const char *servePath, const char *ifname, short ifrFlags) { /* TODO: sock: move out */ int sock; struct sockaddr_un addr; DO_OR_DIE(sock = socket(AF_UNIX, SOCK_SEQPACKET, 0)); addr.sun_family = AF_UNIX; strncpy(addr.sun_path, servePath, SUN_PATH_SZ - 1); DO_OR_DIE (connect(sock, &addr, sizeof(addr))); struct msghdr msg = { 0 }; struct cmsghdr *cmsg = NULL; struct iovec iov = { 0 }; struct tap_request req = { 0 }; strncpy(req.name, ifname, IFNAMSIZ - 1); req.ifrFlags = ifrFlags; msg.msg_iov = &iov; msg.msg_iovlen = 1; iov.iov_base = &req; iov.iov_len = sizeof(struct tap_request); TRUE_OR_DIE(sendmsg(sock, &msg, 0) > 0); int tunFd = -1; DO_OR_DIE(tunFd = recv_fd(sock, 0)); close(sock); return tunFd; } void cleanup(int signo, siginfo_t *info, void *_context) { for (int i = 0; i <= LAST_TEMP_PATH; ++i) { TRUE_OR_DIE(unlink(TEMP_PATHS[i]) != -1 || errno == ENOENT); } if (signo == SIGINT) { exit(EXIT_SUCCESS); } errx(EXIT_FAILURE, "Exiting with signal %d", signo); } int main(int argc, char **argv) { struct sigaction act = { 0 }; act.sa_flags = SA_SIGINFO; act.sa_sigaction = cleanup; DO_OR_DIE(sigaction(SIGINT, &act, NULL)); DO_OR_DIE(sigaction(SIGSEGV, &act, NULL)); bool cmdServe = false; bool cmdPass = false; char *ifname = "vt%d"; char **rest = argv + 1; char **end = argv + argc; TRUE_OR_DIE(argc > 1); if (strcmp(rest[0], "serve") == 0) { cmdServe = true; ++rest; } else if (strcmp(rest[0], "pass") == 0) { cmdPass = true; ++rest; for (; rest != end && rest[0][0] == '-'; ++rest) { if (strcmp(rest[0], "--")) { break; } else if (strncmp(rest[0], "--ifname=", sizeof("--ifname="))) { ifname = rest[0] + sizeof("--ifname="); } } } else { error(EINVAL, EINVAL, "no subcommand \"%s\"", rest[0]); } int nextArgc = argc - (rest - argv); char * const* nextArgv = rest; const char *patternsRaw = secure_getenv("TAPS_ALLOW"); if (patternsRaw == NULL) { patternsRaw = "*"; } struct allow_patterns patterns = { 0 }; if (cmdServe) { PTR_OR_DIE((patterns = parsePatterns(patternsRaw)).patterns); } const char *servePath = secure_getenv("TAPS_SOCK"); if (servePath == NULL) { servePath = "taps.sock"; } if (cmdServe) { acceptRequests(servePath, &patterns); } else if (cmdPass) { TRUE_OR_DIE(nextArgc > 0); int fd = -1; DO_OR_DIE(fd = get(servePath, ifname, 0)); if (fd != 3) { DO_OR_DIE(dup2(fd, 3)); close(fd); fd = 3; } DO_OR_DIE(execvp(nextArgv[0], nextArgv)); } else { error(EINVAL, EINVAL, "subcommand args"); } return 0; }