diff options
-rw-r--r-- | sbin/unwind/Makefile | 7 | ||||
-rw-r--r-- | sbin/unwind/captiveportal.c | 720 | ||||
-rw-r--r-- | sbin/unwind/captiveportal.h | 41 | ||||
-rw-r--r-- | sbin/unwind/control.c | 6 | ||||
-rw-r--r-- | sbin/unwind/frontend.c | 118 | ||||
-rw-r--r-- | sbin/unwind/frontend.h | 5 | ||||
-rw-r--r-- | sbin/unwind/printconf.c | 15 | ||||
-rw-r--r-- | sbin/unwind/resolver.c | 354 | ||||
-rw-r--r-- | sbin/unwind/resolver.h | 8 | ||||
-rw-r--r-- | sbin/unwind/unwind.c | 225 | ||||
-rw-r--r-- | sbin/unwind/unwind.conf.5 | 55 | ||||
-rw-r--r-- | sbin/unwind/unwind.h | 32 | ||||
-rw-r--r-- | sbin/unwind/uw_parse.y | 67 | ||||
-rw-r--r-- | usr.sbin/unwindctl/parser.c | 9 | ||||
-rw-r--r-- | usr.sbin/unwindctl/parser.h | 3 | ||||
-rw-r--r-- | usr.sbin/unwindctl/unwindctl.8 | 8 | ||||
-rw-r--r-- | usr.sbin/unwindctl/unwindctl.c | 28 |
17 files changed, 1631 insertions, 70 deletions
diff --git a/sbin/unwind/Makefile b/sbin/unwind/Makefile index 245c528ffbb..563e6a9cc89 100644 --- a/sbin/unwind/Makefile +++ b/sbin/unwind/Makefile @@ -1,7 +1,8 @@ -# $OpenBSD: Makefile,v 1.2 2019/01/24 15:33:44 florian Exp $ +# $OpenBSD: Makefile,v 1.3 2019/02/03 12:02:30 florian Exp $ PROG= unwind SRCS= control.c resolver.c frontend.c uw_log.c unwind.c uw_parse.y printconf.c +SRCS+= captiveportal.c MAN= unwind.8 unwind.conf.5 .include "${.CURDIR}/libunbound/Makefile.inc" @@ -14,7 +15,7 @@ CFLAGS+= -Wmissing-declarations CFLAGS+= -Wshadow -Wpointer-arith CFLAGS+= -Wsign-compare YFLAGS= -LDADD+= -levent -lutil -lssl -lcrypto -DPADD+= ${LIBEVENT} ${LIBUTIL} ${LIBSSL} ${LIBCRYPTO} +LDADD+= -levent -lutil -ltls -lssl -lcrypto +DPADD+= ${LIBEVENT} ${LIBUTIL} ${LIBTLS} ${LIBSSL} ${LIBCRYPTO} .include <bsd.prog.mk> diff --git a/sbin/unwind/captiveportal.c b/sbin/unwind/captiveportal.c new file mode 100644 index 00000000000..c9a9f8f1d34 --- /dev/null +++ b/sbin/unwind/captiveportal.c @@ -0,0 +1,720 @@ +/* $OpenBSD: captiveportal.c,v 1.1 2019/02/03 12:02:30 florian Exp $ */ + +/* + * Copyright (c) 2018 Florian Obser <florian@openbsd.org> + * Copyright (c) 2005 Claudio Jeker <claudio@openbsd.org> + * Copyright (c) 2004 Esben Norby <norby@openbsd.org> + * Copyright (c) 2003, 2004 Henning Brauer <henning@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/socket.h> +#include <sys/syslog.h> +#include <sys/uio.h> + +#include <netinet/in.h> +#include <net/if.h> +#include <net/route.h> + +#include <ctype.h> +#include <errno.h> +#include <event.h> +#include <imsg.h> +#include <limits.h> +#include <netdb.h> +#include <pwd.h> +#include <signal.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> +#include <vis.h> + +#include "uw_log.h" +#include "unwind.h" +#include "captiveportal.h" + +enum http_global_state { + IDLE, + READING +}; + +enum http_state { + INIT, + SENT_QUERY, + HEADER_READ +}; + +struct http_ctx { + TAILQ_ENTRY(http_ctx) entry; + struct event ev; + int fd; + enum http_state state; + char *buf; + size_t bufsz; + int status; + int content_length; +}; + +__dead void captiveportal_shutdown(void); +void captiveportal_sig_handler(int, short, void *); +void captiveportal_startup(void); +void http_callback(int, short, void *); +int parse_http_header(struct http_ctx *); +void check_http_body(struct http_ctx *ctx); +void free_http_ctx(struct http_ctx *); +void close_other_http_contexts(struct http_ctx *); + +struct unwind_conf *captiveportal_conf; +struct imsgev *iev_main; +struct imsgev *iev_resolver; +struct imsgev *iev_frontend; + +#define MAX_SERVERS_DNS 8 +enum http_global_state http_global_state = IDLE; +TAILQ_HEAD(, http_ctx) http_contexts; +int http_contexts_count; + +struct timeval tv = {5, 0}; + +void +captiveportal_sig_handler(int sig, short event, void *bula) +{ + /* + * Normal signal handler rules don't apply because libevent + * decouples for us. + */ + + switch (sig) { + case SIGINT: + case SIGTERM: + captiveportal_shutdown(); + default: + fatalx("unexpected signal"); + } +} + +void +captiveportal(int debug, int verbose) +{ + struct event ev_sigint, ev_sigterm; + struct passwd *pw; + + captiveportal_conf = config_new_empty(); + + log_init(debug, LOG_DAEMON); + log_setverbose(verbose); + + if ((pw = getpwnam(UNWIND_USER)) == NULL) + fatal("getpwnam"); + + if (chroot(pw->pw_dir) == -1) + fatal("chroot"); + if (chdir("/") == -1) + fatal("chdir(\"/\")"); + + unwind_process = PROC_CAPTIVEPORTAL; + setproctitle("%s", log_procnames[unwind_process]); + log_procinit(log_procnames[unwind_process]); + + 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)) + fatal("can't drop privileges"); + + if (pledge("stdio recvfd", NULL) == -1) + fatal("pledge"); + + event_init(); + + /* Setup signal handler. */ + signal_set(&ev_sigint, SIGINT, captiveportal_sig_handler, NULL); + signal_set(&ev_sigterm, SIGTERM, captiveportal_sig_handler, NULL); + signal_add(&ev_sigint, NULL); + signal_add(&ev_sigterm, NULL); + signal(SIGPIPE, SIG_IGN); + signal(SIGHUP, SIG_IGN); + + /* Setup pipe and event handler to the parent process. */ + if ((iev_main = malloc(sizeof(struct imsgev))) == NULL) + fatal(NULL); + imsg_init(&iev_main->ibuf, 3); + iev_main->handler = captiveportal_dispatch_main; + iev_main->events = EV_READ; + event_set(&iev_main->ev, iev_main->ibuf.fd, iev_main->events, + iev_main->handler, iev_main); + event_add(&iev_main->ev, NULL); + + TAILQ_INIT(&http_contexts); + + event_dispatch(); + + captiveportal_shutdown(); +} + +__dead void +captiveportal_shutdown(void) +{ + /* Close pipes. */ + msgbuf_write(&iev_resolver->ibuf.w); + msgbuf_clear(&iev_resolver->ibuf.w); + close(iev_resolver->ibuf.fd); + msgbuf_write(&iev_frontend->ibuf.w); + msgbuf_clear(&iev_frontend->ibuf.w); + close(iev_frontend->ibuf.fd); + msgbuf_write(&iev_main->ibuf.w); + msgbuf_clear(&iev_main->ibuf.w); + close(iev_main->ibuf.fd); + + config_clear(captiveportal_conf); + + free(iev_resolver); + free(iev_frontend); + free(iev_main); + + log_info("captiveportal exiting"); + exit(0); +} + +int +captiveportal_imsg_compose_main(int type, pid_t pid, void *data, uint16_t datalen) +{ + return (imsg_compose_event(iev_main, type, 0, pid, -1, data, + datalen)); +} + +int +captiveportal_imsg_compose_resolver(int type, pid_t pid, void *data, uint16_t datalen) +{ + return (imsg_compose_event(iev_resolver, type, 0, pid, -1, data, + datalen)); +} + +int +captiveportal_imsg_compose_frontend(int type, pid_t pid, void *data, + uint16_t datalen) +{ + return (imsg_compose_event(iev_frontend, type, 0, pid, -1, data, + datalen)); +} + +void +captiveportal_dispatch_main(int fd, short event, void *bula) +{ + static struct unwind_conf *nconf; + struct unwind_forwarder *unwind_forwarder; + struct imsg imsg; + struct imsgev *iev = bula; + struct imsgbuf *ibuf = &iev->ibuf; + struct http_ctx *ctx; + int n, shut = 0; + + if (event & EV_READ) { + if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN) + fatal("imsg_read error"); + if (n == 0) /* Connection closed. */ + shut = 1; + } + if (event & EV_WRITE) { + if ((n = msgbuf_write(&ibuf->w)) == -1 && errno != EAGAIN) + fatal("msgbuf_write"); + if (n == 0) /* Connection closed. */ + shut = 1; + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("%s: imsg_get error", __func__); + if (n == 0) /* No more messages. */ + break; + + switch (imsg.hdr.type) { + case IMSG_SOCKET_IPC_RESOLVER: + /* + * Setup pipe and event handler to the resolver + * process. + */ + if (iev_resolver) { + fatalx("%s: received unexpected imsg fd " + "to captiveportal", __func__); + break; + } + if ((fd = imsg.fd) == -1) { + fatalx("%s: expected to receive imsg fd to " + "captiveportal but didn't receive any", + __func__); + break; + } + + iev_resolver = malloc(sizeof(struct imsgev)); + if (iev_resolver == NULL) + fatal(NULL); + + imsg_init(&iev_resolver->ibuf, fd); + iev_resolver->handler = captiveportal_dispatch_resolver; + iev_resolver->events = EV_READ; + + event_set(&iev_resolver->ev, iev_resolver->ibuf.fd, + iev_resolver->events, iev_resolver->handler, iev_resolver); + event_add(&iev_resolver->ev, NULL); + break; + case IMSG_SOCKET_IPC_FRONTEND: + /* + * Setup pipe and event handler to the frontend + * process. + */ + if (iev_frontend) { + fatalx("%s: received unexpected imsg fd " + "to frontend", __func__); + break; + } + if ((fd = imsg.fd) == -1) { + fatalx("%s: expected to receive imsg fd to " + "frontend but didn't receive any", + __func__); + break; + } + + iev_frontend = malloc(sizeof(struct imsgev)); + if (iev_frontend == NULL) + fatal(NULL); + + imsg_init(&iev_frontend->ibuf, fd); + iev_frontend->handler = captiveportal_dispatch_frontend; + iev_frontend->events = EV_READ; + + event_set(&iev_frontend->ev, iev_frontend->ibuf.fd, + iev_frontend->events, iev_frontend->handler, iev_frontend); + event_add(&iev_frontend->ev, NULL); + break; + case IMSG_RECONF_CONF: + if (imsg.hdr.len != IMSG_HEADER_SIZE + + sizeof(struct unwind_conf)) + fatalx("%s: IMSG_RECONF_CONF wrong length: %d", + __func__, imsg.hdr.len); + if ((nconf = malloc(sizeof(struct unwind_conf))) == + NULL) + fatal(NULL); + memcpy(nconf, imsg.data, sizeof(struct unwind_conf)); + nconf->captive_portal_host = NULL; + nconf->captive_portal_path = NULL; + nconf->captive_portal_expected_response = NULL; + SIMPLEQ_INIT(&nconf->unwind_forwarder_list); + SIMPLEQ_INIT(&nconf->unwind_dot_forwarder_list); + break; + case IMSG_RECONF_CAPTIVE_PORTAL_HOST: + /* make sure this is a string */ + ((char *)imsg.data)[imsg.hdr.len - IMSG_HEADER_SIZE - 1] + = '\0'; + if ((nconf->captive_portal_host = strdup(imsg.data)) == + NULL) + fatal("%s: strdup", __func__); + break; + case IMSG_RECONF_CAPTIVE_PORTAL_PATH: + /* make sure this is a string */ + ((char *)imsg.data)[imsg.hdr.len - IMSG_HEADER_SIZE - 1] + = '\0'; + if ((nconf->captive_portal_path = strdup(imsg.data)) == + NULL) + fatal("%s: strdup", __func__); + break; + case IMSG_RECONF_CAPTIVE_PORTAL_EXPECTED_RESPONSE: + /* make sure this is a string */ + ((char *)imsg.data)[imsg.hdr.len - IMSG_HEADER_SIZE - 1] + = '\0'; + if ((nconf->captive_portal_expected_response = + strdup(imsg.data)) == NULL) + fatal("%s: strdup", __func__); + break; + case IMSG_RECONF_FORWARDER: + if (imsg.hdr.len != IMSG_HEADER_SIZE + + sizeof(struct unwind_forwarder)) + fatalx("%s: IMSG_RECONF_FORWARDER wrong length:" + " %d", __func__, imsg.hdr.len); + if ((unwind_forwarder = malloc(sizeof(struct + unwind_forwarder))) == NULL) + fatal(NULL); + memcpy(unwind_forwarder, imsg.data, sizeof(struct + unwind_forwarder)); + SIMPLEQ_INSERT_TAIL(&nconf->unwind_forwarder_list, + unwind_forwarder, entry); + break; + case IMSG_RECONF_DOT_FORWARDER: + if (imsg.hdr.len != IMSG_HEADER_SIZE + + sizeof(struct unwind_forwarder)) + fatalx("%s: IMSG_RECONF_DOT_FORWARDER wrong " + "length: %d", __func__, imsg.hdr.len); + if ((unwind_forwarder = malloc(sizeof(struct + unwind_forwarder))) == NULL) + fatal(NULL); + memcpy(unwind_forwarder, imsg.data, sizeof(struct + unwind_forwarder)); + SIMPLEQ_INSERT_TAIL(&nconf->unwind_dot_forwarder_list, + unwind_forwarder, entry); + break; + case IMSG_RECONF_END: + merge_config(captiveportal_conf, nconf); + nconf = NULL; + break; + case IMSG_HTTPSOCK: + if ((fd = imsg.fd) == -1) { + fatalx("%s: expected to receive imsg fd to " + "captiveportal but didn't receive any", + __func__); + break; + } + + if (http_global_state == READING || + http_contexts_count >= MAX_SERVERS_DNS) { + /* don't try more servers */ + close(fd); + break; + } + + if ((ctx = malloc(sizeof(*ctx))) == NULL) { + close(fd); + break; + } + + ctx->state = INIT; + ctx->fd = fd; + ctx->bufsz = 0; + ctx->buf = NULL; + ctx->status = -1; + ctx->content_length = -1; + + event_set(&ctx->ev, fd, EV_READ | EV_WRITE | + EV_PERSIST, http_callback, ctx); + event_add(&ctx->ev, &tv); + + TAILQ_INSERT_TAIL(&http_contexts, ctx, entry); + + http_contexts_count++; + + break; + default: + log_debug("%s: error handling imsg %d", __func__, + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + if (!shut) + imsg_event_add(iev); + else { + /* This pipe is dead. Remove its event handler. */ + event_del(&iev->ev); + event_loopexit(NULL); + } +} + +void +captiveportal_dispatch_resolver(int fd, short event, void *bula) +{ + struct imsgev *iev = bula; + struct imsgbuf *ibuf = &iev->ibuf; + struct imsg imsg; + int n, shut = 0; + + if (event & EV_READ) { + if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN) + fatal("imsg_read error"); + if (n == 0) /* Connection closed. */ + shut = 1; + } + if (event & EV_WRITE) { + if ((n = msgbuf_write(&ibuf->w)) == -1 && errno != EAGAIN) + fatal("msgbuf_write"); + if (n == 0) /* Connection closed. */ + shut = 1; + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("%s: imsg_get error", __func__); + if (n == 0) /* No more messages. */ + break; + + switch (imsg.hdr.type) { + default: + log_debug("%s: error handling imsg %d", __func__, + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + if (!shut) + imsg_event_add(iev); + else { + /* This pipe is dead. Remove its event handler. */ + event_del(&iev->ev); + event_loopexit(NULL); + } +} + +void +captiveportal_dispatch_frontend(int fd, short event, void *bula) +{ + struct imsgev *iev = bula; + struct imsgbuf *ibuf = &iev->ibuf; + struct imsg imsg; + int n, shut = 0; + + if (event & EV_READ) { + if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN) + fatal("imsg_read error"); + if (n == 0) /* Connection closed. */ + shut = 1; + } + if (event & EV_WRITE) { + if ((n = msgbuf_write(&ibuf->w)) == -1 && errno != EAGAIN) + fatal("msgbuf_write"); + if (n == 0) /* Connection closed. */ + shut = 1; + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("%s: imsg_get error", __func__); + if (n == 0) /* No more messages. */ + break; + + switch (imsg.hdr.type) { + default: + log_debug("%s: error handling imsg %d", __func__, + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + if (!shut) + imsg_event_add(iev); + else { + /* This pipe is dead. Remove its event handler. */ + event_del(&iev->ev); + event_loopexit(NULL); + } +} + +void +http_callback(int fd, short events, void *arg) +{ + struct http_ctx *ctx; + ssize_t n; + char *query, buf[512], *vis_str, *p, *ep; + + ctx = (struct http_ctx *)arg; + + if (events & EV_TIMEOUT) { + log_debug("%s: TIMEOUT", __func__); + goto err; + } + + if (events & EV_READ) { + if ((n = read(fd, buf, sizeof(buf))) == -1) { + if (errno == EAGAIN || errno == EINTR) + return; + else { + log_warn("%s: read", __func__); + if (http_global_state == READING) + http_global_state = IDLE; + goto err; + } + } + + if (http_contexts_count > 1) + close_other_http_contexts(ctx); + http_global_state = READING; + + if (n == 0) { + check_http_body(ctx); + return; + } + p = recallocarray(ctx->buf, ctx->bufsz, ctx->bufsz + n, 1); + if (p == NULL) { + log_warn("%s", __func__); + goto err; + } + ctx->buf = p; + memcpy(ctx->buf + ctx->bufsz, buf, n); + ctx->bufsz += n; + + if (ctx->state == HEADER_READ && ctx->content_length != -1 && + ctx->bufsz >= (size_t)ctx->content_length) { + check_http_body(ctx); + return; + } + + if (ctx->state == SENT_QUERY) { + ep = memmem(ctx->buf, ctx->bufsz, "\r\n\r\n", 4); + if (ep != NULL) { + ctx->state = HEADER_READ; + *ep = '\0'; + if (strlen(ctx->buf) != (uintptr_t) + (ep - ctx->buf)) { + log_warnx("binary data in header"); + goto err; + } + stravis(&vis_str, ctx->buf, + VIS_NL | VIS_CSTYLE); + log_debug("header\n%s", vis_str); + free(vis_str); + + if (parse_http_header(ctx) != 0) + goto err; + + p = ctx->buf; + ep += 4; + ctx->bufsz = (ctx->buf + ctx->bufsz) - ep; + ctx->buf = malloc(ctx->bufsz); + memcpy(ctx->buf, ep, ctx->bufsz); + free(p); + } + } + } + + if (events & EV_WRITE) { + if (ctx->state == INIT) { + n = asprintf(&query, + "GET %s HTTP/1.1\r\nHost: %s\r\n" + "Connection: close\r\n\r\n", + captiveportal_conf->captive_portal_path, + captiveportal_conf->captive_portal_host); + write(fd, query, n); + free(query); + event_del(&ctx->ev); + event_set(&ctx->ev, fd, EV_READ | EV_PERSIST, + http_callback, ctx); + event_add(&ctx->ev, &tv); + ctx->state = SENT_QUERY; + } else { + log_warnx("invalid state: %d", ctx->state); + goto err; + } + } + return; +err: + free_http_ctx(ctx); +} + +int +parse_http_header(struct http_ctx *ctx) +{ + char *p, *ep; + const char *errstr; + + /* scan past HTTP/1.x */ + p = strchr(ctx->buf, ' '); + if (p == NULL) + return (1); + while (isspace((int)*p)) + p++; + ep = strchr(p, ' '); + if (ep == NULL) + return (1); + *ep = '\0'; + ctx->status = strtonum(p, 100, 599, &errstr); + if (errstr != NULL) { + log_warnx("%s: status is %s: %s", __func__, errstr, p); + return (1); + } + + log_debug("%s: status: %d", __func__, ctx->status); + + /* ignore parse errors from here on out, we got the status */ + + p = strstr(ep + 1, "Content-Length:"); + if (p == NULL) + return (0); + + p += sizeof("Content-Length:") - 1; + while (isspace((int)*p)) + p++; + + ep = strchr(p, '\r'); + if (ep == NULL) + return (0); + + *ep = '\0'; + ctx->content_length = strtonum(p, 0, INT_MAX, &errstr); + if (errstr != NULL) { + log_warnx("%s: Content-Lenght is %s: %s", __func__, errstr, p); + ctx->content_length = -1; + return (0); + } + log_debug("content-length: %d", ctx->content_length); + return (0); +} + +void +check_http_body(struct http_ctx *ctx) +{ + enum captive_portal_state state; + char *p, *vis_str; + + p = recallocarray(ctx->buf, ctx->bufsz, ctx->bufsz + 1, 1); + if (p == NULL) { + log_warn("%s", __func__); + free_http_ctx(ctx); + return; + } + ctx->buf = p; + *(ctx->buf + ctx->bufsz) = '\0'; + ctx->bufsz++; + stravis(&vis_str, ctx->buf, VIS_NL | VIS_CSTYLE); + log_debug("body[%ld]\n%s", ctx->bufsz, vis_str); + + if (ctx->status == captiveportal_conf->captive_portal_expected_status && + strcmp(vis_str, + captiveportal_conf->captive_portal_expected_response) == 0) { + log_debug("%s: not behind captive portal", __func__); + state = NOT_BEHIND; + } else { + log_debug("%s: behind captive portal", __func__); + state = BEHIND; + } + captiveportal_imsg_compose_resolver(IMSG_CAPTIVEPORTAL_STATE, 0, + &state, sizeof(state)); + free_http_ctx(ctx); + http_global_state = IDLE; +} + +void +free_http_ctx(struct http_ctx *ctx) +{ + if (ctx == NULL) + return; + + event_del(&ctx->ev); + close(ctx->fd); + TAILQ_REMOVE(&http_contexts, ctx, entry); + free(ctx->buf); + free(ctx); + http_contexts_count--; +} + +void +close_other_http_contexts(struct http_ctx *octx) +{ + struct http_ctx *ctx, *t; + + log_debug("%s", __func__); + TAILQ_FOREACH_SAFE(ctx, &http_contexts, entry, t) + if(ctx != octx) + free_http_ctx(ctx); +} diff --git a/sbin/unwind/captiveportal.h b/sbin/unwind/captiveportal.h new file mode 100644 index 00000000000..cee72005e5c --- /dev/null +++ b/sbin/unwind/captiveportal.h @@ -0,0 +1,41 @@ +/* $OpenBSD: captiveportal.h,v 1.1 2019/02/03 12:02:30 florian Exp $ */ + +/* + * Copyright (c) 2018 Florian Obser <florian@openbsd.org> + * Copyright (c) 2004, 2005 Esben Norby <norby@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + + +enum captive_portal_state { + PORTAL_UNCHECKED, + PORTAL_UNKNOWN, + BEHIND, + NOT_BEHIND +}; + +static const char * const captive_portal_state_str[] = { + "unchecked", + "unknown", + "behind", + "not behind" +}; + +void captiveportal(int, int); +void captiveportal_dispatch_main(int, short, void *); +void captiveportal_dispatch_resolver(int, short, void *); +void captiveportal_dispatch_frontend(int, short, void *); +int captiveportal_imsg_compose_main(int, pid_t, void *, uint16_t); +int captiveportal_imsg_compose_resolver(int, pid_t, void *, uint16_t); +int captiveportal_imsg_compose_frontend(int, pid_t, void *, uint16_t); diff --git a/sbin/unwind/control.c b/sbin/unwind/control.c index b442be42fec..0c4b8fc412b 100644 --- a/sbin/unwind/control.c +++ b/sbin/unwind/control.c @@ -1,4 +1,4 @@ -/* $OpenBSD: control.c,v 1.5 2019/01/31 18:06:14 solene Exp $ */ +/* $OpenBSD: control.c,v 1.6 2019/02/03 12:02:30 florian Exp $ */ /* * Copyright (c) 2003, 2004 Henning Brauer <henning@openbsd.org> @@ -256,6 +256,10 @@ control_dispatch_imsg(int fd, short event, void *bula) case IMSG_CTL_RELOAD: frontend_imsg_compose_main(imsg.hdr.type, 0, NULL, 0); break; + case IMSG_CTL_RECHECK_CAPTIVEPORTAL: + frontend_imsg_compose_resolver(imsg.hdr.type, + imsg.hdr.pid, NULL, 0); + break; case IMSG_CTL_LOG_VERBOSE: if (imsg.hdr.len != IMSG_HEADER_SIZE + sizeof(verbose)) diff --git a/sbin/unwind/frontend.c b/sbin/unwind/frontend.c index ab8dfececa8..b7e4381c99e 100644 --- a/sbin/unwind/frontend.c +++ b/sbin/unwind/frontend.c @@ -1,4 +1,4 @@ -/* $OpenBSD: frontend.c,v 1.9 2019/02/01 13:05:55 florian Exp $ */ +/* $OpenBSD: frontend.c,v 1.10 2019/02/03 12:02:30 florian Exp $ */ /* * Copyright (c) 2018 Florian Obser <florian@openbsd.org> @@ -86,13 +86,13 @@ void handle_route_message(struct rt_msghdr *, void get_rtaddrs(int, struct sockaddr *, struct sockaddr **); void rtmget_default(void); -char *ip_port(struct sockaddr *); struct pending_query *find_pending_query(uint64_t); void parse_dhcp_lease(int); struct unwind_conf *frontend_conf; struct imsgev *iev_main; struct imsgev *iev_resolver; +struct imsgev *iev_captiveportal; struct event ev_route; int udp4sock = -1, udp6sock = -1, routesock = -1; @@ -206,6 +206,9 @@ frontend_shutdown(void) msgbuf_write(&iev_resolver->ibuf.w); msgbuf_clear(&iev_resolver->ibuf.w); close(iev_resolver->ibuf.fd); + msgbuf_write(&iev_captiveportal->ibuf.w); + msgbuf_clear(&iev_captiveportal->ibuf.w); + close(iev_captiveportal->ibuf.fd); msgbuf_write(&iev_main->ibuf.w); msgbuf_clear(&iev_main->ibuf.w); close(iev_main->ibuf.fd); @@ -213,6 +216,7 @@ frontend_shutdown(void) config_clear(frontend_conf); free(iev_resolver); + free(iev_captiveportal); free(iev_main); log_info("frontend exiting"); @@ -233,6 +237,13 @@ frontend_imsg_compose_resolver(int type, pid_t pid, void *data, uint16_t datalen datalen)); } +int +frontend_imsg_compose_captiveportal(int type, pid_t pid, void *data, uint16_t datalen) +{ + return (imsg_compose_event(iev_captiveportal, type, 0, pid, -1, data, + datalen)); +} + void frontend_dispatch_main(int fd, short event, void *bula) { @@ -263,7 +274,7 @@ frontend_dispatch_main(int fd, short event, void *bula) break; switch (imsg.hdr.type) { - case IMSG_SOCKET_IPC: + case IMSG_SOCKET_IPC_RESOLVER: /* * Setup pipe and event handler to the resolver * process. @@ -292,6 +303,35 @@ frontend_dispatch_main(int fd, short event, void *bula) iev_resolver->events, iev_resolver->handler, iev_resolver); event_add(&iev_resolver->ev, NULL); break; + case IMSG_SOCKET_IPC_CAPTIVEPORTAL: + /* + * Setup pipe and event handler to the captiveportal + * process. + */ + if (iev_captiveportal) { + fatalx("%s: received unexpected imsg fd " + "to frontend", __func__); + break; + } + if ((fd = imsg.fd) == -1) { + fatalx("%s: expected to receive imsg fd to " + "frontend but didn't receive any", + __func__); + break; + } + + iev_captiveportal = malloc(sizeof(struct imsgev)); + if (iev_captiveportal == NULL) + fatal(NULL); + + imsg_init(&iev_captiveportal->ibuf, fd); + iev_captiveportal->handler = frontend_dispatch_captiveportal; + iev_captiveportal->events = EV_READ; + + event_set(&iev_captiveportal->ev, iev_captiveportal->ibuf.fd, + iev_captiveportal->events, iev_captiveportal->handler, iev_captiveportal); + event_add(&iev_captiveportal->ev, NULL); + break; case IMSG_RECONF_CONF: if (imsg.hdr.len != IMSG_HEADER_SIZE + sizeof(struct unwind_conf)) @@ -301,9 +341,36 @@ frontend_dispatch_main(int fd, short event, void *bula) NULL) fatal(NULL); memcpy(nconf, imsg.data, sizeof(struct unwind_conf)); + nconf->captive_portal_host = NULL; + nconf->captive_portal_path = NULL; + nconf->captive_portal_expected_response = NULL; SIMPLEQ_INIT(&nconf->unwind_forwarder_list); SIMPLEQ_INIT(&nconf->unwind_dot_forwarder_list); break; + case IMSG_RECONF_CAPTIVE_PORTAL_HOST: + /* make sure this is a string */ + ((char *)imsg.data)[imsg.hdr.len - IMSG_HEADER_SIZE - 1] + = '\0'; + if ((nconf->captive_portal_host = strdup(imsg.data)) == + NULL) + fatal("%s: strdup", __func__); + break; + case IMSG_RECONF_CAPTIVE_PORTAL_PATH: + /* make sure this is a string */ + ((char *)imsg.data)[imsg.hdr.len - IMSG_HEADER_SIZE - 1] + = '\0'; + if ((nconf->captive_portal_path = strdup(imsg.data)) == + NULL) + fatal("%s: strdup", __func__); + break; + case IMSG_RECONF_CAPTIVE_PORTAL_EXPECTED_RESPONSE: + /* make sure this is a string */ + ((char *)imsg.data)[imsg.hdr.len - IMSG_HEADER_SIZE - 1] + = '\0'; + if ((nconf->captive_portal_expected_response = + strdup(imsg.data)) == NULL) + fatal("%s: strdup", __func__); + break; case IMSG_RECONF_FORWARDER: if (imsg.hdr.len != IMSG_HEADER_SIZE + sizeof(struct unwind_forwarder)) @@ -482,6 +549,7 @@ frontend_dispatch_resolver(int fd, short event, void *bula) frontend_imsg_compose_main(IMSG_OPEN_PORTS, 0, NULL, 0); break; case IMSG_CTL_RESOLVER_INFO: + case IMSG_CTL_CAPTIVEPORTAL_INFO: case IMSG_CTL_RESOLVER_WHY_BOGUS: case IMSG_CTL_RESOLVER_HISTOGRAM: case IMSG_CTL_END: @@ -504,6 +572,50 @@ frontend_dispatch_resolver(int fd, short event, void *bula) } void +frontend_dispatch_captiveportal(int fd, short event, void *bula) +{ + struct imsgev *iev = bula; + struct imsgbuf *ibuf = &iev->ibuf; + struct imsg imsg; + int n, shut = 0; + + if (event & EV_READ) { + if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN) + fatal("imsg_read error"); + if (n == 0) /* Connection closed. */ + shut = 1; + } + if (event & EV_WRITE) { + if ((n = msgbuf_write(&ibuf->w)) == -1 && errno != EAGAIN) + fatal("msgbuf_write"); + if (n == 0) /* Connection closed. */ + shut = 1; + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("%s: imsg_get error", __func__); + if (n == 0) /* No more messages. */ + break; + + switch (imsg.hdr.type) { + default: + log_debug("%s: error handling imsg %d", __func__, + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + if (!shut) + imsg_event_add(iev); + else { + /* This pipe is dead. Remove its event handler. */ + event_del(&iev->ev); + event_loopexit(NULL); + } +} + +void frontend_startup(void) { if (!event_initialized(&ev_route)) diff --git a/sbin/unwind/frontend.h b/sbin/unwind/frontend.h index 052c9e72cfc..102df500cf3 100644 --- a/sbin/unwind/frontend.h +++ b/sbin/unwind/frontend.h @@ -1,4 +1,4 @@ -/* $OpenBSD: frontend.h,v 1.1 2019/01/23 13:11:00 florian Exp $ */ +/* $OpenBSD: frontend.h,v 1.2 2019/02/03 12:02:30 florian Exp $ */ /* * Copyright (c) 2018 Florian Obser <florian@openbsd.org> @@ -22,5 +22,8 @@ TAILQ_HEAD(ctl_conns, ctl_conn) ctl_conns; void frontend(int, int); void frontend_dispatch_main(int, short, void *); void frontend_dispatch_resolver(int, short, void *); +void frontend_dispatch_captiveportal(int, short, void *); int frontend_imsg_compose_main(int, pid_t, void *, uint16_t); int frontend_imsg_compose_resolver(int, pid_t, void *, uint16_t); +int frontend_imsg_compose_captiveportal(int, pid_t, void *, uint16_t); +char *ip_port(struct sockaddr *); diff --git a/sbin/unwind/printconf.c b/sbin/unwind/printconf.c index 6f8fe1bddcd..5f544fd8e4f 100644 --- a/sbin/unwind/printconf.c +++ b/sbin/unwind/printconf.c @@ -1,4 +1,4 @@ -/* $OpenBSD: printconf.c,v 1.4 2019/01/29 19:32:36 florian Exp $ */ +/* $OpenBSD: printconf.c,v 1.5 2019/02/03 12:02:30 florian Exp $ */ /* * Copyright (c) 2018 Florian Obser <florian@openbsd.org> @@ -77,4 +77,17 @@ print_config(struct unwind_conf *conf) } printf("}\n"); } + + if (conf->captive_portal_host != NULL) { + printf("captive portal {\n"); + printf("\turl \"http://%s%s\"\n", + conf->captive_portal_host, conf->captive_portal_path); + printf("\texpected status %d\n", + conf->captive_portal_expected_status); + if (conf->captive_portal_expected_response != NULL) + printf("\texpected response \"%s\"\n", + conf->captive_portal_expected_response); + printf("\tauto %s\n", yesno(conf->captive_portal_auto)); + printf("}\n"); + } } diff --git a/sbin/unwind/resolver.c b/sbin/unwind/resolver.c index 3e18936b1b0..6ce00e41afe 100644 --- a/sbin/unwind/resolver.c +++ b/sbin/unwind/resolver.c @@ -1,4 +1,4 @@ -/* $OpenBSD: resolver.c,v 1.13 2019/01/29 19:13:01 florian Exp $ */ +/* $OpenBSD: resolver.c,v 1.14 2019/02/03 12:02:30 florian Exp $ */ /* * Copyright (c) 2018 Florian Obser <florian@openbsd.org> @@ -39,15 +39,20 @@ #include <unistd.h> #include "libunbound/config.h" +#include "libunbound/libunbound/libworker.h" #include "libunbound/libunbound/unbound.h" #include "libunbound/libunbound/unbound-event.h" +#include "libunbound/sldns/sbuffer.h" #include "libunbound/sldns/rrdef.h" #include "libunbound/sldns/pkthdr.h" #include "libunbound/sldns/wire2str.h" +#include "libunbound/util/regional.h" #include <openssl/crypto.h> +#include "captiveportal.h" #include "uw_log.h" +#include "frontend.h" #include "unwind.h" #include "resolver.h" @@ -78,6 +83,7 @@ struct check_resolver_data { __dead void resolver_shutdown(void); void resolver_sig_handler(int sig, short, void *); void resolver_dispatch_frontend(int, short, void *); +void resolver_dispatch_captiveportal(int, short, void *); void resolver_dispatch_main(int, short, void *); void resolve_done(void *, int, void *, int, int, char *, int); @@ -113,7 +119,12 @@ void send_detailed_resolver_info(struct unwind_resolver *, pid_t); void send_resolver_histogram_info(struct unwind_resolver *, pid_t pid); - +void check_captive_portal(int); +void check_captive_portal_timo(int, short, void *); +void check_captive_portal_resolve_done(void *, int, void *, + int, int, char *, int); +int check_captive_portal_changed(struct unwind_conf *, + struct unwind_conf *); /* for openssl */ void init_locks(void); unsigned long id_callback(void); @@ -121,18 +132,23 @@ void lock_callback(int, int, const char *, int); struct unwind_conf *resolver_conf; struct imsgev *iev_frontend; +struct imsgev *iev_captiveportal; struct imsgev *iev_main; struct unwind_forwarder_head dhcp_forwarder_list; struct unwind_resolver *recursor, *forwarder, *static_forwarder; struct unwind_resolver *static_dot_forwarder; struct timeval resolver_check_pause = { 30, 0}; - +#define PORTAL_CHECK_SEC 15 +#define PORTAL_CHECK_MAXSEC 600 +struct timeval captive_portal_check_tv = {PORTAL_CHECK_SEC, 0}; +struct event captive_portal_check_ev; struct event_base *ev_base; /* for openssl */ pthread_mutex_t *locks; enum unwind_resolver_state global_state = DEAD; +enum captive_portal_state captive_portal_state = PORTAL_UNCHECKED; void resolver_sig_handler(int sig, short event, void *arg) @@ -207,6 +223,8 @@ resolver(int debug, int verbose) iev_main->handler, iev_main); event_add(&iev_main->ev, NULL); + evtimer_set(&captive_portal_check_ev, check_captive_portal_timo, NULL); + init_locks(); CRYPTO_set_id_callback(id_callback); CRYPTO_set_locking_callback(lock_callback); @@ -224,29 +242,19 @@ __dead void resolver_shutdown(void) { log_debug("%s", __func__); - /* XXX we might have many more ctx lying aroung */ - if (recursor != NULL) { - recursor->ref_cnt = 0; /* not coming back to deref ctx*/ - free_resolver(recursor); - } - if (forwarder != NULL) { - forwarder->ref_cnt = 0; /* not coming back to deref ctx */ - free_resolver(forwarder); - } - if (static_forwarder != NULL) { - static_forwarder->ref_cnt = 0; - free_resolver(static_forwarder); - } /* Close pipes. */ msgbuf_clear(&iev_frontend->ibuf.w); close(iev_frontend->ibuf.fd); + msgbuf_clear(&iev_captiveportal->ibuf.w); + close(iev_captiveportal->ibuf.fd); msgbuf_clear(&iev_main->ibuf.w); close(iev_main->ibuf.fd); config_clear(resolver_conf); free(iev_frontend); + free(iev_captiveportal); free(iev_main); log_info("resolver exiting"); @@ -254,12 +262,27 @@ resolver_shutdown(void) } int +resolver_imsg_compose_main(int type, pid_t pid, void *data, uint16_t datalen) +{ + return (imsg_compose_event(iev_main, type, 0, pid, -1, data, + datalen)); +} + +int resolver_imsg_compose_frontend(int type, pid_t pid, void *data, uint16_t datalen) { return (imsg_compose_event(iev_frontend, type, 0, pid, -1, data, datalen)); } +int +resolver_imsg_compose_captiveportal(int type, pid_t pid, void *data, + uint16_t datalen) +{ + return (imsg_compose_event(iev_captiveportal, type, 0, pid, -1, + data, datalen)); +} + void resolver_dispatch_frontend(int fd, short event, void *bula) { @@ -336,7 +359,7 @@ resolver_dispatch_frontend(int fd, short event, void *bula) if ((err = ub_resolve_event(res->ctx, query_imsg->qname, query_imsg->t, query_imsg->c, - (void *)query_imsg, resolve_done, + query_imsg, resolve_done, &query_imsg->async_id)) != 0) { log_warn("%s: ub_resolve_async: err: %d, %s", __func__, err, ub_strerror(err)); @@ -356,6 +379,71 @@ resolver_dispatch_frontend(int fd, short event, void *bula) memcpy(&type, imsg.data, sizeof(type)); show_status(type, imsg.hdr.pid); break; + case IMSG_CTL_RECHECK_CAPTIVEPORTAL: + check_captive_portal(1); + break; + default: + log_debug("%s: unexpected imsg %d", __func__, + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + if (!shut) + imsg_event_add(iev); + else { + /* This pipe is dead. Remove its event handler. */ + event_del(&iev->ev); + event_loopexit(NULL); + } +} + +void +resolver_dispatch_captiveportal(int fd, short event, void *bula) +{ + struct imsgev *iev = bula; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + int shut = 0; + + + ibuf = &iev->ibuf; + + if (event & EV_READ) { + if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN) + fatal("imsg_read error"); + if (n == 0) /* Connection closed. */ + shut = 1; + } + if (event & EV_WRITE) { + if ((n = msgbuf_write(&ibuf->w)) == -1 && errno != EAGAIN) + fatal("msgbuf_write"); + if (n == 0) /* Connection closed. */ + shut = 1; + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("%s: imsg_get error", __func__); + if (n == 0) /* No more messages. */ + break; + + switch (imsg.hdr.type) { + case IMSG_CAPTIVEPORTAL_STATE: + if (imsg.hdr.len != IMSG_HEADER_SIZE + + sizeof(captive_portal_state)) + fatalx("%s: IMSG_CAPTIVEPORTAL_STATE wrong " + "length: %d", __func__, imsg.hdr.len); + memcpy(&captive_portal_state, imsg.data, + sizeof(captive_portal_state)); + log_debug("%s: IMSG_CAPTIVEPORTAL_STATE: %s", __func__, + captive_portal_state_str[captive_portal_state]); + + if (captive_portal_state == NOT_BEHIND) + evtimer_del(&captive_portal_check_ev); + + break; default: log_debug("%s: unexpected imsg %d", __func__, imsg.hdr.type); @@ -383,6 +471,7 @@ resolver_dispatch_main(int fd, short event, void *bula) ssize_t n; int shut = 0, forwarders_changed; int dot_forwarders_changed; + int captive_portal_changed; ibuf = &iev->ibuf; @@ -406,7 +495,7 @@ resolver_dispatch_main(int fd, short event, void *bula) break; switch (imsg.hdr.type) { - case IMSG_SOCKET_IPC: + case IMSG_SOCKET_IPC_FRONTEND: /* * Setup pipe and event handler to the frontend * process. @@ -432,6 +521,34 @@ resolver_dispatch_main(int fd, short event, void *bula) iev_frontend); event_add(&iev_frontend->ev, NULL); break; + case IMSG_SOCKET_IPC_CAPTIVEPORTAL: + /* + * Setup pipe and event handler to the captiveportal + * process. + */ + if (iev_captiveportal) + fatalx("%s: received unexpected imsg fd " + "to resolver", __func__); + + if ((fd = imsg.fd) == -1) + fatalx("%s: expected to receive imsg fd to " + "resolver but didn't receive any", __func__); + + iev_captiveportal = malloc(sizeof(struct imsgev)); + if (iev_captiveportal == NULL) + fatal(NULL); + + imsg_init(&iev_captiveportal->ibuf, fd); + iev_captiveportal->handler = + resolver_dispatch_captiveportal; + iev_captiveportal->events = EV_READ; + + event_set(&iev_captiveportal->ev, + iev_captiveportal->ibuf.fd, + iev_captiveportal->events, iev_captiveportal->handler, + iev_captiveportal); + event_add(&iev_captiveportal->ev, NULL); + break; case IMSG_RECONF_CONF: if (imsg.hdr.len != IMSG_HEADER_SIZE + sizeof(struct unwind_conf)) @@ -440,9 +557,36 @@ resolver_dispatch_main(int fd, short event, void *bula) if ((nconf = malloc(sizeof(struct unwind_conf))) == NULL) fatal(NULL); memcpy(nconf, imsg.data, sizeof(struct unwind_conf)); + nconf->captive_portal_host = NULL; + nconf->captive_portal_path = NULL; + nconf->captive_portal_expected_response = NULL; SIMPLEQ_INIT(&nconf->unwind_forwarder_list); SIMPLEQ_INIT(&nconf->unwind_dot_forwarder_list); break; + case IMSG_RECONF_CAPTIVE_PORTAL_HOST: + /* make sure this is a string */ + ((char *)imsg.data)[imsg.hdr.len - IMSG_HEADER_SIZE - 1] + = '\0'; + if ((nconf->captive_portal_host = strdup(imsg.data)) == + NULL) + fatal("%s: strdup", __func__); + break; + case IMSG_RECONF_CAPTIVE_PORTAL_PATH: + /* make sure this is a string */ + ((char *)imsg.data)[imsg.hdr.len - IMSG_HEADER_SIZE - 1] + = '\0'; + if ((nconf->captive_portal_path = strdup(imsg.data)) == + NULL) + fatal("%s: strdup", __func__); + break; + case IMSG_RECONF_CAPTIVE_PORTAL_EXPECTED_RESPONSE: + /* make sure this is a string */ + ((char *)imsg.data)[imsg.hdr.len - IMSG_HEADER_SIZE - 1] + = '\0'; + if ((nconf->captive_portal_expected_response = + strdup(imsg.data)) == NULL) + fatal("%s: strdup", __func__); + break; case IMSG_RECONF_FORWARDER: if (imsg.hdr.len != IMSG_HEADER_SIZE + sizeof(struct unwind_forwarder)) @@ -476,6 +620,8 @@ resolver_dispatch_main(int fd, short event, void *bula) dot_forwarders_changed = check_forwarders_changed( &resolver_conf->unwind_dot_forwarder_list, &nconf->unwind_dot_forwarder_list); + captive_portal_changed = check_captive_portal_changed( + resolver_conf, nconf); merge_config(resolver_conf, nconf); nconf = NULL; if (forwarders_changed) { @@ -486,6 +632,13 @@ resolver_dispatch_main(int fd, short event, void *bula) log_debug("static DoT forwarders changed"); new_static_dot_forwarders(); } + if (resolver_conf->captive_portal_host == NULL) + captive_portal_state = PORTAL_UNCHECKED; + else if (captive_portal_state == PORTAL_UNCHECKED || + captive_portal_changed) { + if (resolver_conf->captive_portal_auto) + check_captive_portal(1); + } break; default: log_debug("%s: unexpected imsg %d", __func__, @@ -607,6 +760,8 @@ parse_dhcp_forwarders(char *resolvers) &dhcp_forwarder_list)) { replace_forwarders(&new_forwarder_list, &dhcp_forwarder_list); new_forwarders(); + if (resolver_conf->captive_portal_auto) + check_captive_portal(1); } else log_debug("%s: forwarders didn't change", __func__); } @@ -1012,6 +1167,14 @@ best_resolver(void) unwind_resolver_type_str[forwarder->type], unwind_resolver_state_str[forwarder->state]); + log_debug("%s: %s captive portal", __func__, captive_portal_state_str[ + captive_portal_state]); + + if (captive_portal_state == UNKNOWN || captive_portal_state == BEHIND) { + if (forwarder != NULL) + return (forwarder); + } + res = recursor; if (resolver_cmp(res, static_dot_forwarder) < 0) @@ -1065,6 +1228,8 @@ show_status(enum unwind_resolver_type type, pid_t pid) switch(type) { case RESOLVER_NONE: + resolver_imsg_compose_frontend(IMSG_CTL_CAPTIVEPORTAL_INFO, + pid, &captive_portal_state, sizeof(captive_portal_state)); send_resolver_info(recursor, recursor == best, pid); send_resolver_info(forwarder, forwarder == best, pid); send_resolver_info(static_forwarder, static_forwarder == best, @@ -1134,3 +1299,156 @@ send_resolver_histogram_info(struct unwind_resolver *res, pid_t pid) resolver_imsg_compose_frontend(IMSG_CTL_RESOLVER_HISTOGRAM, pid, histogram, sizeof(histogram)); } + +void +check_captive_portal_timo(int fd, short events, void *arg) +{ + captive_portal_check_tv.tv_sec *= 2; + if (captive_portal_check_tv.tv_sec > PORTAL_CHECK_MAXSEC) + captive_portal_check_tv.tv_sec = PORTAL_CHECK_MAXSEC; + check_captive_portal(0); +} + +void +check_captive_portal(int timer_reset) +{ + struct unwind_resolver *res; + int err; + + log_debug("%s", __func__); + + if (resolver_conf->captive_portal_host == NULL) { + log_debug("%s: no captive portal url configured", __func__); + return; + } + + if (forwarder == NULL) { + log_debug("%s no DHCP nameservers known", __func__); + return; + } + + if (timer_reset) + captive_portal_check_tv.tv_sec = PORTAL_CHECK_SEC; + + evtimer_add(&captive_portal_check_ev, &captive_portal_check_tv); + + + captive_portal_state = PORTAL_UNKNOWN; + + res = forwarder; + + resolver_ref(res); + + if ((err = ub_resolve_event(res->ctx, + resolver_conf->captive_portal_host, LDNS_RR_TYPE_A, + LDNS_RR_CLASS_IN, res, check_captive_portal_resolve_done, + NULL)) != 0) + log_warn("%s: ub_resolve_async: err: %d, %s", + __func__, err, ub_strerror(err)); + +} + +void +check_captive_portal_resolve_done(void *arg, int rcode, void *answer_packet, + int answer_len, int sec, char *why_bogus, int was_ratelimited) +{ + struct unwind_resolver *res; + struct ub_result *result; + sldns_buffer *buf; + struct regional *region; + struct sockaddr_in sin; + int i; + char *str, rdata_buf[sizeof("XXX.XXX.XXX.XXX")]; + + res = (struct unwind_resolver *)arg; + + if ((result = calloc(1, sizeof(*result))) == NULL) + goto out; + + memset(&sin, 0, sizeof(sin)); + sin.sin_len = sizeof(sin); + sin.sin_family = AF_INET; + sin.sin_port = htons(80); + + log_debug("%s: %d", __func__, rcode); + + if ((str = sldns_wire2str_pkt(answer_packet, answer_len)) != NULL) { + log_debug("%s", str); + free(str); + } + + buf = sldns_buffer_new(answer_len); + region = regional_create(); + result->rcode = LDNS_RCODE_SERVFAIL; + if(region && buf) { + sldns_buffer_clear(buf); + sldns_buffer_write(buf, answer_packet, answer_len); + sldns_buffer_flip(buf); + libworker_enter_result(result, buf, region, sec); + result->answer_packet = NULL; //answer_packet; + result->answer_len = 0; //answer_len; + sldns_buffer_free(buf); + regional_destroy(region); + + i = 0; + while(result->data[i] != NULL) { + sldns_wire2str_rdata_buf(result->data[i], + result->len[i], rdata_buf, sizeof(rdata_buf), + LDNS_RR_TYPE_A); + log_debug("%s: result[%d] = %d: %s", __func__, i, + result->len[i], rdata_buf); + + memcpy(&sin.sin_addr.s_addr, result->data[i], + sizeof(sin.sin_addr.s_addr)); + log_debug("%s: ip_port: %s", __func__, + ip_port((struct sockaddr *)&sin)); + + resolver_imsg_compose_main(IMSG_OPEN_HTTP_PORT, 0, + &sin, sizeof(sin)); + i++; + } + } +out: + ub_resolve_free(result); + resolver_unref(res); +} + +int +check_captive_portal_changed(struct unwind_conf *a, struct unwind_conf *b) +{ + + if (a->captive_portal_expected_status != + b->captive_portal_expected_status) + return (1); + + if (a->captive_portal_host == NULL && b->captive_portal_host != NULL) + return (1); + if (a->captive_portal_host != NULL && b->captive_portal_host == NULL) + return (1); + if (a->captive_portal_host != NULL && b->captive_portal_host != NULL && + strcmp(a->captive_portal_host, b->captive_portal_host) != 0) + return (1); + + if (a->captive_portal_path == NULL && b->captive_portal_path != NULL) + return (1); + if (a->captive_portal_path != NULL && b->captive_portal_path == NULL) + return (1); + if (a->captive_portal_path != NULL && b->captive_portal_path != NULL && + strcmp(a->captive_portal_path, b->captive_portal_path) != 0) + return (1); + + if (a->captive_portal_expected_response == NULL && + b->captive_portal_expected_response != NULL) + return (1); + if (a->captive_portal_expected_response != NULL && + b->captive_portal_expected_response == NULL) + return (1); + if (a->captive_portal_expected_response != NULL && + b->captive_portal_expected_response != NULL && + strcmp(a->captive_portal_expected_response, + b->captive_portal_expected_response) != 0) + return (1); + + return (0); + +} diff --git a/sbin/unwind/resolver.h b/sbin/unwind/resolver.h index da9525ba3a0..2265e3c7ec1 100644 --- a/sbin/unwind/resolver.h +++ b/sbin/unwind/resolver.h @@ -1,4 +1,4 @@ -/* $OpenBSD: resolver.h,v 1.2 2019/01/27 12:40:54 florian Exp $ */ +/* $OpenBSD: resolver.h,v 1.3 2019/02/03 12:02:30 florian Exp $ */ /* * Copyright (c) 2018 Florian Obser <florian@openbsd.org> @@ -68,5 +68,7 @@ struct ctl_resolver_info { int selected; }; -void resolver(int, int); -int resolver_imsg_compose_frontend(int, pid_t, void *, uint16_t); +void resolver(int, int); +int resolver_imsg_compose_main(int, pid_t, void *, uint16_t); +int resolver_imsg_compose_frontend(int, pid_t, void *, uint16_t); +int resolver_imsg_compose_captiveportal(int, pid_t, void *, uint16_t); diff --git a/sbin/unwind/unwind.c b/sbin/unwind/unwind.c index d72b98f98b0..5e829a9c389 100644 --- a/sbin/unwind/unwind.c +++ b/sbin/unwind/unwind.c @@ -1,4 +1,4 @@ -/* $OpenBSD: unwind.c,v 1.9 2019/02/01 15:52:35 florian Exp $ */ +/* $OpenBSD: unwind.c,v 1.10 2019/02/03 12:02:30 florian Exp $ */ /* * Copyright (c) 2018 Florian Obser <florian@openbsd.org> @@ -45,6 +45,7 @@ #include "frontend.h" #include "resolver.h" #include "control.h" +#include "captiveportal.h" __dead void usage(void); __dead void main_shutdown(void); @@ -55,22 +56,26 @@ static pid_t start_child(int, char *, int, int, int); void main_dispatch_frontend(int, short, void *); void main_dispatch_resolver(int, short, void *); +void main_dispatch_captiveportal(int, short, void *); -static int main_imsg_send_ipc_sockets(struct imsgbuf *, struct imsgbuf *); +static int main_imsg_send_ipc_sockets(struct imsgbuf *, struct imsgbuf *, + struct imsgbuf *); static int main_imsg_send_config(struct unwind_conf *); int main_reload(void); -int main_sendboth(enum imsg_type, void *, uint16_t); +int main_sendall(enum imsg_type, void *, uint16_t); void open_dhcp_lease(int); void open_ports(void); struct unwind_conf *main_conf; struct imsgev *iev_frontend; struct imsgev *iev_resolver; +struct imsgev *iev_captiveportal; char *conffile; pid_t frontend_pid; pid_t resolver_pid; +pid_t captiveportal_pid; uint32_t cmd_opts; @@ -113,10 +118,12 @@ main(int argc, char *argv[]) { struct event ev_sigint, ev_sigterm, ev_sighup; int ch; - int debug = 0, resolver_flag = 0, frontend_flag = 0; + int debug = 0, resolver_flag = 0; + int frontend_flag = 0, captiveportal_flag = 0; char *saved_argv0; int pipe_main2frontend[2]; int pipe_main2resolver[2]; + int pipe_main2captiveportal[2]; int frontend_routesock, rtfilter; int control_fd; char *csock; @@ -131,8 +138,11 @@ main(int argc, char *argv[]) if (saved_argv0 == NULL) saved_argv0 = "unwind"; - while ((ch = getopt(argc, argv, "dEFf:ns:v")) != -1) { + while ((ch = getopt(argc, argv, "CdEFf:ns:v")) != -1) { switch (ch) { + case 'C': + captiveportal_flag = 1; + break; case 'd': debug = 1; break; @@ -163,13 +173,15 @@ main(int argc, char *argv[]) argc -= optind; argv += optind; - if (argc > 0 || (resolver_flag && frontend_flag)) + if (argc > 0 || (resolver_flag && frontend_flag && captiveportal_flag)) usage(); if (resolver_flag) resolver(debug, cmd_opts & (OPT_VERBOSE | OPT_VERBOSE2)); else if (frontend_flag) frontend(debug, cmd_opts & (OPT_VERBOSE | OPT_VERBOSE2)); + else if (captiveportal_flag) + captiveportal(debug, cmd_opts & (OPT_VERBOSE | OPT_VERBOSE2)); if ((main_conf = parse_config(conffile)) == NULL) exit(1); @@ -204,6 +216,9 @@ main(int argc, char *argv[]) if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, PF_UNSPEC, pipe_main2resolver) == -1) fatal("main2resolver socketpair"); + if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, + PF_UNSPEC, pipe_main2captiveportal) == -1) + fatal("main2captiveportal socketpair"); /* Start children. */ resolver_pid = start_child(PROC_RESOLVER, saved_argv0, @@ -212,6 +227,9 @@ main(int argc, char *argv[]) frontend_pid = start_child(PROC_FRONTEND, saved_argv0, pipe_main2frontend[1], debug, cmd_opts & (OPT_VERBOSE | OPT_VERBOSE2)); + captiveportal_pid = start_child(PROC_CAPTIVEPORTAL, saved_argv0, + pipe_main2captiveportal[1], debug, cmd_opts & (OPT_VERBOSE | + OPT_VERBOSE2)); unwind_process = PROC_MAIN; log_procinit(log_procnames[unwind_process]); @@ -230,14 +248,17 @@ main(int argc, char *argv[]) /* Setup pipes to children. */ if ((iev_frontend = malloc(sizeof(struct imsgev))) == NULL || + (iev_captiveportal = malloc(sizeof(struct imsgev))) == NULL || (iev_resolver = malloc(sizeof(struct imsgev))) == NULL) fatal(NULL); imsg_init(&iev_frontend->ibuf, pipe_main2frontend[0]); iev_frontend->handler = main_dispatch_frontend; imsg_init(&iev_resolver->ibuf, pipe_main2resolver[0]); iev_resolver->handler = main_dispatch_resolver; + imsg_init(&iev_captiveportal->ibuf, pipe_main2captiveportal[0]); + iev_captiveportal->handler = main_dispatch_captiveportal; - /* Setup event handlers for pipes to resolver & frontend. */ + /* Setup event handlers for pipes. */ iev_frontend->events = EV_READ; event_set(&iev_frontend->ev, iev_frontend->ibuf.fd, iev_frontend->events, iev_frontend->handler, iev_frontend); @@ -248,7 +269,14 @@ main(int argc, char *argv[]) iev_resolver->handler, iev_resolver); event_add(&iev_resolver->ev, NULL); - if (main_imsg_send_ipc_sockets(&iev_frontend->ibuf, &iev_resolver->ibuf)) + iev_captiveportal->events = EV_READ; + event_set(&iev_captiveportal->ev, iev_captiveportal->ibuf.fd, + iev_captiveportal->events, iev_captiveportal->handler, + iev_captiveportal); + event_add(&iev_captiveportal->ev, NULL); + + if (main_imsg_send_ipc_sockets(&iev_frontend->ibuf, + &iev_resolver->ibuf, &iev_captiveportal->ibuf)) fatal("could not establish imsg links"); if ((control_fd = control_init(csock)) == -1) @@ -290,6 +318,8 @@ main_shutdown(void) close(iev_frontend->ibuf.fd); msgbuf_clear(&iev_resolver->ibuf.w); close(iev_resolver->ibuf.fd); + msgbuf_clear(&iev_captiveportal->ibuf.w); + close(iev_captiveportal->ibuf.fd); config_clear(main_conf); @@ -307,6 +337,7 @@ main_shutdown(void) free(iev_frontend); free(iev_resolver); + free(iev_captiveportal); log_info("terminating"); exit(0); @@ -342,6 +373,9 @@ start_child(int p, char *argv0, int fd, int debug, int verbose) case PROC_FRONTEND: argv[argc++] = "-F"; break; + case PROC_CAPTIVEPORTAL: + argv[argc++] = "-C"; + break; } if (debug) argv[argc++] = "-d"; @@ -433,6 +467,77 @@ main_dispatch_frontend(int fd, short event, void *bula) void main_dispatch_resolver(int fd, short event, void *bula) { + struct imsgev *iev = bula; + struct imsgbuf *ibuf; + struct imsg imsg; + struct sockaddr_in sin; + ssize_t n; + int shut = 0, httpsock; + + ibuf = &iev->ibuf; + + if (event & EV_READ) { + if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN) + fatal("imsg_read error"); + if (n == 0) /* Connection closed. */ + shut = 1; + } + if (event & EV_WRITE) { + if ((n = msgbuf_write(&ibuf->w)) == -1 && errno != EAGAIN) + fatal("msgbuf_write"); + if (n == 0) /* Connection closed. */ + shut = 1; + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("imsg_get"); + if (n == 0) /* No more messages. */ + break; + + switch (imsg.hdr.type) { + case IMSG_OPEN_HTTP_PORT: + if (imsg.hdr.len != IMSG_HEADER_SIZE + + sizeof(sin)) + fatalx("%s: IMSG_OPEN_HTTP_PORT wrong length: " + "%d", __func__, imsg.hdr.len); + memcpy(&sin, imsg.data, sizeof(sin)); + + if ((httpsock = socket(AF_INET, SOCK_STREAM | + SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) == -1) { + log_warn("%s: socket", __func__); + break; + } + if (connect(httpsock, (struct sockaddr *)&sin, + sin.sin_len) == -1) { + if (errno != EINPROGRESS) { + log_warn("%s: connect", __func__); + close(httpsock); + break; + } + } + main_imsg_compose_captiveportal_fd(IMSG_HTTPSOCK, 0, + httpsock); + break; + default: + log_debug("%s: error handling imsg %d", __func__, + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + if (!shut) + imsg_event_add(iev); + else { + /* This pipe is dead. Remove its event handler. */ + event_del(&iev->ev); + event_loopexit(NULL); + } +} + +void +main_dispatch_captiveportal(int fd, short event, void *bula) +{ struct imsgev *iev = bula; struct imsgbuf *ibuf; struct imsg imsg; @@ -492,7 +597,6 @@ main_imsg_compose_frontend_fd(int type, pid_t pid, int fd) imsg_compose_event(iev_frontend, type, 0, pid, fd, NULL, 0); } - void main_imsg_compose_resolver(int type, pid_t pid, void *data, uint16_t datalen) { @@ -502,6 +606,23 @@ main_imsg_compose_resolver(int type, pid_t pid, void *data, uint16_t datalen) } void +main_imsg_compose_captiveportal(int type, pid_t pid, void *data, + uint16_t datalen) +{ + if (iev_captiveportal) + imsg_compose_event(iev_captiveportal, type, 0, pid, -1, data, + datalen); +} + +void +main_imsg_compose_captiveportal_fd(int type, pid_t pid, int fd) +{ + if (iev_frontend) + imsg_compose_event(iev_captiveportal, type, 0, pid, fd, NULL, + 0); +} + +void imsg_event_add(struct imsgev *iev) { iev->events = EV_READ; @@ -528,21 +649,45 @@ imsg_compose_event(struct imsgev *iev, uint16_t type, uint32_t peerid, static int main_imsg_send_ipc_sockets(struct imsgbuf *frontend_buf, - struct imsgbuf *resolver_buf) + struct imsgbuf *resolver_buf, struct imsgbuf *captiveportal_buf) { int pipe_frontend2resolver[2]; + int pipe_frontend2captiveportal[2]; + int pipe_resolver2captiveportal[2]; if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, PF_UNSPEC, pipe_frontend2resolver) == -1) return (-1); - if (imsg_compose(frontend_buf, IMSG_SOCKET_IPC, 0, 0, + if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, + PF_UNSPEC, pipe_frontend2captiveportal) == -1) + return (-1); + + if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, + PF_UNSPEC, pipe_resolver2captiveportal) == -1) + return (-1); + + if (imsg_compose(frontend_buf, IMSG_SOCKET_IPC_RESOLVER, 0, 0, pipe_frontend2resolver[0], NULL, 0) == -1) return (-1); - if (imsg_compose(resolver_buf, IMSG_SOCKET_IPC, 0, 0, + if (imsg_compose(resolver_buf, IMSG_SOCKET_IPC_FRONTEND, 0, 0, pipe_frontend2resolver[1], NULL, 0) == -1) return (-1); + if (imsg_compose(frontend_buf, IMSG_SOCKET_IPC_CAPTIVEPORTAL, 0, 0, + pipe_frontend2captiveportal[0], NULL, 0) == -1) + return (-1); + if (imsg_compose(captiveportal_buf, IMSG_SOCKET_IPC_FRONTEND, 0, 0, + pipe_frontend2captiveportal[1], NULL, 0) == -1) + return (-1); + + if (imsg_compose(resolver_buf, IMSG_SOCKET_IPC_CAPTIVEPORTAL, 0, 0, + pipe_resolver2captiveportal[0], NULL, 0) == -1) + return (-1); + if (imsg_compose(captiveportal_buf, IMSG_SOCKET_IPC_RESOLVER, 0, 0, + pipe_resolver2captiveportal[1], NULL, 0) == -1) + return (-1); + return (0); } @@ -568,40 +713,62 @@ main_imsg_send_config(struct unwind_conf *xconf) struct unwind_forwarder *unwind_forwarder; /* Send fixed part of config to children. */ - if (main_sendboth(IMSG_RECONF_CONF, xconf, sizeof(*xconf)) == -1) + if (main_sendall(IMSG_RECONF_CONF, xconf, sizeof(*xconf)) == -1) return (-1); + if (xconf->captive_portal_host != NULL) { + if (main_sendall(IMSG_RECONF_CAPTIVE_PORTAL_HOST, + xconf->captive_portal_host, + strlen(xconf->captive_portal_host) + 1) == -1) + return (-1); + } + + if (xconf->captive_portal_path != NULL) { + if (main_sendall(IMSG_RECONF_CAPTIVE_PORTAL_PATH, + xconf->captive_portal_path, + strlen(xconf->captive_portal_path) + 1) == -1) + return (-1); + } + + if (xconf->captive_portal_expected_response != NULL) { + if (main_sendall(IMSG_RECONF_CAPTIVE_PORTAL_EXPECTED_RESPONSE, + xconf->captive_portal_expected_response, + strlen(xconf->captive_portal_expected_response) + 1) + == -1) + return (-1); + } /* send static forwarders to children */ SIMPLEQ_FOREACH(unwind_forwarder, &xconf->unwind_forwarder_list, entry) { - if (main_sendboth(IMSG_RECONF_FORWARDER, unwind_forwarder, + if (main_sendall(IMSG_RECONF_FORWARDER, unwind_forwarder, sizeof(*unwind_forwarder)) == -1) return (-1); - } /* send static DoT forwarders to children */ SIMPLEQ_FOREACH(unwind_forwarder, &xconf->unwind_dot_forwarder_list, entry) { - if (main_sendboth(IMSG_RECONF_DOT_FORWARDER, unwind_forwarder, + if (main_sendall(IMSG_RECONF_DOT_FORWARDER, unwind_forwarder, sizeof(*unwind_forwarder)) == -1) return (-1); - } /* Tell children the revised config is now complete. */ - if (main_sendboth(IMSG_RECONF_END, NULL, 0) == -1) + if (main_sendall(IMSG_RECONF_END, NULL, 0) == -1) return (-1); return (0); } int -main_sendboth(enum imsg_type type, void *buf, uint16_t len) +main_sendall(enum imsg_type type, void *buf, uint16_t len) { if (imsg_compose_event(iev_frontend, type, 0, 0, -1, buf, len) == -1) return (-1); if (imsg_compose_event(iev_resolver, type, 0, 0, -1, buf, len) == -1) return (-1); + if (imsg_compose_event(iev_captiveportal, type, 0, 0, -1, buf, len) == + -1) + return (-1); return (0); } @@ -624,6 +791,21 @@ merge_config(struct unwind_conf *conf, struct unwind_conf *xconf) conf->unwind_options = xconf->unwind_options; + free(conf->captive_portal_host); + conf->captive_portal_host = xconf->captive_portal_host; + + free(conf->captive_portal_path); + conf->captive_portal_path = xconf->captive_portal_path; + + free(conf->captive_portal_expected_response); + conf->captive_portal_expected_response = + xconf->captive_portal_expected_response; + + conf->captive_portal_expected_status = + xconf->captive_portal_expected_status; + + conf->captive_portal_auto = xconf->captive_portal_auto; + /* Add new forwarders. */ while ((unwind_forwarder = SIMPLEQ_FIRST(&xconf->unwind_forwarder_list)) != NULL) { @@ -653,6 +835,11 @@ config_new_empty(void) SIMPLEQ_INIT(&xconf->unwind_forwarder_list); SIMPLEQ_INIT(&xconf->unwind_dot_forwarder_list); + if ((xconf->captive_portal_expected_response = strdup("")) == NULL) + fatal(NULL); + xconf->captive_portal_expected_status = 200; + xconf->captive_portal_auto = 1; + return (xconf); } diff --git a/sbin/unwind/unwind.conf.5 b/sbin/unwind/unwind.conf.5 index d20526e4a27..8e4b89e5292 100644 --- a/sbin/unwind/unwind.conf.5 +++ b/sbin/unwind/unwind.conf.5 @@ -1,4 +1,4 @@ -.\" $OpenBSD: unwind.conf.5,v 1.3 2019/01/29 19:30:41 florian Exp $ +.\" $OpenBSD: unwind.conf.5,v 1.4 2019/02/03 12:02:30 florian Exp $ .\" .\" Copyright (c) 2018 Florian Obser <florian@openbsd.org> .\" Copyright (c) 2005 Esben Norby <norby@openbsd.org> @@ -18,7 +18,7 @@ .\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF .\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. .\" -.Dd $Mdocdate: January 29 2019 $ +.Dd $Mdocdate: February 3 2019 $ .Dt UNWIND.CONF 5 .Os .Sh NAME @@ -69,7 +69,56 @@ If .Ic DoT is specified use DNS over TLS when sending queries to the server at .Ar address . - +.El +.Pp +.Nm unwind +can detect when it is running behind a +.Dq Captive Portal +by sending a HTTP request and checking the response against the +configured expected response. +The check will be triggered when +.Xr dhclient 8 +reports new nameservers. +If the response does not match, +.Nm unwind +will use the DHCP provided nameservers and periodically re-checks if the user +passed the +.Dq Captive Portal . +.Bl -tag -width Ds +.It Ic captive portal Brq ... +.Dq CaptivePortal +detection is configured inside the +.Oc captive portal +block. +.Bl -tag -width "url URL" -compatc +.It Ic url Ar URL +URL to send HTTP queries to. +This parameter is required. +.It Ic expected response Ar response +The body of the HTTP response is compare to +.Ar response. +The default is the empty string. +.It Ic expteced status Ar status +The expected HTTP status code. +The default is 200. +.It Ic auto Op Cm yes | Cm no +When +.Ic auto +is set to +.Cm yes +.Nm unwind +automatically triggers a +.Dq Captive Portal +check when the network is changed. +When set to +.Cm no +a +.Dq Captive Portal +check can be triggered by +.Xr unwindctl 8 . +The default is +.Cm yes +.El .El .Sh FILES .Bl -tag -width "/etc/unwind.conf" -compact diff --git a/sbin/unwind/unwind.h b/sbin/unwind/unwind.h index 2ee967fde81..94d11b45682 100644 --- a/sbin/unwind/unwind.h +++ b/sbin/unwind/unwind.h @@ -1,4 +1,4 @@ -/* $OpenBSD: unwind.h,v 1.5 2019/01/30 12:17:02 benno Exp $ */ +/* $OpenBSD: unwind.h,v 1.6 2019/02/03 12:02:30 florian Exp $ */ /* * Copyright (c) 2018 Florian Obser <florian@openbsd.org> @@ -38,13 +38,15 @@ enum { PROC_MAIN, PROC_RESOLVER, - PROC_FRONTEND + PROC_FRONTEND, + PROC_CAPTIVEPORTAL, } unwind_process; static const char * const log_procnames[] = { "main", "resolver", "frontend", + "captive portal", }; struct imsgev { @@ -59,7 +61,11 @@ enum imsg_type { IMSG_CTL_LOG_VERBOSE, IMSG_CTL_RELOAD, IMSG_CTL_STATUS, + IMSG_CTL_CAPTIVEPORTAL_INFO, IMSG_RECONF_CONF, + IMSG_RECONF_CAPTIVE_PORTAL_HOST, + IMSG_RECONF_CAPTIVE_PORTAL_PATH, + IMSG_RECONF_CAPTIVE_PORTAL_EXPECTED_RESPONSE, IMSG_RECONF_FORWARDER, IMSG_RECONF_DOT_FORWARDER, IMSG_RECONF_END, @@ -69,7 +75,9 @@ enum imsg_type { IMSG_CONTROLFD, IMSG_STARTUP, IMSG_STARTUP_DONE, - IMSG_SOCKET_IPC, + IMSG_SOCKET_IPC_FRONTEND, + IMSG_SOCKET_IPC_RESOLVER, + IMSG_SOCKET_IPC_CAPTIVEPORTAL, IMSG_QUERY, IMSG_ANSWER_HEADER, IMSG_ANSWER, @@ -82,7 +90,11 @@ enum imsg_type { IMSG_CTL_RESOLVER_INFO, IMSG_CTL_RESOLVER_WHY_BOGUS, IMSG_CTL_RESOLVER_HISTOGRAM, - IMSG_CTL_END + IMSG_CTL_END, + IMSG_CTL_RECHECK_CAPTIVEPORTAL, + IMSG_OPEN_HTTP_PORT, + IMSG_HTTPSOCK, + IMSG_CAPTIVEPORTAL_STATE }; struct unwind_forwarder { @@ -92,8 +104,13 @@ struct unwind_forwarder { struct unwind_conf { SIMPLEQ_HEAD(unwind_forwarder_head, unwind_forwarder) unwind_forwarder_list; - struct unwind_forwarder_head unwind_dot_forwarder_list; - int unwind_options; + struct unwind_forwarder_head unwind_dot_forwarder_list; + int unwind_options; + char *captive_portal_host; + char *captive_portal_path; + char *captive_portal_expected_response; + int captive_portal_expected_status; + int captive_portal_auto; }; struct query_imsg { @@ -113,8 +130,9 @@ extern uint32_t cmd_opts; /* unwind.c */ void main_imsg_compose_frontend(int, pid_t, void *, uint16_t); void main_imsg_compose_frontend_fd(int, pid_t, int); - void main_imsg_compose_resolver(int, pid_t, void *, uint16_t); +void main_imsg_compose_captiveportal(int, pid_t, void *, uint16_t); +void main_imsg_compose_captiveportal_fd(int, pid_t, int); void merge_config(struct unwind_conf *, struct unwind_conf *); void imsg_event_add(struct imsgev *); int imsg_compose_event(struct imsgev *, uint16_t, uint32_t, pid_t, diff --git a/sbin/unwind/uw_parse.y b/sbin/unwind/uw_parse.y index c5804759a44..cd710d0bff0 100644 --- a/sbin/unwind/uw_parse.y +++ b/sbin/unwind/uw_parse.y @@ -1,4 +1,4 @@ -/* $OpenBSD: uw_parse.y,v 1.8 2019/01/30 12:18:48 benno Exp $ */ +/* $OpenBSD: uw_parse.y,v 1.9 2019/02/03 12:02:30 florian Exp $ */ /* * Copyright (c) 2018 Florian Obser <florian@openbsd.org> @@ -99,7 +99,8 @@ typedef struct { %} %token STRICT YES NO INCLUDE ERROR -%token FORWARDER DOT PORT +%token FORWARDER DOT PORT CAPTIVE PORTAL URL EXPECTED RESPONSE +%token STATUS AUTO %token <v.string> STRING %token <v.number> NUMBER @@ -114,6 +115,7 @@ grammar : /* empty */ | grammar conf_main '\n' | grammar varset '\n' | grammar unwind_forwarder '\n' + | grammar captive_portal '\n' | grammar error '\n' { file->errors++; } ; @@ -176,8 +178,55 @@ optnl : '\n' optnl /* zero or more newlines */ | /*empty*/ ; -nl : '\n' optnl /* one or more newlines */ - ; +captive_portal : CAPTIVE PORTAL captive_portal_block + ; +captive_portal_block : '{' optnl captive_portal_opts_l '}' + | captive_portal_optsl + ; + +captive_portal_opts_l : captive_portal_opts_l captive_portal_optsl optnl + | captive_portal_optsl optnl + ; + +captive_portal_optsl : URL STRING { + char *ep; + if (strncmp($2, "http://", 7) != 0) { + yyerror("only http:// urls are " + "supported: %s", $2); + free($2); + YYERROR; + } + if ((ep = strchr($2 + 7, '/')) != NULL) { + conf->captive_portal_path = + strdup(ep); + *ep = '\0'; + } else + conf->captive_portal_path = strdup("/"); + if (conf->captive_portal_path == NULL) + err(1, "strdup"); + if ((conf->captive_portal_host = + strdup($2 + 7)) == NULL) + err(1, "strdup"); + free($2); + } + | EXPECTED RESPONSE STRING { + if ((conf->captive_portal_expected_response = + strdup($3)) == NULL) + err(1, "strdup"); + free($3); + } + | EXPECTED STATUS NUMBER { + if ($3 < 100 || $3 > 599) { + yyerror("%lld is an invalid http " + "status", $3); + YYERROR; + } + conf->captive_portal_expected_status = $3; + } + | AUTO yesno { + conf->captive_portal_auto = $2; + } + ; unwind_forwarder : FORWARDER forwarder_block ; @@ -188,6 +237,7 @@ forwarder_block : '{' optnl forwarderopts_l '}' forwarderopts_l : forwarderopts_l forwarderoptsl optnl | forwarderoptsl optnl + ; forwarderoptsl : STRING { struct sockaddr_storage *ss; @@ -346,14 +396,21 @@ lookup(char *s) { /* This has to be sorted always. */ static const struct keywords keywords[] = { - {"dot", DOT}, {"DoT", DOT}, + {"auto", AUTO}, + {"captive", CAPTIVE}, + {"dot", DOT}, + {"expected", EXPECTED}, {"forwarder", FORWARDER}, {"include", INCLUDE}, {"no", NO}, {"port", PORT}, + {"portal", PORTAL}, + {"response", RESPONSE}, + {"status", STATUS}, {"strict", STRICT}, {"tls", DOT}, + {"url", URL}, {"yes", YES}, }; const struct keywords *p; diff --git a/usr.sbin/unwindctl/parser.c b/usr.sbin/unwindctl/parser.c index 57ba623b797..9c6d22480ab 100644 --- a/usr.sbin/unwindctl/parser.c +++ b/usr.sbin/unwindctl/parser.c @@ -1,4 +1,4 @@ -/* $OpenBSD: parser.c,v 1.2 2019/01/27 12:41:39 florian Exp $ */ +/* $OpenBSD: parser.c,v 1.3 2019/02/03 12:02:30 florian Exp $ */ /* * Copyright (c) 2004 Esben Norby <norby@openbsd.org> @@ -51,11 +51,13 @@ struct token { static const struct token t_main[]; static const struct token t_log[]; static const struct token t_status[]; +static const struct token t_recheck[]; static const struct token t_main[] = { {KEYWORD, "reload", RELOAD, NULL}, {KEYWORD, "status", STATUS, t_status}, {KEYWORD, "log", NONE, t_log}, + {KEYWORD, "recheck", NONE, t_recheck}, {ENDTOKEN, "", NONE, NULL} }; @@ -75,6 +77,11 @@ static const struct token t_status[] = { {ENDTOKEN, "", STATUS, NULL} }; +static const struct token t_recheck[] = { + {KEYWORD, "portal", PORTAL, NULL}, + {ENDTOKEN, "", NONE, NULL} +}; + static const struct token *match_token(const char *, const struct token *, struct parse_result *); static void show_valid_args(const struct token *); diff --git a/usr.sbin/unwindctl/parser.h b/usr.sbin/unwindctl/parser.h index 10d8e7815e0..a2482694284 100644 --- a/usr.sbin/unwindctl/parser.h +++ b/usr.sbin/unwindctl/parser.h @@ -1,4 +1,4 @@ -/* $OpenBSD: parser.h,v 1.2 2019/01/27 12:41:39 florian Exp $ */ +/* $OpenBSD: parser.h,v 1.3 2019/02/03 12:02:30 florian Exp $ */ /* * Copyright (c) 2004 Esben Norby <norby@openbsd.org> @@ -23,6 +23,7 @@ enum actions { LOG_VERBOSE, LOG_BRIEF, RELOAD, + PORTAL, STATUS, STATUS_RECURSOR, STATUS_DHCP, diff --git a/usr.sbin/unwindctl/unwindctl.8 b/usr.sbin/unwindctl/unwindctl.8 index f8dd5a47b2e..d04fa17aa7a 100644 --- a/usr.sbin/unwindctl/unwindctl.8 +++ b/usr.sbin/unwindctl/unwindctl.8 @@ -1,4 +1,4 @@ -.\" $OpenBSD: unwindctl.8,v 1.1 2019/01/23 13:12:19 florian Exp $ +.\" $OpenBSD: unwindctl.8,v 1.2 2019/02/03 12:02:30 florian Exp $ .\" .\" Copyright (c) 2004, 2005 Esben Norby <norby@openbsd.org> .\" @@ -14,7 +14,7 @@ .\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF .\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. .\" -.Dd $Mdocdate: January 23 2019 $ +.Dd $Mdocdate: February 3 2019 $ .Dt UNWINDCTL 8 .Os .Sh NAME @@ -53,6 +53,10 @@ Enable verbose logging. Enable very noisy debug logging. .It Cm reload Reload the configuration file. +.It Cm recheck Cm portal +Run the +.Dq Captive Portal +detection. .It Cm status Op Cm recursor Cm dhcp Cm static Show a status summary. If one of diff --git a/usr.sbin/unwindctl/unwindctl.c b/usr.sbin/unwindctl/unwindctl.c index 21146b84767..f90347314ae 100644 --- a/usr.sbin/unwindctl/unwindctl.c +++ b/usr.sbin/unwindctl/unwindctl.c @@ -1,4 +1,4 @@ -/* $OpenBSD: unwindctl.c,v 1.3 2019/01/31 13:36:42 solene Exp $ */ +/* $OpenBSD: unwindctl.c,v 1.4 2019/02/03 12:02:30 florian Exp $ */ /* * Copyright (c) 2005 Claudio Jeker <claudio@openbsd.org> @@ -38,6 +38,7 @@ #include <unistd.h> #include "unwind.h" +#include "captiveportal.h" #include "frontend.h" #include "resolver.h" #include "parser.h" @@ -140,6 +141,12 @@ main(int argc, char *argv[]) printf("reload request sent.\n"); done = 1; break; + case PORTAL: + imsg_compose(ibuf, IMSG_CTL_RECHECK_CAPTIVEPORTAL, 0, 0, -1, + NULL, 0); + printf("recheck request sent.\n"); + done = 1; + break; case STATUS_RECURSOR: type = RECURSOR; imsg_compose(ibuf, IMSG_CTL_STATUS, 0, 0, -1, &type, @@ -210,11 +217,28 @@ show_status_msg(struct imsg *imsg) { static int header; struct ctl_resolver_info *cri; + enum captive_portal_state captive_portal_state; - if (!header++) + if (imsg->hdr.type != IMSG_CTL_CAPTIVEPORTAL_INFO && !header++) printf("%8s %16s %s\n", "selected", "type", "status"); switch (imsg->hdr.type) { + case IMSG_CTL_CAPTIVEPORTAL_INFO: + memcpy(&captive_portal_state, imsg->data, + sizeof(captive_portal_state)); + switch (captive_portal_state) { + case PORTAL_UNCHECKED: + case PORTAL_UNKNOWN: + printf("captive portal is %s\n\n", + captive_portal_state_str[captive_portal_state]); + break; + case BEHIND: + case NOT_BEHIND: + printf("%s captive portal\n\n", + captive_portal_state_str[captive_portal_state]); + break; + } + break; case IMSG_CTL_RESOLVER_INFO: cri = imsg->data; printf("%8s %16s %s\n", cri->selected ? "*" : " ", |