/* $OpenBSD: tftp-proxy.c,v 1.5 2012/10/06 16:29:51 jmc Exp $ * * Copyright (c) 2005 DLS Internet Services * Copyright (c) 2004, 2005 Camiel Dobbelaar, * * 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. * 3. The name of the author may not be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 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 #include #include #include #include #include #include #include #include #include #include "filter.h" #define CHROOT_DIR "/var/empty" #define NOPRIV_USER "proxy" #define DEFTRANSWAIT 2 #define NTOP_BUFS 4 #define PKTSIZE SEGSIZE+4 const char *opcode(int); const char *sock_ntop(struct sockaddr *); static void usage(void); struct proxy_listener { struct event ev; TAILQ_ENTRY(proxy_listener) entry; int (*cmsg2dst)(struct cmsghdr *, struct sockaddr_storage *); int s; }; void proxy_listen(const char *, const char *, int); void proxy_listener_events(void); int proxy_dst4(struct cmsghdr *, struct sockaddr_storage *); int proxy_dst6(struct cmsghdr *, struct sockaddr_storage *); void proxy_recv(int, short, void *); struct fd_reply { TAILQ_ENTRY(fd_reply) entry; int fd; }; struct privproc { struct event pop_ev; struct event push_ev; TAILQ_HEAD(, fd_reply) replies; struct evbuffer *buf; }; void proxy_privproc(int, struct passwd *); void privproc_push(int, short, void *); void privproc_pop(int, short, void *); void unprivproc_push(int, short, void *); void unprivproc_pop(int, short, void *); void unprivproc_timeout(int, short, void *); char ntop_buf[NTOP_BUFS][INET6_ADDRSTRLEN]; struct loggers { void (*err)(int, const char *, ...); void (*errx)(int, const char *, ...); void (*warn)(const char *, ...); void (*warnx)(const char *, ...); void (*info)(const char *, ...); }; const struct loggers conslogger = { err, errx, warn, warnx, warnx }; void syslog_err(int, const char *, ...); void syslog_errx(int, const char *, ...); void syslog_warn(const char *, ...); void syslog_warnx(const char *, ...); void syslog_info(const char *, ...); void syslog_vstrerror(int, int, const char *, va_list); const struct loggers syslogger = { syslog_err, syslog_errx, syslog_warn, syslog_warnx, syslog_info, }; const struct loggers *logger = &conslogger; #define lerr(_e, _f...) logger->err((_e), _f) #define lerrx(_e, _f...) logger->errx((_e), _f) #define lwarn(_f...) logger->warn(_f) #define lwarnx(_f...) logger->warnx(_f) #define linfo(_f...) logger->info(_f) __dead void usage(void) { extern char *__progname; fprintf(stderr, "usage: %s [-46dv] [-l address] [-p port]" " [-w transwait]\n", __progname); exit(1); } int debug = 0; int verbose = 0; struct timeval transwait = { DEFTRANSWAIT, 0 }; int on = 1; struct addr_pair { struct sockaddr_storage src; struct sockaddr_storage dst; }; struct proxy_request { char buf[SEGSIZE_MAX + 4]; size_t buflen; struct addr_pair addrs; struct event ev; TAILQ_ENTRY(proxy_request) entry; u_int32_t id; }; struct proxy_child { TAILQ_HEAD(, proxy_request) fdrequests; TAILQ_HEAD(, proxy_request) tmrequests; struct event push_ev; struct event pop_ev; struct evbuffer *buf; }; struct proxy_child *child = NULL; TAILQ_HEAD(, proxy_listener) proxy_listeners; int main(int argc, char *argv[]) { extern char *__progname; int c; const char *errstr; struct passwd *pw; char *addr = "localhost"; char *port = "6969"; int family = AF_UNSPEC; int pair[2]; while ((c = getopt(argc, argv, "46dvl:p:w:")) != -1) { switch (c) { case '4': family = AF_INET; break; case '6': family = AF_INET6; break; case 'd': verbose = debug = 1; break; case 'l': addr = optarg; break; case 'p': port = optarg; break; case 'v': verbose = 1; break; case 'w': transwait.tv_sec = strtonum(optarg, 1, 30, &errstr); if (errstr) errx(1, "wait is %s", errstr); break; default: usage(); /* NOTREACHED */ } } if (geteuid() != 0) errx(1, "need root privileges"); if (!debug && daemon(1, 0) == -1) err(1, "daemon"); if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, pair) == -1) lerr(1, "socketpair"); pw = getpwnam(NOPRIV_USER); if (pw == NULL) lerrx(1, "no %s user", NOPRIV_USER); switch (fork()) { case -1: lerr(1, "fork"); case 0: setproctitle("privproc"); close(pair[1]); proxy_privproc(pair[0], pw); /* this never returns */ default: setproctitle("unprivproc"); close(pair[0]); break; } child = calloc(1, sizeof(*child)); if (child == NULL) lerr(1, "alloc(child)"); child->buf = evbuffer_new(); if (child->buf == NULL) lerr(1, "child evbuffer"); TAILQ_INIT(&child->fdrequests); TAILQ_INIT(&child->tmrequests); if (!debug) { openlog(__progname, LOG_PID|LOG_NDELAY, LOG_DAEMON); tzset(); logger = &syslogger; } proxy_listen(addr, port, family); /* open /dev/pf */ init_filter(NULL, verbose); /* revoke privs */ pw = getpwnam(NOPRIV_USER); if (!pw) lerrx(1, "no such user %s", NOPRIV_USER); if (chroot(CHROOT_DIR) == -1) lerr(1, "chroot %s", CHROOT_DIR); if (chdir("/") == -1) lerr(1, "chdir %s", CHROOT_DIR); if (setgroups(1, &pw->pw_gid) || setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) || setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid)) err(1, "unable to revoke privs"); event_init(); proxy_listener_events(); if (ioctl(pair[1], FIONBIO, &on) == -1) lerr(1, "ioctl(FIONBIO)"); event_set(&child->pop_ev, pair[1], EV_READ | EV_PERSIST, unprivproc_pop, NULL); event_set(&child->push_ev, pair[1], EV_WRITE, unprivproc_push, NULL); event_add(&child->pop_ev, NULL); event_dispatch(); return(0); } void proxy_privproc(int s, struct passwd *pw) { extern char *__progname; struct privproc p; if (!debug) { openlog(__progname, LOG_PID|LOG_NDELAY, LOG_DAEMON); tzset(); logger = &syslogger; } if (ioctl(s, FIONBIO, &on) == -1) lerr(1, "ioctl(FIONBIO)"); if (chroot(CHROOT_DIR) == -1) lerr(1, "chroot to %s", CHROOT_DIR); if (chdir("/") == -1) lerr(1, "chdir to %s", CHROOT_DIR); if (setgroups(1, &pw->pw_gid) || setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid)) lerr(1, "unable to set group ids"); TAILQ_INIT(&p.replies); p.buf = evbuffer_new(); if (p.buf == NULL) err(1, "pop evbuffer_new"); event_init(); event_set(&p.pop_ev, s, EV_READ | EV_PERSIST, privproc_pop, &p); event_set(&p.push_ev, s, EV_WRITE, privproc_push, &p); event_add(&p.pop_ev, NULL); event_dispatch(); } void privproc_pop(int fd, short events, void *arg) { struct addr_pair req; struct privproc *p = arg; struct fd_reply *rep; int add = 0; switch (evbuffer_read(p->buf, fd, sizeof(req))) { case 0: lerrx(1, "unprivproc has gone"); case -1: switch (errno) { case EAGAIN: case EINTR: return; default: lerr(1, "privproc_pop read"); } default: break; } while (EVBUFFER_LENGTH(p->buf) >= sizeof(req)) { evbuffer_remove(p->buf, &req, sizeof(req)); /* do i really need to check this? */ if (req.src.ss_family != req.dst.ss_family) lerrx(1, "family mismatch"); rep = calloc(1, sizeof(*rep)); if (rep == NULL) lerr(1, "reply calloc"); rep->fd = socket(req.src.ss_family, SOCK_DGRAM, IPPROTO_UDP); if (rep->fd == -1) lerr(1, "privproc socket"); if (ioctl(rep->fd, FIONBIO, &on) == -1) err(1, "privproc ioctl(FIONBIO)"); if (setsockopt(rep->fd, SOL_SOCKET, SO_BINDANY, &on, sizeof(on)) == -1) lerr(1, "privproc setsockopt(BINDANY)"); if (setsockopt(rep->fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1) lerr(1, "privproc setsockopt(REUSEADDR)"); if (setsockopt(rep->fd, SOL_SOCKET, SO_REUSEPORT, &on, sizeof(on)) == -1) lerr(1, "privproc setsockopt(REUSEPORT)"); if (bind(rep->fd, (struct sockaddr *)&req.src, req.src.ss_len) == -1) lerr(1, "privproc bind"); if (TAILQ_EMPTY(&p->replies)) add = 1; TAILQ_INSERT_TAIL(&p->replies, rep, entry); } if (add) event_add(&p->push_ev, NULL); } void privproc_push(int fd, short events, void *arg) { struct privproc *p = arg; struct fd_reply *rep; struct msghdr msg; union { struct cmsghdr hdr; char buf[CMSG_SPACE(sizeof(int))]; } cmsgbuf; struct cmsghdr *cmsg; struct iovec iov; int result = 0; while ((rep = TAILQ_FIRST(&p->replies)) != NULL) { memset(&msg, 0, sizeof(msg)); msg.msg_control = (caddr_t)&cmsgbuf.buf; msg.msg_controllen = sizeof(cmsgbuf.buf); cmsg = CMSG_FIRSTHDR(&msg); cmsg->cmsg_len = CMSG_LEN(sizeof(int)); cmsg->cmsg_level = SOL_SOCKET; cmsg->cmsg_type = SCM_RIGHTS; *(int *)CMSG_DATA(cmsg) = rep->fd; iov.iov_base = &result; iov.iov_len = sizeof(int); msg.msg_iov = &iov; msg.msg_iovlen = 1; switch (sendmsg(fd, &msg, 0)) { case sizeof(int): break; case -1: if (errno == EAGAIN) goto again; lerr(1, "privproc sendmsg"); /* NOTREACHED */ default: lerrx(1, "privproc sendmsg weird len"); } TAILQ_REMOVE(&p->replies, rep, entry); close(rep->fd); free(rep); } if (TAILQ_EMPTY(&p->replies)) return; again: event_add(&p->push_ev, NULL); } void proxy_listen(const char *addr, const char *port, int family) { struct proxy_listener *l; struct addrinfo hints, *res, *res0; int error; int s, on = 1; int serrno; const char *cause = NULL; memset(&hints, 0, sizeof(hints)); hints.ai_family = family; hints.ai_socktype = SOCK_DGRAM; hints.ai_flags = AI_PASSIVE; TAILQ_INIT(&proxy_listeners); error = getaddrinfo(addr, port, &hints, &res0); if (error) errx(1, "%s:%s: %s", addr, port, gai_strerror(error)); for (res = res0; res != NULL; res = res->ai_next) { s = socket(res->ai_family, res->ai_socktype, res->ai_protocol); if (s == -1) { cause = "socket"; continue; } if (bind(s, res->ai_addr, res->ai_addrlen) == -1) { cause = "bind"; serrno = errno; close(s); errno = serrno; continue; } l = calloc(1, sizeof(*l)); if (l == NULL) err(1, "listener alloc"); if (ioctl(s, FIONBIO, &on) == -1) err(1, "ioctl(FIONBIO)"); switch (res->ai_family) { case AF_INET: l->cmsg2dst = proxy_dst4; if (setsockopt(s, IPPROTO_IP, IP_RECVDSTADDR, &on, sizeof(on)) == -1) errx(1, "setsockopt(IP_RECVDSTADDR)"); if (setsockopt(s, IPPROTO_IP, IP_RECVDSTPORT, &on, sizeof(on)) == -1) errx(1, "setsockopt(IP_RECVDSTPORT)"); break; case AF_INET6: l->cmsg2dst = proxy_dst6; if (setsockopt(s, IPPROTO_IPV6, IPV6_RECVPKTINFO, &on, sizeof(on)) == -1) errx(1, "setsockopt(IPV6_RECVPKTINFO)"); break; } l->s = s; TAILQ_INSERT_TAIL(&proxy_listeners, l, entry); } if (TAILQ_EMPTY(&proxy_listeners)) err(1, "%s", cause); } void proxy_listener_events(void) { struct proxy_listener *l; TAILQ_FOREACH(l, &proxy_listeners, entry) { event_set(&l->ev, l->s, EV_READ | EV_PERSIST, proxy_recv, l); event_add(&l->ev, NULL); } } char safety[SEGSIZE_MAX + 4]; int proxy_dst4(struct cmsghdr *cmsg, struct sockaddr_storage *ss) { struct sockaddr_in *sin = (struct sockaddr_in *)ss; if (cmsg->cmsg_level != IPPROTO_IP) return (0); switch (cmsg->cmsg_type) { case IP_RECVDSTADDR: memcpy(&sin->sin_addr, CMSG_DATA(cmsg), sizeof(sin->sin_addr)); if (sin->sin_addr.s_addr == INADDR_BROADCAST) return (-1); break; case IP_RECVDSTPORT: memcpy(&sin->sin_port, CMSG_DATA(cmsg), sizeof(sin->sin_port)); break; } return (0); } int proxy_dst6(struct cmsghdr *cmsg, struct sockaddr_storage *ss) { struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)ss; struct in6_pktinfo *ipi = (struct in6_pktinfo *)CMSG_DATA(cmsg); if (cmsg->cmsg_level != IPPROTO_IPV6) return (0); switch (cmsg->cmsg_type) { case IPV6_PKTINFO: memcpy(&sin6->sin6_addr, &ipi->ipi6_addr, sizeof(sin6->sin6_addr)); #ifdef __KAME__ if (IN6_IS_ADDR_LINKLOCAL(&ipi->ipi6_addr)) sin6->sin6_scope_id = ipi->ipi6_ifindex; #endif break; /* XXX PORT */ } return (0); } void proxy_recv(int fd, short events, void *arg) { struct proxy_listener *l = arg; union { struct cmsghdr hdr; char buf[CMSG_SPACE(sizeof(struct sockaddr_storage)) + CMSG_SPACE(sizeof(in_port_t))]; } cmsgbuf; struct cmsghdr *cmsg; struct msghdr msg; struct iovec iov; ssize_t n; struct proxy_request *r; struct tftphdr *tp; r = calloc(1, sizeof(*r)); if (r == NULL) { recv(fd, safety, sizeof(safety), 0); return; } r->id = arc4random(); /* XXX unique? */ bzero(&msg, sizeof(msg)); iov.iov_base = r->buf; iov.iov_len = sizeof(r->buf); msg.msg_name = &r->addrs.src; msg.msg_namelen = sizeof(r->addrs.src); msg.msg_iov = &iov; msg.msg_iovlen = 1; msg.msg_control = &cmsgbuf.buf; msg.msg_controllen = sizeof(cmsgbuf.buf); n = recvmsg(fd, &msg, 0); if (n == -1) { switch (errno) { case EAGAIN: case EINTR: goto err; default: lerr(1, "recvmsg"); /* NOTREACHED */ } } r->buflen = n; /* check the packet */ if (n < 5) { /* not enough to be a real packet */ goto err; } tp = (struct tftphdr *)r->buf; switch (ntohs(tp->th_opcode)) { case RRQ: case WRQ: break; default: goto err; } r->addrs.dst.ss_family = r->addrs.src.ss_family; r->addrs.dst.ss_len = r->addrs.src.ss_len; /* get local address if possible */ for (cmsg = CMSG_FIRSTHDR(&msg); cmsg != NULL; cmsg = CMSG_NXTHDR(&msg, cmsg)) { if (l->cmsg2dst(cmsg, &r->addrs.dst) == -1) goto err; } if (verbose) { linfo("%s:%d -> %s:%d \"%s %s\"", sock_ntop((struct sockaddr *)&r->addrs.src), ntohs(((struct sockaddr_in *)&r->addrs.src)->sin_port), sock_ntop((struct sockaddr *)&r->addrs.dst), ntohs(((struct sockaddr_in *)&r->addrs.dst)->sin_port), opcode(ntohs(tp->th_opcode)), tp->th_stuff); /* XXX tp->th_stuff could be garbage */ } TAILQ_INSERT_TAIL(&child->fdrequests, r, entry); evbuffer_add(child->buf, &r->addrs, sizeof(r->addrs)); event_add(&child->push_ev, NULL); return; err: free(r); } void unprivproc_push(int fd, short events, void *arg) { if (evbuffer_write(child->buf, fd) == -1) lerr(1, "child evbuffer_write"); if (EVBUFFER_LENGTH(child->buf)) event_add(&child->push_ev, NULL); } void unprivproc_pop(int fd, short events, void *arg) { struct proxy_request *r; struct msghdr msg; union { struct cmsghdr hdr; char buf[CMSG_SPACE(sizeof(int))]; } cmsgbuf; struct cmsghdr *cmsg; struct iovec iov; int result; int s; do { memset(&msg, 0, sizeof(msg)); iov.iov_base = &result; iov.iov_len = sizeof(int); msg.msg_iov = &iov; msg.msg_iovlen = 1; msg.msg_control = &cmsgbuf.buf; msg.msg_controllen = sizeof(cmsgbuf.buf); switch (recvmsg(fd, &msg, 0)) { case sizeof(int): break; case -1: switch (errno) { case EAGAIN: case EINTR: return; default: lerr(1, "child recvmsg"); } /* NOTREACHED */ case 0: lerrx(1, "privproc closed connection"); default: lerrx(1, "child recvmsg was weird"); /* NOTREACHED */ } if (result != 0) { errno = result; lerr(1, "child fdpass fail"); } cmsg = CMSG_FIRSTHDR(&msg); if (cmsg == NULL) lerrx(1, "%s: no message header", __func__); if (cmsg->cmsg_type != SCM_RIGHTS) { lerrx(1, "%s: expected type %d got %d", __func__, SCM_RIGHTS, cmsg->cmsg_type); } s = (*(int *)CMSG_DATA(cmsg)); r = TAILQ_FIRST(&child->fdrequests); if (r == NULL) lerrx(1, "got fd without a pending request"); TAILQ_REMOVE(&child->fdrequests, r, entry); /* get ready to add rules */ if (prepare_commit(r->id) == -1) lerr(1, "%s: prepare_commit", __func__); if (add_filter(r->id, PF_IN, (struct sockaddr *)&r->addrs.dst, (struct sockaddr *)&r->addrs.src, ntohs(((struct sockaddr_in *)&r->addrs.src)->sin_port), IPPROTO_UDP) == -1) lerr(1, "%s: couldn't add pass in", __func__); if (add_filter(r->id, PF_OUT, (struct sockaddr *)&r->addrs.dst, (struct sockaddr *)&r->addrs.src, ntohs(((struct sockaddr_in *)&r->addrs.src)->sin_port), IPPROTO_UDP) == -1) lerr(1, "%s: couldn't add pass out", __func__); if (do_commit() == -1) lerr(1, "%s: couldn't commit rules", __func__); /* forward the initial tftp request and start the insanity */ if (sendto(s, r->buf, r->buflen, 0, (struct sockaddr *)&r->addrs.dst, r->addrs.dst.ss_len) == -1) lerr(1, "%s: unable to send", __func__); close(s); evtimer_set(&r->ev, unprivproc_timeout, r); evtimer_add(&r->ev, &transwait); TAILQ_INSERT_TAIL(&child->tmrequests, r, entry); } while (!TAILQ_EMPTY(&child->fdrequests)); } void unprivproc_timeout(int fd, short events, void *arg) { struct proxy_request *r = arg; TAILQ_REMOVE(&child->tmrequests, r, entry); /* delete our rdr rule and clean up */ prepare_commit(r->id); do_commit(); free(r); } const char * opcode(int code) { static char str[6]; switch (code) { case 1: (void)snprintf(str, sizeof(str), "RRQ"); break; case 2: (void)snprintf(str, sizeof(str), "WRQ"); break; default: (void)snprintf(str, sizeof(str), "(%d)", code); break; } return (str); } const char * sock_ntop(struct sockaddr *sa) { static int n = 0; /* Cycle to next buffer. */ n = (n + 1) % NTOP_BUFS; ntop_buf[n][0] = '\0'; if (sa->sa_family == AF_INET) { struct sockaddr_in *sin = (struct sockaddr_in *)sa; return (inet_ntop(AF_INET, &sin->sin_addr, ntop_buf[n], sizeof ntop_buf[0])); } if (sa->sa_family == AF_INET6) { struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)sa; return (inet_ntop(AF_INET6, &sin6->sin6_addr, ntop_buf[n], sizeof ntop_buf[0])); } return (NULL); } void syslog_vstrerror(int e, int priority, const char *fmt, va_list ap) { char *s; if (vasprintf(&s, fmt, ap) == -1) { syslog(LOG_EMERG, "unable to alloc in syslog_vstrerror"); exit(1); } syslog(priority, "%s: %s", s, strerror(e)); free(s); } void syslog_err(int ecode, const char *fmt, ...) { va_list ap; va_start(ap, fmt); syslog_vstrerror(errno, LOG_EMERG, fmt, ap); va_end(ap); exit(ecode); } void syslog_errx(int ecode, const char *fmt, ...) { va_list ap; va_start(ap, fmt); vsyslog(LOG_WARNING, fmt, ap); va_end(ap); exit(ecode); } void syslog_warn(const char *fmt, ...) { va_list ap; va_start(ap, fmt); syslog_vstrerror(errno, LOG_WARNING, fmt, ap); va_end(ap); } void syslog_warnx(const char *fmt, ...) { va_list ap; va_start(ap, fmt); vsyslog(LOG_WARNING, fmt, ap); va_end(ap); } void syslog_info(const char *fmt, ...) { va_list ap; va_start(ap, fmt); vsyslog(LOG_INFO, fmt, ap); va_end(ap); }