In case you are wondering why happy eyballs: It's a variation on this:
https://en.wikipedia.org/wiki/Happy_Eyeballs

unwind has a concept of a best nameserver type. It considers a
configured DoT nameserver to be better than doing it's own recursive
resolving. Recursive resolving is considered to be better than asking
the dhcp provided nameservers.

It also actively checks the quality of a nameserver. Does it actually
work? Than it's better than an unreachable one. Does it support DNSSEC
validation by passing through the required records, than it's better
than a non-validating one. (The crypto is always done by unwind
itself.)

Now, this is all static. Once it figured out that DoT works and can do
validation, DoT it is. Always. No matter how far away it is.

Or you don't have DoT configured but you can talk unfiltered DNS to
the internet. The recursor will be picked. No matter if you can only
see parts of the internet. Maybe all .org nameservers are unreachable
from your location.

unwind knows about this. It tracks how long it takes to get an answer.
It knows when it can't talk to the .org nameservers. Either the
queries time out or we get a icmp error back. But it doesn't do
anything about it. Because unwind determind (statically) that you are
already using the best nameserver type, why try something else? Well,
because that particular query might be better answerd by a different
nameserver type. Or maybe you are sitting behind a sat link and
everything is terribly slow.

This diff sorts the nameserver types by quality, as above (validation,
resolving, dead...), and as a tie breaker it adds the median of the
round trip time of previous queries into the mix. 

Then it considers the whole list, not just the best one. It picks the
top one from the list, sends a query and waits "median" time for an
answer. If none comes in it picks the next one and so on. Eventually
an answer is found (maybe the first one came back, or the 2nd one was
faster) and the answer is send back to the client.

To keep the configured (or default) preference the first resolver type
gets a head start of 200ms.

One other interesting thing about this is that it gets us past captive
portals without a check URL, that's why this diff is so huge, it rips
out all the captive portal stuff (please apply with patch -E):
 17 files changed, 385 insertions(+), 1683 deletions(-)

Please test this. I'm particularly interested in reports from people
who move between networks and need to get past captive portals.

diff --git etc/examples/unwind.conf etc/examples/unwind.conf
deleted file mode 100644
index ea60cf9b6ff..00000000000
--- etc/examples/unwind.conf
+++ /dev/null
@@ -1,35 +0,0 @@
-# $OpenBSD: unwind.conf,v 1.1 2019/02/05 16:23:58 florian Exp $
-#
-# unwind(8) works without a configuration file in most cases.
-# See unwind.conf(5) for configuration options.
-
-# Non-exhaustive list of connectivity test providers.
-# To only occasionally run a captive portal check manually from unwindctl(8)
-# add "auto no" to the block.
-#captive portal {
-#      url "http://captive.apple.com/";
-#      expected response 
"<HTML><HEAD><TITLE>Success</TITLE></HEAD><BODY>Success</BODY></HTML>\n"
-#      # auto no # uncomment to run manually from unwindctl(8)
-#}
-#captive portal {
-#      url "http://clients3.google.com/generate_204";
-#      expected status 204
-#      # auto no # uncomment to run manually from unwindctl(8)
-#}
-#captive portal {
-#      url "http://detectportal.firefox.com/";
-#      expected response "success\n"
-#      # auto no # uncomment to run manually from unwindctl(8)
-#}
-
-# Running a connectivity test provider with httpd(8).
-# httpd.conf
-#server "c.YOUR-DOMAIN.com" {
-#      listen on * port 80
-#      location "*" { block return 204 }
-#}
-# unwind.conf
-#captive portal {
-#      url "http://c.YOUR-DOMAIN.com/";
-#      expected status 204
-#}
diff --git sbin/unwind/Makefile sbin/unwind/Makefile
index b48a14ce4b1..cdc8af97170 100644
--- sbin/unwind/Makefile
+++ sbin/unwind/Makefile
@@ -2,7 +2,6 @@
 
 PROG=  unwind
 SRCS=  control.c resolver.c frontend.c log.c unwind.c parse.y printconf.c
-SRCS+= captiveportal.c
 MAN=   unwind.8 unwind.conf.5
 
 .include "${.CURDIR}/libunbound/Makefile.inc"
@@ -15,7 +14,7 @@ CFLAGS+= -Wmissing-declarations
 CFLAGS+= -Wshadow -Wpointer-arith
 CFLAGS+= -Wsign-compare
 YFLAGS=
-LDADD+=        -levent -lutil -ltls -lssl -lcrypto
-DPADD+= ${LIBEVENT} ${LIBUTIL} ${LIBTLS} ${LIBSSL} ${LIBCRYPTO}
+LDADD+=        -levent -lutil -lssl -lcrypto
+DPADD+= ${LIBEVENT} ${LIBUTIL} ${LIBSSL} ${LIBCRYPTO}
 
 .include <bsd.prog.mk>
diff --git sbin/unwind/captiveportal.c sbin/unwind/captiveportal.c
deleted file mode 100644
index 0c7b0c1f207..00000000000
--- sbin/unwind/captiveportal.c
+++ /dev/null
@@ -1,676 +0,0 @@
-/*     $OpenBSD: captiveportal.c,v 1.12 2019/05/14 14:51:31 florian Exp $      
*/
-
-/*
- * Copyright (c) 2018 Florian Obser <[email protected]>
- * Copyright (c) 2005 Claudio Jeker <[email protected]>
- * Copyright (c) 2004 Esben Norby <[email protected]>
- * Copyright (c) 2003, 2004 Henning Brauer <[email protected]>
- *
- * 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 "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 uw_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(\"/\")");
-
-       uw_process = PROC_CAPTIVEPORTAL;
-       setproctitle("%s", log_procnames[uw_process]);
-       log_procinit(log_procnames[uw_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 uw_conf   *nconf;
-       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:
-               case IMSG_RECONF_CAPTIVE_PORTAL_HOST:
-               case IMSG_RECONF_CAPTIVE_PORTAL_PATH:
-               case IMSG_RECONF_CAPTIVE_PORTAL_EXPECTED_RESPONSE:
-               case IMSG_RECONF_BLOCKLIST_FILE:
-               case IMSG_RECONF_FORWARDER:
-               case IMSG_RECONF_DOT_FORWARDER:
-                       imsg_receive_config(&imsg, &nconf);
-                       break;
-               case IMSG_RECONF_END:
-                       if (nconf == NULL)
-                               fatalx("%s: IMSG_RECONF_END without "
-                                   "IMSG_RECONF_CONF", __func__);
-                       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, verbose, 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_CTL_LOG_VERBOSE:
-                       if (IMSG_DATA_SIZE(imsg) != sizeof(verbose))
-                               fatalx("%s: IMSG_CTL_LOG_VERBOSE wrong length: "
-                                   "%lu", __func__, IMSG_DATA_SIZE(imsg));
-                       memcpy(&verbose, imsg.data, sizeof(verbose));
-                       log_setverbose(verbose);
-                       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
-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 = strcasestr(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 sbin/unwind/captiveportal.h sbin/unwind/captiveportal.h
deleted file mode 100644
index cee72005e5c..00000000000
--- sbin/unwind/captiveportal.h
+++ /dev/null
@@ -1,41 +0,0 @@
-/*     $OpenBSD: captiveportal.h,v 1.1 2019/02/03 12:02:30 florian Exp $       
*/
-
-/*
- * Copyright (c) 2018 Florian Obser <[email protected]>
- * Copyright (c) 2004, 2005 Esben Norby <[email protected]>
- *
- * 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 sbin/unwind/control.c sbin/unwind/control.c
index f2dd612dc46..66c762d891e 100644
--- sbin/unwind/control.c
+++ sbin/unwind/control.c
@@ -259,10 +259,6 @@ 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_DATA_SIZE(imsg) != sizeof(verbose))
                                break;
@@ -272,8 +268,6 @@ control_dispatch_imsg(int fd, short event, void *bula)
                            imsg.data, IMSG_DATA_SIZE(imsg));
                        frontend_imsg_compose_resolver(imsg.hdr.type,
                            imsg.hdr.pid, imsg.data, IMSG_DATA_SIZE(imsg));
-                       frontend_imsg_compose_captiveportal(imsg.hdr.type,
-                           imsg.hdr.pid, imsg.data,  IMSG_DATA_SIZE(imsg));
 
                        memcpy(&verbose, imsg.data, sizeof(verbose));
                        log_setverbose(verbose);
diff --git sbin/unwind/frontend.c sbin/unwind/frontend.c
index e81c7b247ef..f1ee3572259 100644
--- sbin/unwind/frontend.c
+++ sbin/unwind/frontend.c
@@ -121,7 +121,6 @@ void                         free_bl(void);
 struct uw_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;
 int                     ta_fd = -1;
@@ -247,9 +246,6 @@ 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);
@@ -257,7 +253,6 @@ frontend_shutdown(void)
        config_clear(frontend_conf);
 
        free(iev_resolver);
-       free(iev_captiveportal);
        free(iev_main);
 
        log_info("frontend exiting");
@@ -278,14 +273,6 @@ frontend_imsg_compose_resolver(int type, pid_t pid, void 
*data,
            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)
 {
@@ -345,42 +332,7 @@ frontend_dispatch_main(int fd, short event, void *bula)
                            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:
-               case IMSG_RECONF_CAPTIVE_PORTAL_HOST:
-               case IMSG_RECONF_CAPTIVE_PORTAL_PATH:
-               case IMSG_RECONF_CAPTIVE_PORTAL_EXPECTED_RESPONSE:
                case IMSG_RECONF_BLOCKLIST_FILE:
                case IMSG_RECONF_FORWARDER:
                case IMSG_RECONF_DOT_FORWARDER:
@@ -533,7 +485,6 @@ frontend_dispatch_resolver(int fd, short event, void *bula)
                        send_answer(pq);
                        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_AUTOCONF_RESOLVER_INFO:
@@ -579,50 +530,6 @@ 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)
 {
diff --git sbin/unwind/frontend.h sbin/unwind/frontend.h
index 10ee9952bf9..919c17b0dfb 100644
--- sbin/unwind/frontend.h
+++ sbin/unwind/frontend.h
@@ -35,10 +35,8 @@ struct imsg_rdns_proposal {
 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 *);
 void            add_new_ta(struct trust_anchor_head *, char *);
 void            free_tas(struct trust_anchor_head *);
diff --git sbin/unwind/parse.y sbin/unwind/parse.y
index a26f46e6c08..da4ab78fb54 100644
--- sbin/unwind/parse.y
+++ sbin/unwind/parse.y
@@ -98,9 +98,9 @@ typedef struct {
 
 %}
 
-%token YES NO INCLUDE ERROR
-%token FORWARDER DOT PORT CAPTIVE PORTAL URL EXPECTED RESPONSE
-%token STATUS AUTO AUTHENTICATION NAME PREFERENCE RECURSOR DHCP STUB
+%token INCLUDE ERROR
+%token FORWARDER DOT PORT 
+%token AUTHENTICATION NAME PREFERENCE RECURSOR DHCP STUB
 %token BLOCK LIST LOG
 
 %token <v.string>      STRING
@@ -116,7 +116,6 @@ grammar             : /* empty */
                | grammar varset '\n'
                | grammar uw_pref '\n'
                | grammar uw_forwarder '\n'
-               | grammar captive_portal '\n'
                | grammar block_list '\n'
                | grammar error '\n'            { file->errors++; }
                ;
@@ -149,10 +148,6 @@ string             : string STRING {
                | STRING
                ;
 
-yesno          : YES   { $$ = 1; }
-               | NO    { $$ = 0; }
-               ;
-
 varset         : STRING '=' string             {
                        char *s = $1;
                        if (cmd_opts & OPT_VERBOSE)
@@ -194,57 +189,7 @@ block_list         : BLOCK LIST STRING log {
                        }
                        ;
 
-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;
-                       }
-                       ;
-
-uw_pref                        : PREFERENCE { conf->res_pref_len = 0; } 
pref_block
+uw_pref                        : PREFERENCE { conf->res_pref.len = 0; } 
pref_block
                        ;
 
 pref_block             : '{' optnl prefopts_l '}'
@@ -258,11 +203,11 @@ prefopts_l                : prefopts_l prefoptsl optnl
 prefoptsl              : prefopt {
                                if (!check_pref_uniq($1))
                                        YYERROR;
-                               if (conf->res_pref_len >= UW_RES_NONE) {
+                               if (conf->res_pref.len >= UW_RES_NONE) {
                                        yyerror("preference list too long");
                                        YYERROR;
                                }
-                               conf->res_pref[conf->res_pref_len++] = $1;
+                               conf->res_pref.types[conf->res_pref.len++] = $1;
                        }
                        ;
 
@@ -405,28 +350,19 @@ lookup(char *s)
        static const struct keywords keywords[] = {
                {"DoT",                 DOT},
                {"authentication",      AUTHENTICATION},
-               {"auto",                AUTO},
                {"block",               BLOCK},
-               {"captive",             CAPTIVE},
                {"dhcp",                DHCP},
                {"dot",                 DOT},
-               {"expected",            EXPECTED},
                {"forwarder",           FORWARDER},
                {"include",             INCLUDE},
                {"list",                LIST},
                {"log",                 LOG},
                {"name",                NAME},
-               {"no",                  NO},
                {"port",                PORT},
-               {"portal",              PORTAL},
                {"preference",          PREFERENCE},
                {"recursor",            RECURSOR},
-               {"response",            RESPONSE},
-               {"status",              STATUS},
                {"stub",                STUB},
                {"tls",                 DOT},
-               {"url",                 URL},
-               {"yes",                 YES},
        };
        const struct keywords   *p;
 
@@ -936,8 +872,8 @@ check_pref_uniq(enum uw_resolver_type type)
 {
        int      i;
 
-       for (i = 0; i < conf->res_pref_len; i++)
-               if (conf->res_pref[i] == type) {
+       for (i = 0; i < conf->res_pref.len; i++)
+               if (conf->res_pref.types[i] == type) {
                        yyerror("%s is already in the preference list",
                            uw_resolver_type_str[type]);
                        return (0);
diff --git sbin/unwind/printconf.c sbin/unwind/printconf.c
index e84a99bfeb6..25f5af81495 100644
--- sbin/unwind/printconf.c
+++ sbin/unwind/printconf.c
@@ -68,10 +68,11 @@ print_config(struct uw_conf *conf)
        struct uw_forwarder     *uw_forwarder;
        int                      i;
 
-       if (conf->res_pref_len > 0) {
+       if (conf->res_pref.len > 0) {
                printf("preference {");
-               for (i = 0; i < conf->res_pref_len; i++) {
-                       printf(" %s", uw_resolver_type_str[conf->res_pref[i]]);
+               for (i = 0; i < conf->res_pref.len; i++) {
+                       printf(" %s",
+                           uw_resolver_type_str[conf->res_pref.types[i]]);
                }
                printf(" }\n");
        }
@@ -93,19 +94,6 @@ print_config(struct uw_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");
-       }
-
        if (conf->blocklist_file != NULL)
                printf("block list \"%s\"%s\n", conf->blocklist_file,
                    conf->blocklist_log ? " log" : "");
diff --git sbin/unwind/resolver.c sbin/unwind/resolver.c
index 39bee165c87..1a576bdbb71 100644
--- sbin/unwind/resolver.c
+++ sbin/unwind/resolver.c
@@ -54,21 +54,26 @@
 
 #include <openssl/crypto.h>
 
-#include "captiveportal.h"
 #include "log.h"
 #include "frontend.h"
 #include "unwind.h"
 #include "resolver.h"
 
+#define        TLS_DEFAULT_CA_CERT_FILE        "/etc/ssl/cert.pem"
 #define        UB_LOG_VERBOSE                  4
 #define        UB_LOG_BRIEF                    0
 
+/*
+ * The prefered resolver type can be this many ms slower than the next
+ * best and still be picked
+ */
+#define        PREF_RESOLVER_MEDIAN_SKEW       200     /* 200 ms */
+
+#define        DOUBT_NXDOMAIN_SEC              5 * 60  /* 5 minutes */
+
 #define        RESOLVER_CHECK_SEC              1
 #define        RESOLVER_CHECK_MAXSEC           1024 /* ~17 minutes */
 
-#define        PORTAL_CHECK_SEC                15
-#define        PORTAL_CHECK_MAXSEC             600
-
 #define        TRUST_ANCHOR_RETRY_INTERVAL     8640
 #define        TRUST_ANCHOR_QUERY_INTERVAL     43200
 
@@ -91,8 +96,21 @@ struct uw_resolver {
        int                      check_running;
        char                    *why_bogus;
        int64_t                  histogram[nitems(histogram_limits)];
+       int64_t                  latest_histogram[nitems(histogram_limits)];
 };
 
+struct running_query {
+       TAILQ_ENTRY(running_query)       entry;
+       struct query_imsg               *query_imsg;
+       struct event                     timer_ev;
+       struct timespec                  tp;
+       struct resolver_preference       res_pref;
+       int                              next_resolver;
+       int                              running;
+};
+
+TAILQ_HEAD(, running_query)     running_queries;
+
 typedef void (*resolve_cb_t)(struct uw_resolver *, void *, int, void *, int,
     int, char *);
 
@@ -105,8 +123,13 @@ struct resolver_cb_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 *);
+int                     sort_resolver_types(struct resolver_preference *);
+void                    setup_query(struct query_imsg *);
+struct running_query   *find_running_query(uint64_t);
+void                    try_resolver_timo(int, short, void *);
+int                     try_next_resolver(struct running_query *);
+
 int                     resolve(struct uw_resolver *, const char*, int, int,
                             void*, resolve_cb_t);
 void                    resolve_done(struct uw_resolver *, void *, int, void *,
@@ -137,22 +160,14 @@ void                       replace_forwarders(struct 
uw_forwarder_head *,
                             struct uw_forwarder_head *);
 void                    resolver_ref(struct uw_resolver *);
 void                    resolver_unref(struct uw_resolver *);
-struct uw_resolver     *best_resolver(void);
-int                     resolver_cmp(struct uw_resolver *,
-                            struct uw_resolver *);
+int                     resolver_cmp(const void *, const void *);
 void                    restart_resolvers(void);
 void                    show_status(enum uw_resolver_type, pid_t);
-void                    send_resolver_info(struct uw_resolver *, int, pid_t);
+void                    send_resolver_info(struct uw_resolver *, pid_t);
 void                    send_detailed_resolver_info(struct uw_resolver *,
                             pid_t);
 void                    send_resolver_histogram_info(struct uw_resolver *,
                             pid_t);
-void                    check_captive_portal(int);
-void                    check_captive_portal_timo(int, short, void *);
-int                     check_captive_portal_changed(struct uw_conf *,
-                            struct uw_conf *);
-void                    captive_portal_resolve_done(struct uw_resolver *,
-                            void *, int, void *, int, int, char *);
 void                    trust_anchor_resolve(void);
 void                    trust_anchor_timo(int, short, void *);
 void                    trust_anchor_resolve_done(struct uw_resolver *, void *,
@@ -161,16 +176,14 @@ void                       
replace_autoconf_forwarders(struct
                             imsg_rdns_proposal *);
 struct uw_forwarder    *find_forwarder(struct uw_forwarder_head *,
                             const char *);
+int64_t                         histogram_median(int64_t *);
 
 struct uw_conf                 *resolver_conf;
 struct imsgev                  *iev_frontend;
-struct imsgev                  *iev_captiveportal;
 struct imsgev                  *iev_main;
 struct uw_forwarder_head        autoconf_forwarder_list;
 struct uw_resolver             *resolvers[UW_RES_NONE];
-struct timeval                  captive_portal_check_tv =
-                                    {PORTAL_CHECK_SEC, 0};
-struct event                    captive_portal_check_ev;
+struct timespec                         last_network_change;
 
 struct event                    trust_anchor_timer;
 
@@ -178,8 +191,6 @@ static struct trust_anchor_head      trust_anchors, 
new_trust_anchors;
 
 struct event_base              *ev_base;
 
-enum captive_portal_state       captive_portal_state = PORTAL_UNCHECKED;
-
 static const char * const       as112_zones[] = {
        /* RFC1918 */
        "10.in-addr.arpa. transparent",
@@ -318,7 +329,7 @@ resolver(int debug, int verbose)
            setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid))
                fatal("can't drop privileges");
 
-       if (unveil(tls_default_ca_cert_file(), "r") == -1)
+       if (unveil(TLS_DEFAULT_CA_CERT_FILE, "r") == -1)
                fatal("unveil");
 
        if (pledge("stdio inet dns rpath recvfd", NULL) == -1)
@@ -347,14 +358,16 @@ 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);
        evtimer_set(&trust_anchor_timer, trust_anchor_timo, NULL);
 
+       clock_gettime(CLOCK_MONOTONIC, &last_network_change);
+
        new_recursor();
 
        TAILQ_INIT(&autoconf_forwarder_list);
        TAILQ_INIT(&trust_anchors);
        TAILQ_INIT(&new_trust_anchors);
+       TAILQ_INIT(&running_queries);
 
        event_dispatch();
 
@@ -369,15 +382,12 @@ resolver_shutdown(void)
        /* 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");
@@ -398,14 +408,6 @@ resolver_imsg_compose_frontend(int type, pid_t pid, void 
*data,
            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)
 {
@@ -413,11 +415,10 @@ resolver_dispatch_frontend(int fd, short event, void 
*bula)
        struct imsgbuf                  *ibuf;
        struct imsg                      imsg;
        struct query_imsg               *query_imsg;
-       struct uw_resolver              *res;
        enum uw_resolver_type            type;
        ssize_t                          n;
        int                              shut = 0, verbose;
-       int                              update_resolvers;
+       int                              update_resolvers, i;
        char                            *ta;
 
        ibuf = &iev->ibuf;
@@ -468,23 +469,7 @@ resolver_dispatch_frontend(int fd, short event, void *bula)
                        log_debug("%s: IMSG_QUERY[%llu], qname: %s, t: %d, "
                            "c: %d", __func__, query_imsg->id,
                            query_imsg->qname, query_imsg->t, query_imsg->c);
-
-                       res = best_resolver();
-
-                       if (res == NULL) {
-                               log_warnx("can't find working resolver");
-                               free(query_imsg);
-                               break;
-                       }
-
-                       log_debug("%s: choosing %s", __func__,
-                           uw_resolver_type_str[res->type]);
-
-                       clock_gettime(CLOCK_MONOTONIC, &query_imsg->tp);
-
-                       if (resolve(res, query_imsg->qname, query_imsg->t,
-                           query_imsg->c, query_imsg, resolve_done) != 0)
-                               free(query_imsg);
+                       setup_query(query_imsg);
                        break;
                case IMSG_CTL_STATUS:
                        if (IMSG_DATA_SIZE(imsg) != sizeof(type))
@@ -493,9 +478,6 @@ 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;
                case IMSG_NEW_TA:
                        /* make sure this is a string */
                        ((char *)imsg.data)[IMSG_DATA_SIZE(imsg) - 1] = '\0';
@@ -517,7 +499,15 @@ resolver_dispatch_frontend(int fd, short event, void *bula)
                        }
                        break;
                case IMSG_NETWORK_CHANGED:
+                       clock_gettime(CLOCK_MONOTONIC, &last_network_change);
                        schedule_recheck_all_resolvers();
+                       for (i = 0; i < UW_RES_NONE; i++) {
+                               if (resolvers[i] == NULL)
+                                       continue;
+                               memset(resolvers[i]->latest_histogram, 0,
+                                   sizeof(resolvers[i]->latest_histogram));
+                       }
+
                        break;
                case IMSG_REPLACE_DNS:
                        if (IMSG_DATA_SIZE(imsg) !=
@@ -543,70 +533,6 @@ resolver_dispatch_frontend(int fd, short event, void *bula)
        }
 }
 
-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_DATA_SIZE(imsg) !=
-                           sizeof(captive_portal_state))
-                               fatalx("%s: IMSG_CAPTIVEPORTAL_STATE wrong "
-                                   "length: %lu", __func__,
-                                   IMSG_DATA_SIZE(imsg));
-                       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);
-                               schedule_recheck_all_resolvers();
-                       }
-                       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_main(int fd, short event, void *bula)
 {
@@ -617,7 +543,6 @@ 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;
 
@@ -667,42 +592,12 @@ 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_STARTUP:
                        if (pledge("stdio inet dns rpath", NULL) == -1)
                                fatal("pledge");
                        break;
                case IMSG_RECONF_CONF:
-               case IMSG_RECONF_CAPTIVE_PORTAL_HOST:
-               case IMSG_RECONF_CAPTIVE_PORTAL_PATH:
-               case IMSG_RECONF_CAPTIVE_PORTAL_EXPECTED_RESPONSE:
                case IMSG_RECONF_BLOCKLIST_FILE:
                case IMSG_RECONF_FORWARDER:
                case IMSG_RECONF_DOT_FORWARDER:
@@ -718,8 +613,6 @@ resolver_dispatch_main(int fd, short event, void *bula)
                        dot_forwarders_changed = check_forwarders_changed(
                            &resolver_conf->uw_dot_forwarder_list,
                            &nconf->uw_dot_forwarder_list);
-                       captive_portal_changed = check_captive_portal_changed(
-                           resolver_conf, nconf);
                        merge_config(resolver_conf, nconf);
                        nconf = NULL;
                        if (forwarders_changed) {
@@ -730,14 +623,6 @@ resolver_dispatch_main(int fd, short event, void *bula)
                                log_debug("static DoT forwarders changed");
                                new_static_dot_forwarders();
                        }
-                       if (captive_portal_changed) {
-                               if (resolver_conf->captive_portal_auto)
-                                       check_captive_portal(1);
-                               else {
-                                       captive_portal_state = PORTAL_UNCHECKED;
-                                       schedule_recheck_all_resolvers();
-                               }
-                       }
                        break;
                default:
                        log_debug("%s: unexpected imsg %d", __func__,
@@ -755,6 +640,142 @@ resolver_dispatch_main(int fd, short event, void *bula)
        }
 }
 
+int
+sort_resolver_types(struct resolver_preference *dst)
+{
+       memcpy(dst, &resolver_conf->res_pref, sizeof(*dst));
+       /*
+        * Sort by resolver quality, validating > resolving etc.
+        * mergesort is stable and keeps the configured preference order
+        */
+
+       return mergesort(dst->types, dst->len, sizeof(dst->types[0]),
+           resolver_cmp);
+}
+
+void
+setup_query(struct query_imsg *query_imsg)
+{
+       struct running_query    *rq;
+       int                      i;
+
+       if (find_running_query(query_imsg->id) != NULL) {
+               free(query_imsg);
+               return;
+       }
+
+       if ((rq = calloc(1, sizeof(*rq))) == NULL) {
+               log_warnx(NULL);
+               free(query_imsg);
+               return;
+       }
+
+       clock_gettime(CLOCK_MONOTONIC, &rq->tp);
+       rq->query_imsg = query_imsg;
+       rq->next_resolver = 0;
+
+       if (sort_resolver_types(&rq->res_pref) == -1) {
+               log_warn("mergesort");
+               free(rq->query_imsg);
+               free(rq);
+               return;
+       }
+
+       for (i = 0; i < rq->res_pref.len; i++) {
+               if (resolvers[rq->res_pref.types[i]] == NULL)
+                   continue;
+               log_debug("%s: %s[%s] %lldms", __func__,
+                   uw_resolver_type_str[rq->res_pref.types[i]],
+                   uw_resolver_state_str[resolvers[rq->res_pref.types[i]]
+                   ->state], histogram_median(resolvers[rq->res_pref.types[i]]
+                   ->latest_histogram));
+       }
+
+       evtimer_set(&rq->timer_ev, try_resolver_timo, rq);
+
+       TAILQ_INSERT_TAIL(&running_queries, rq, entry);
+       try_next_resolver(rq);
+}
+
+struct running_query *
+find_running_query(uint64_t id)
+{
+       struct running_query    *rq;
+
+       TAILQ_FOREACH(rq, &running_queries, entry) {
+               if (rq->query_imsg->id == id)
+                       return rq;
+       }
+       return NULL;
+}
+
+void
+try_resolver_timo(int fd, short events, void *arg)
+{
+       struct running_query    *rq = arg;
+
+       try_next_resolver(rq);
+}
+
+int
+try_next_resolver(struct running_query *rq)
+{
+       struct uw_resolver      *res = NULL;
+       struct query_imsg       *query_imsg = NULL;
+       struct timespec          tp, elapsed;
+       struct timeval           tv = {0, 0};
+       int64_t                  ms;
+
+       while(rq->next_resolver < rq->res_pref.len &&
+           (res=resolvers[rq->res_pref.types[rq->next_resolver]]) == NULL)
+               rq->next_resolver++;
+
+       if (res == NULL) {
+               evtimer_del(&rq->timer_ev); /* we are not going to find one */
+               log_warnx("%s: could not find working resolver", __func__);
+               goto err;
+       }
+
+       rq->next_resolver++;
+       clock_gettime(CLOCK_MONOTONIC, &tp);
+       timespecsub(&tp, &rq->tp, &elapsed);
+       ms = elapsed.tv_sec * 1000 + elapsed.tv_nsec / 1000000;
+
+       log_debug("%s[+%lldms]: %s[%s]", __func__, ms,
+           uw_resolver_type_str[res->type], uw_resolver_state_str[res->state]);
+       if ((query_imsg = malloc(sizeof(*query_imsg))) == NULL) {
+               log_warnx("%s", __func__);
+               goto err;
+       }
+       memcpy(query_imsg, rq->query_imsg, sizeof(*query_imsg));
+       clock_gettime(CLOCK_MONOTONIC, &query_imsg->tp);
+
+       if (res->type == resolver_conf->res_pref.types[0])
+               tv.tv_usec = 1000 * (PREF_RESOLVER_MEDIAN_SKEW +
+                   histogram_median(res->latest_histogram));
+       else
+               tv.tv_usec = 1000 * histogram_median(res->latest_histogram);
+
+       evtimer_add(&rq->timer_ev, &tv);
+
+       if (resolve(res, query_imsg->qname, query_imsg->t,
+           query_imsg->c, query_imsg, resolve_done) != 0)
+               goto err;
+       rq->running++;
+
+       return 0;
+
+ err:
+       free(query_imsg);
+       if (rq->running == 0) {
+               TAILQ_REMOVE(&running_queries, rq, entry);
+               evtimer_del(&rq->timer_ev);
+               free(rq->query_imsg);
+               free(rq);
+       }
+       return 1;
+}
+
 int
 resolve(struct uw_resolver *res, const char* name, int rrtype, int rrclass,
     void *mydata, resolve_cb_t cb)
@@ -811,20 +832,25 @@ void
 resolve_done(struct uw_resolver *res, void *arg, int rcode,
     void *answer_packet, int answer_len, int sec, char *why_bogus)
 {
+       struct ub_result        *result = NULL;
+       sldns_buffer            *buf = NULL;
+       struct regional         *region = NULL;
        struct query_imsg       *query_imsg;
+       struct running_query    *rq;
        struct timespec          tp, elapsed;
        int64_t                  ms;
        size_t                   i;
+       int                      asr_pref_pos = -1;
        char                    *str;
+       char                     rcode_buf[16];
 
        clock_gettime(CLOCK_MONOTONIC, &tp);
 
        query_imsg = (struct query_imsg *)arg;
+       rq = find_running_query(query_imsg->id);
 
        timespecsub(&tp, &query_imsg->tp, &elapsed);
 
-       log_debug("elapsed: %lld.%ld", elapsed.tv_sec, elapsed.tv_nsec);
-
        ms = elapsed.tv_sec * 1000 + elapsed.tv_nsec / 1000000;
 
        for (i = 0; i < nitems(histogram_limits); i++) {
@@ -833,14 +859,10 @@ resolve_done(struct uw_resolver *res, void *arg, int 
rcode,
        }
        if (i == nitems(histogram_limits))
                log_debug("histogram bucket error");
-       else
+       else {
                res->histogram[i]++;
-
-       log_debug("%s: ref_cnt: %d, elapsed: %lldms, "
-           "histogram: %lld - %lld", __func__, res->ref_cnt, ms,
-           histogram_limits[i], res->histogram[i]);
-
-       log_debug("%s: rcode: %d", __func__, rcode);
+               res->latest_histogram[i]++;
+       }
 
        if (answer_len < LDNS_HEADER_SIZE) {
                log_warnx("bad packet: too short");
@@ -853,32 +875,122 @@ resolve_done(struct uw_resolver *res, void *arg, int 
rcode,
                goto servfail;
        }
 
+       if ((result = calloc(1, sizeof(*result))) == NULL)
+               goto servfail;
+       if ((buf = sldns_buffer_new(answer_len)) == NULL)
+               goto servfail;
+       if ((region = regional_create()) == NULL)
+               goto servfail;
+
+       result->rcode = LDNS_RCODE_SERVFAIL;
+
+       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;
+       result->answer_len = 0;
+
+       sldns_wire2str_rcode_buf(result->rcode, rcode_buf, sizeof(rcode_buf));
+       log_debug("%s[%s]: rcode: %s[%d], elapsed: %lldms, "
+           "histogram: %lld - %lld", __func__, uw_resolver_type_str[res->type],
+           rcode_buf, result->rcode, ms, histogram_limits[i],
+           res->histogram[i]);
+
+       if (result->rcode == LDNS_RCODE_NXDOMAIN && res->type != UW_RES_ASR) {
+               timespecsub(&tp, &last_network_change, &elapsed);
+               if (elapsed.tv_sec < DOUBT_NXDOMAIN_SEC) {
+                       /*
+                        * Doubt NXDOMAIN if we just switched networks,
+                        * we might be behind a captive portal.
+                        */
+                       log_debug("%s: doubt NXDOMAIN from %s", __func__,
+                           uw_resolver_type_str[res->type]);
+                       if (rq) {
+                               /* search for ASR */
+                               for (i = 0; i < (size_t)rq->res_pref.len; i++)
+                                       if (rq->res_pref.types[i] ==
+                                           UW_RES_ASR) {
+                                               asr_pref_pos = i;
+                                               break;
+                                       }
+
+                               if (asr_pref_pos != -1) {
+                                       /* go to ASR if not yet scheduled */
+                                       if (asr_pref_pos >= rq->next_resolver &&
+                                           resolvers[UW_RES_ASR] != NULL) {
+                                               rq->next_resolver =
+                                                   asr_pref_pos;
+                                               goto retry;
+                                       } else
+                                               goto out;
+                               }
+                               log_debug("%s: answering NXDOMAIN, couldn't "
+                                   "find working ASR", __func__);
+                       }
+               } else
+                       log_debug("%s: answering NXDOMAIN, network change "
+                           "%llds ago", __func__, elapsed.tv_sec);
+       }
+
        if ((str = sldns_wire2str_pkt(answer_packet, answer_len)) != NULL) {
                log_debug("%s", str);
                free(str);
        }
 
+       if (result->rcode == LDNS_RCODE_SERVFAIL)
+               goto servfail;
+
        query_imsg->err = 0;
 
        if (res->state == VALIDATING)
                query_imsg->bogus = sec == BOGUS;
        else
                query_imsg->bogus = 0;
-       resolver_imsg_compose_frontend(IMSG_ANSWER_HEADER, 0, query_imsg,
-           sizeof(*query_imsg));
 
-       /* XXX imsg overflow */
-       resolver_imsg_compose_frontend(IMSG_ANSWER, 0,
-           answer_packet, answer_len);
+       if (rq) {
+               rq->running--;
+               resolver_imsg_compose_frontend(IMSG_ANSWER_HEADER, 0,
+                   query_imsg, sizeof(*query_imsg));
+
+               /* XXX imsg overflow */
+               resolver_imsg_compose_frontend(IMSG_ANSWER, 0, answer_packet,
+                   answer_len);
+
+               TAILQ_REMOVE(&running_queries, rq, entry);
+               evtimer_del(&rq->timer_ev);
+               free(rq->query_imsg);
+               free(rq);
+       }
 
        free(query_imsg);
+       sldns_buffer_free(buf);
+       regional_destroy(region);
+       ub_resolve_free(result);
+
        return;
 
-servfail:
-       query_imsg->err = -4; /* UB_SERVFAIL */
-       resolver_imsg_compose_frontend(IMSG_ANSWER_HEADER, 0, query_imsg,
-           sizeof(*query_imsg));
+ servfail:
+       if (rq) {
+               rq->running--;
+               if (try_next_resolver(rq) != 0 && rq->running == 0) {
+                       /* we are the last one, send SERVFAIL */
+                       query_imsg->err = -4; /* UB_SERVFAIL */
+                       resolver_imsg_compose_frontend(IMSG_ANSWER_HEADER, 0,
+                           query_imsg, sizeof(*query_imsg));
+               }
+       }
+       goto out;
+ retry:
+       if (rq) {
+               rq->running--;
+               try_next_resolver(rq);
+       }
+ out:
        free(query_imsg);
+       sldns_buffer_free(buf);
+       regional_destroy(region);
+       ub_resolve_free(result);
 }
 
 void
@@ -1087,7 +1199,7 @@ create_resolver(enum uw_resolver_type type, int oppdot)
                        set_forwarders_oppdot(res, &autoconf_forwarder_list,
                            853);
                        ub_ctx_set_option(res->ctx, "tls-cert-bundle:",
-                           tls_default_ca_cert_file());
+                           TLS_DEFAULT_CA_CERT_FILE);
                        ub_ctx_set_tls(res->ctx, 1);
                } else {
                        set_forwarders_oppdot(res, &autoconf_forwarder_list,
@@ -1100,7 +1212,7 @@ create_resolver(enum uw_resolver_type type, int oppdot)
                        set_forwarders_oppdot(res,
                            &resolver_conf->uw_forwarder_list, 853);
                        ub_ctx_set_option(res->ctx, "tls-cert-bundle:",
-                           tls_default_ca_cert_file());
+                           TLS_DEFAULT_CA_CERT_FILE);
                        ub_ctx_set_tls(res->ctx, 1);
                } else
                        set_forwarders_oppdot(res,
@@ -1109,7 +1221,7 @@ create_resolver(enum uw_resolver_type type, int oppdot)
        case UW_RES_DOT:
                set_forwarders(res, &resolver_conf->uw_dot_forwarder_list);
                ub_ctx_set_option(res->ctx, "tls-cert-bundle:",
-                   tls_default_ca_cert_file());
+                   TLS_DEFAULT_CA_CERT_FILE);
                ub_ctx_set_tls(res->ctx, 1);
                break;
        default:
@@ -1471,75 +1583,40 @@ replace_forwarders(struct uw_forwarder_head *new_list, 
struct
        }
 }
 
-struct uw_resolver*
-best_resolver(void)
-{
-       struct uw_resolver      *res = NULL;
-       int                      i;
-
-       log_debug("%s: %s: %s, %s: %s%s, %s: %s%s, %s: %s, %s: %s, "
-           "captive_portal: %s",
-           __func__,
-           uw_resolver_type_str[UW_RES_RECURSOR], resolvers[UW_RES_RECURSOR]
-           != NULL ? uw_resolver_state_str[resolvers[UW_RES_RECURSOR]->state]
-           : "NA",
-           uw_resolver_type_str[UW_RES_DHCP], resolvers[UW_RES_DHCP] != NULL ?
-           uw_resolver_state_str[resolvers[UW_RES_DHCP]->state] : "NA",
-           resolvers[UW_RES_DHCP] != NULL &&
-           resolvers[UW_RES_DHCP]->oppdot ? " (OppDot)" : "",
-           uw_resolver_type_str[UW_RES_FORWARDER],
-           resolvers[UW_RES_FORWARDER] != NULL ?
-           uw_resolver_state_str[resolvers[UW_RES_FORWARDER]->state] : "NA",
-           resolvers[UW_RES_FORWARDER] != NULL &&
-           resolvers[UW_RES_FORWARDER]->oppdot ? " (OppDot)" : "",
-           uw_resolver_type_str[UW_RES_DOT],
-           resolvers[UW_RES_DOT] != NULL ?
-           uw_resolver_state_str[resolvers[UW_RES_DOT]->state] : "NA",
-           uw_resolver_type_str[UW_RES_ASR],
-           resolvers[UW_RES_ASR] != NULL ?
-           uw_resolver_state_str[resolvers[UW_RES_ASR]->state] : "NA",
-           captive_portal_state_str[captive_portal_state]);
-
-       if (captive_portal_state == PORTAL_UNKNOWN || captive_portal_state ==
-           BEHIND) {
-               if (resolvers[UW_RES_ASR] != NULL && resolvers[UW_RES_ASR]->
-                    state != DEAD) {
-                       res = resolvers[UW_RES_ASR];
-                       goto out;
-               }
-       }
-
-       res = resolvers[resolver_conf->res_pref[0]];
-
-       for (i = 1; i < resolver_conf->res_pref_len; i++)
-               if (resolver_cmp(res,
-                   resolvers[resolver_conf->res_pref[i]]) < 0)
-                       res = resolvers[resolver_conf->res_pref[i]];
-out:
-       if (res != NULL)
-               log_debug("%s: %s state: %s%s", __func__,
-                   uw_resolver_type_str[res->type],
-                   uw_resolver_state_str[res->state],
-                   res->oppdot ? " (OppDoT)" : "");
-       else
-               log_debug("%s: non found", __func__);
-
-       return (res);
-}
-
 int
-resolver_cmp(struct uw_resolver *a, struct uw_resolver *b)
+resolver_cmp(const void *_a, const void *_b)
 {
-       if (a == NULL && b == NULL)
+       const enum uw_resolver_type      a = *(const enum uw_resolver_type *)_a;
+       const enum uw_resolver_type      b = *(const enum uw_resolver_type *)_b;
+       int64_t                          a_median, b_median;
+
+       if (resolvers[a] == NULL && resolvers[b] == NULL)
                return 0;
 
-       if (b == NULL)
+       if (resolvers[b] == NULL)
+               return -1;
+
+       if (resolvers[a] == NULL)
                return 1;
 
-       if (a == NULL)
+       if (resolvers[a]->state < resolvers[b]->state)
+               return 1;
+       else if (resolvers[a]->state > resolvers[b]->state)
                return -1;
-
-       return (a->state < b->state ? -1 : a->state > b->state ? 1 : 0);
+       else {
+               a_median = histogram_median(resolvers[a]->latest_histogram);
+               b_median = histogram_median(resolvers[b]->latest_histogram);
+               if (resolvers[a]->type == resolver_conf->res_pref.types[0])
+                       a_median -= PREF_RESOLVER_MEDIAN_SKEW;
+               else if (resolvers[b]->type == resolver_conf->res_pref.types[0])
+                       b_median -= PREF_RESOLVER_MEDIAN_SKEW;
+               if (a_median < b_median)
+                       return -1;
+               else if (a_median > b_median)
+                       return 1;
+               else
+                       return 0;
+       }
 }
 
 void
@@ -1561,22 +1638,19 @@ restart_resolvers(void)
 void
 show_status(enum uw_resolver_type type, pid_t pid)
 {
-       struct uw_resolver              *best;
        struct uw_forwarder             *uw_forwarder;
        struct ctl_forwarder_info        cfi;
+       struct resolver_preference       res_pref;
        int                              i;
 
-       best = best_resolver();
-
        switch(type) {
        case UW_RES_NONE:
-               resolver_imsg_compose_frontend(IMSG_CTL_CAPTIVEPORTAL_INFO,
-                   pid, &captive_portal_state, sizeof(captive_portal_state));
-               for (i = 0; i < resolver_conf->res_pref_len; i++)
-                       send_resolver_info(
-                           resolvers[resolver_conf->res_pref[i]],
-                           resolvers[resolver_conf->res_pref[i]] ==
-                           best, pid);
+               if (sort_resolver_types(&res_pref) == -1)
+                       log_warn("mergesort");
+
+               for (i = 0; i < resolver_conf->res_pref.len; i++)
+                       send_resolver_info(resolvers[res_pref.types[i]], pid);
+
                TAILQ_FOREACH(uw_forwarder, &autoconf_forwarder_list, entry) {
                        memset(&cfi, 0, sizeof(cfi));
                        cfi.if_index = uw_forwarder->if_index;
@@ -1594,8 +1668,7 @@ show_status(enum uw_resolver_type type, pid_t pid)
        case UW_RES_FORWARDER:
        case UW_RES_DOT:
        case UW_RES_ASR:
-               send_resolver_info(resolvers[type], resolvers[type] == best,
-                   pid);
+               send_resolver_info(resolvers[type], pid);
                send_detailed_resolver_info(resolvers[type], pid);
                break;
        default:
@@ -1606,7 +1679,7 @@ show_status(enum uw_resolver_type type, pid_t pid)
 }
 
 void
-send_resolver_info(struct uw_resolver *res, int selected, pid_t pid)
+send_resolver_info(struct uw_resolver *res, pid_t pid)
 {
        struct ctl_resolver_info         cri;
 
@@ -1615,7 +1688,6 @@ send_resolver_info(struct uw_resolver *res, int selected, 
pid_t pid)
 
        cri.state = res->state;
        cri.type = res->type;
-       cri.selected = selected;
        cri.oppdot = res->oppdot;
        resolver_imsg_compose_frontend(IMSG_CTL_RESOLVER_INFO, pid, &cri,
            sizeof(cri));
@@ -1648,154 +1720,18 @@ send_resolver_histogram_info(struct uw_resolver *res, 
pid_t pid)
                    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 uw_resolver      *res;
-
-       log_debug("%s", __func__);
-
-       if (resolver_conf->captive_portal_host == NULL) {
-               log_debug("%s: no captive portal url configured", __func__);
-               captive_portal_state = PORTAL_UNCHECKED;
-               schedule_recheck_all_resolvers();
-               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;
-
-       if ((res = best_resolver()) == NULL)
-               return;
-
-       resolve(res, resolver_conf->captive_portal_host,
-           LDNS_RR_TYPE_A, LDNS_RR_CLASS_IN, NULL,
-           captive_portal_resolve_done);
-}
-
-void
-captive_portal_resolve_done(struct uw_resolver *res, void *arg, int rcode,
-    void *answer_packet, int answer_len, int sec, char *why_bogus)
-{
-       struct ub_result        *result = NULL;
-       sldns_buffer            *buf = NULL;
-       struct regional         *region = NULL;
-       struct in_addr          *in;
-       int                      i;
-       char                    *str, rdata_buf[sizeof("xxx.xxx.xxx.xxx")];
-
-       if (answer_len < LDNS_HEADER_SIZE) {
-               log_warnx("bad packet: too short");
-               goto out;
-       }
-
-       if ((result = calloc(1, sizeof(*result))) == NULL)
-               goto out;
-
-       log_debug("%s: rcode: %d", __func__, rcode);
-       if ((str = sldns_wire2str_pkt(answer_packet, answer_len)) != NULL) {
-               log_debug("%s", str);
-               free(str);
-       }
-
-       if ((buf = sldns_buffer_new(answer_len)) == NULL)
-               goto out;
-       if ((region = regional_create()) == NULL)
-               goto out;
-       result->rcode = LDNS_RCODE_SERVFAIL;
-
-       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;
-       result->answer_len = 0;
-
-       if (result->rcode != LDNS_RCODE_NOERROR) {
-               log_debug("%s: result->rcode: %d", __func__,
-                   result->rcode);
-               goto out;
-       }
-
-       i = 0;
-       while(result->data[i] != NULL) {
-               if (result->len[i] == 4) {
-                       in = (struct in_addr*) result->data[i];
-                       log_debug("%s: %s", __func__, inet_ntop(AF_INET,
-                           in, rdata_buf, sizeof(rdata_buf)));
-                       resolver_imsg_compose_main(
-                           IMSG_CONNECT_CAPTIVE_PORTAL_HOST, 0, in,
-                           sizeof(*in));
-               }
-               i++;
-       }
- out:
-       sldns_buffer_free(buf);
-       regional_destroy(region);
-       ub_resolve_free(result);
-}
-
-int
-check_captive_portal_changed(struct uw_conf *a, struct uw_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);
-}
-
 void
 trust_anchor_resolve(void)
 {
-       struct uw_resolver      *res;
-       struct timeval           tv = {TRUST_ANCHOR_RETRY_INTERVAL, 0};
+       struct resolver_preference       res_pref;
+       struct uw_resolver              *res;
+       struct timeval                   tv = {TRUST_ANCHOR_RETRY_INTERVAL, 0};
 
        log_debug("%s", __func__);
+       if (sort_resolver_types(&res_pref) == -1)
+               log_warn("mergesort");
 
-       res = best_resolver();
+       res = resolvers[res_pref.types[0]];
 
        if (res == NULL || res->state < VALIDATING)
                goto err;
@@ -1994,8 +1930,6 @@ replace_autoconf_forwarders(struct imsg_rdns_proposal 
*rdns_proposal)
                    &autoconf_forwarder_list);
                new_forwarders(0);
                new_asr_forwarders();
-               if (resolver_conf->captive_portal_auto)
-                       check_captive_portal(1);
                log_debug("%s: forwarders changed", __func__);
        } else {
                log_debug("%s: forwarders didn't change", __func__);
@@ -2016,3 +1950,21 @@ find_forwarder(struct uw_forwarder_head *list, const 
char *name) {
        }
        return NULL;
 }
+
+int64_t
+histogram_median(int64_t *histogram)
+{
+       size_t   i;
+       int64_t  sample_count = 0, running_count = 0;
+
+       for (i = 1; i < nitems(histogram_limits); i++)
+               sample_count += histogram[i];
+
+       for (i = 1; i < nitems(histogram_limits); i++) {
+               running_count += histogram[i];
+               if (running_count >= sample_count / 2)
+                       break;
+       }
+
+       return histogram_limits[i];
+}
diff --git sbin/unwind/resolver.h sbin/unwind/resolver.h
index 9be94385c4c..f8e312c5d6c 100644
--- sbin/unwind/resolver.h
+++ sbin/unwind/resolver.h
@@ -49,7 +49,6 @@ static const int64_t          histogram_limits[] = {
 struct ctl_resolver_info {
        enum uw_resolver_state   state;
        enum uw_resolver_type    type;
-       int                      selected;
        int                      oppdot;
 };
 
@@ -62,4 +61,3 @@ struct ctl_forwarder_info {
 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 sbin/unwind/unwind.c sbin/unwind/unwind.c
index 87e3fa87ac3..b9c576e08fc 100644
--- sbin/unwind/unwind.c
+++ sbin/unwind/unwind.c
@@ -47,7 +47,6 @@
 #include "frontend.h"
 #include "resolver.h"
 #include "control.h"
-#include "captiveportal.h"
 
 #define        TRUST_ANCHOR_FILE       "/var/db/unwind.key"
 
@@ -60,28 +59,23 @@ 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 *,
-                   struct imsgbuf *);
+static int     main_imsg_send_ipc_sockets(struct imsgbuf *, struct imsgbuf *);
 static int     main_imsg_send_config(struct uw_conf *);
 
 int            main_reload(void);
 int            main_sendall(enum imsg_type, void *, uint16_t);
 void           open_ports(void);
 void           solicit_dns_proposals(void);
-void           connect_captive_portal_host(struct in_addr *);
 void           send_blocklist_fd(void);
 
 struct uw_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;
 
@@ -126,9 +120,8 @@ main(int argc, char *argv[])
 {
        struct event     ev_sigint, ev_sigterm, ev_sighup;
        int              ch, debug = 0, resolver_flag = 0, frontend_flag = 0;
-       int              captiveportal_flag = 0, frontend_routesock, rtfilter;
+       int              frontend_routesock, rtfilter;
        int              pipe_main2frontend[2], pipe_main2resolver[2];
-       int              pipe_main2captiveportal[2];
        int              control_fd, ta_fd;
        char            *csock, *saved_argv0;
 
@@ -142,11 +135,8 @@ main(int argc, char *argv[])
        if (saved_argv0 == NULL)
                saved_argv0 = "unwind";
 
-       while ((ch = getopt(argc, argv, "CdEFf:ns:v")) != -1) {
+       while ((ch = getopt(argc, argv, "dEFf:ns:v")) != -1) {
                switch (ch) {
-               case 'C':
-                       captiveportal_flag = 1;
-                       break;
                case 'd':
                        debug = 1;
                        break;
@@ -177,15 +167,13 @@ main(int argc, char *argv[])
 
        argc -= optind;
        argv += optind;
-       if (argc > 0 || (resolver_flag && frontend_flag && captiveportal_flag))
+       if (argc > 0 || (resolver_flag && frontend_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);
@@ -220,9 +208,6 @@ 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,
@@ -231,9 +216,6 @@ 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));
 
        uw_process = PROC_MAIN;
        log_procinit(log_procnames[uw_process]);
@@ -252,15 +234,12 @@ 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. */
        iev_frontend->events = EV_READ;
@@ -273,14 +252,8 @@ main(int argc, char *argv[])
            iev_resolver->events, iev_resolver->handler, iev_resolver);
        event_add(&iev_resolver->ev, NULL);
 
-       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))
+           &iev_resolver->ibuf))
                fatal("could not establish imsg links");
 
        if ((control_fd = control_init(csock)) == -1)
@@ -336,8 +309,6 @@ 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);
 
@@ -355,7 +326,6 @@ main_shutdown(void)
 
        free(iev_frontend);
        free(iev_resolver);
-       free(iev_captiveportal);
 
        log_info("terminating");
        exit(0);
@@ -394,9 +364,6 @@ 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";
@@ -480,7 +447,6 @@ main_dispatch_resolver(int fd, short event, void *bula)
        struct imsgev           *iev = bula;
        struct imsgbuf          *ibuf;
        struct imsg              imsg;
-       struct in_addr          *in;
        ssize_t                  n;
        int                      shut = 0;
 
@@ -499,61 +465,6 @@ main_dispatch_resolver(int fd, short event, void *bula)
                        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_CONNECT_CAPTIVE_PORTAL_HOST:
-                       if (IMSG_DATA_SIZE(imsg) != sizeof(*in))
-                               fatalx("%s: IMSG_CONNECT_CAPTIVE_PORTAL_HOST "
-                                   "wrong length: %lu", __func__,
-                                   IMSG_DATA_SIZE(imsg));
-                       in = (struct in_addr *)imsg.data;
-                       connect_captive_portal_host(in);
-                       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;
-       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("imsg_get");
@@ -600,23 +511,6 @@ main_imsg_compose_resolver(int type, pid_t pid, void 
*data, uint16_t datalen)
                    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)
 {
@@ -644,24 +538,14 @@ 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 *captiveportal_buf)
+    struct imsgbuf *resolver_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 (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);
@@ -669,20 +553,6 @@ main_imsg_send_ipc_sockets(struct imsgbuf *frontend_buf,
            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);
 }
 
@@ -713,27 +583,6 @@ main_imsg_send_config(struct uw_conf *xconf)
        /* Send fixed part of config to children. */
        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);
-       }
 
        if (xconf->blocklist_file != NULL) {
                if (main_sendall(IMSG_RECONF_BLOCKLIST_FILE,
@@ -771,9 +620,6 @@ main_sendall(enum imsg_type type, void *buf, uint16_t len)
                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);
 }
 
@@ -794,25 +640,9 @@ merge_config(struct uw_conf *conf, struct uw_conf *xconf)
                free(uw_forwarder);
        }
 
-       conf->res_pref_len = xconf->res_pref_len;
        memcpy(&conf->res_pref, &xconf->res_pref,
            sizeof(conf->res_pref));
 
-       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;
-
        free(conf->blocklist_file);
        conf->blocklist_file = xconf->blocklist_file;
        conf->blocklist_log = xconf->blocklist_log;
@@ -850,19 +680,13 @@ config_new_empty(void)
        if (xconf == NULL)
                fatal(NULL);
 
-       memcpy(&xconf->res_pref, &default_res_pref,
+       memcpy(&xconf->res_pref.types, &default_res_pref,
            sizeof(default_res_pref));
-       xconf->res_pref_len = 5;
+       xconf->res_pref.len = 5;
 
        TAILQ_INIT(&xconf->uw_forwarder_list);
        TAILQ_INIT(&xconf->uw_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);
 }
 
@@ -951,38 +775,6 @@ solicit_dns_proposals(void)
                log_warn("failed to send solicitation");
 }
 
-void
-connect_captive_portal_host(struct in_addr *in)
-{
-       struct sockaddr         *sa;
-       struct sockaddr_in       sin;
-       int                      httpsock;
-
-       sa = (struct sockaddr *)&sin;
-       memset(&sin, 0, sizeof(sin));
-       sin.sin_len = sizeof(sin);
-       sin.sin_family = AF_INET;
-       sin.sin_addr = *in;
-       sin.sin_port = htons(80);
-       log_debug("%s: ip_port: %s", __func__, ip_port(sa));
-
-       if ((httpsock = socket(AF_INET, SOCK_STREAM |
-           SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) == -1) {
-               log_warn("%s: socket", __func__);
-               return;
-       }
-       if (connect(httpsock, sa, sizeof(sin)) == -1) {
-               if (errno != EINPROGRESS) {
-                       log_warn("%s: connect", __func__);
-                       close(httpsock);
-                       return;
-               }
-       }
-
-       main_imsg_compose_captiveportal_fd(IMSG_HTTPSOCK, 0,
-           httpsock);
-}
-
 void
 send_blocklist_fd(void)
 {
@@ -1014,33 +806,9 @@ imsg_receive_config(struct imsg *imsg, struct uw_conf 
**xconf)
                        fatal(NULL);
                nconf = *xconf;
                memcpy(nconf, imsg->data, sizeof(struct uw_conf));
-               nconf->captive_portal_host = NULL;
-               nconf->captive_portal_path = NULL;
-               nconf->captive_portal_expected_response = NULL;
                TAILQ_INIT(&nconf->uw_forwarder_list);
                TAILQ_INIT(&nconf->uw_dot_forwarder_list);
                break;
-       case IMSG_RECONF_CAPTIVE_PORTAL_HOST:
-               /* make sure this is a string */
-               ((char *)imsg->data)[IMSG_DATA_SIZE(*imsg) - 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_DATA_SIZE(*imsg) - 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_DATA_SIZE(*imsg) - 1] = '\0';
-               if ((nconf->captive_portal_expected_response =
-                   strdup(imsg->data)) == NULL)
-                       fatal("%s: strdup", __func__);
-               break;
        case IMSG_RECONF_BLOCKLIST_FILE:
                /* make sure this is a string */
                ((char *)imsg->data)[IMSG_DATA_SIZE(*imsg) - 1] = '\0';
diff --git sbin/unwind/unwind.conf.5 sbin/unwind/unwind.conf.5
index 597c989b17d..6a659382a16 100644
--- sbin/unwind/unwind.conf.5
+++ sbin/unwind/unwind.conf.5
@@ -72,45 +72,6 @@ answers with a return code of
 With
 .Cm log
 blocked queries are logged.
-.It Ic captive portal Brq ...
-.Nm unwind
-can detect when it is running behind a
-.Dq captive portal
-by sending an HTTP request and checking the response against the
-configured expected response.
-The check is triggered when
-.Xr dhclient 8
-reports new nameservers.
-If the response does not match,
-.Nm unwind
-uses the DHCP provided nameservers and periodically re-checks if the user
-passed the captive portal.
-.Bl -tag -width Ds
-.It Ic auto Op Cm yes | no
-When
-.Ic auto
-is set to
-.Cm yes
-.Nm unwind
-automatically triggers a captive portal check
-when the network is changed.
-When set to
-.Cm no
-a captive portal check can be triggered by
-.Xr unwindctl 8 .
-The default is
-.Cm yes .
-.It Ic expected response Ar response
-The body of the HTTP response is compared to
-.Ar response .
-The default is the empty string.
-.It Ic expected status Ar status
-The expected HTTP status code.
-The default is 200.
-.It Ic url Ar URL
-URL to send HTTP queries to.
-This parameter is required.
-.El
 .It Ic forwarder Brq Ar address Oo Ic port Ar number Oc Oo Oo Ic 
authentication name Ar name Oc Ic DoT Oc ...
 A list of addresses of DNS name servers to forward queries to.
 .Ic port
diff --git sbin/unwind/unwind.h sbin/unwind/unwind.h
index 3dda5fc5136..eec3778ca1a 100644
--- sbin/unwind/unwind.h
+++ sbin/unwind/unwind.h
@@ -44,14 +44,12 @@ enum {
        PROC_MAIN,
        PROC_RESOLVER,
        PROC_FRONTEND,
-       PROC_CAPTIVEPORTAL,
 } uw_process;
 
 static const char * const log_procnames[] = {
        "main",
        "resolver",
        "frontend",
-       "captive portal",
 };
 
 enum uw_resolver_type {
@@ -83,11 +81,7 @@ 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_BLOCKLIST_FILE,
        IMSG_RECONF_FORWARDER,
        IMSG_RECONF_DOT_FORWARDER,
@@ -100,7 +94,6 @@ enum imsg_type {
        IMSG_STARTUP_DONE,
        IMSG_SOCKET_IPC_FRONTEND,
        IMSG_SOCKET_IPC_RESOLVER,
-       IMSG_SOCKET_IPC_CAPTIVEPORTAL,
        IMSG_QUERY,
        IMSG_ANSWER_HEADER,
        IMSG_ANSWER,
@@ -109,15 +102,12 @@ enum imsg_type {
        IMSG_CTL_RESOLVER_HISTOGRAM,
        IMSG_CTL_AUTOCONF_RESOLVER_INFO,
        IMSG_CTL_END,
-       IMSG_CTL_RECHECK_CAPTIVEPORTAL,
        IMSG_HTTPSOCK,
-       IMSG_CAPTIVEPORTAL_STATE,
        IMSG_TAFD,
        IMSG_NEW_TA,
        IMSG_NEW_TAS_ABORT,
        IMSG_NEW_TAS_DONE,
        IMSG_NETWORK_CHANGED,
-       IMSG_CONNECT_CAPTIVE_PORTAL_HOST,
        IMSG_BLFD,
        IMSG_REPLACE_DNS,
 };
@@ -130,17 +120,16 @@ struct uw_forwarder {
        uint16_t                                 port;
 };
 
+struct resolver_preference {
+       enum uw_resolver_type                    types[UW_RES_NONE];
+       int                                      len;
+};
+
 TAILQ_HEAD(uw_forwarder_head, uw_forwarder);
 struct uw_conf {
        struct uw_forwarder_head         uw_forwarder_list;
        struct uw_forwarder_head         uw_dot_forwarder_list;
-       enum uw_resolver_type            res_pref[UW_RES_NONE];
-       int                              res_pref_len;
-       char                            *captive_portal_host;
-       char                            *captive_portal_path;
-       char                            *captive_portal_expected_response;
-       int                              captive_portal_expected_status;
-       int                              captive_portal_auto;
+       struct resolver_preference       res_pref;
        char                            *blocklist_file;
        int                              blocklist_log;
 };
@@ -161,8 +150,6 @@ extern uint32_t      cmd_opts;
 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 uw_conf *, struct uw_conf *);
 void   imsg_event_add(struct imsgev *);
 int    imsg_compose_event(struct imsgev *, uint16_t, uint32_t, pid_t,
diff --git usr.sbin/unwindctl/parser.c usr.sbin/unwindctl/parser.c
index a130de587f7..ba2402d7936 100644
--- usr.sbin/unwindctl/parser.c
+++ usr.sbin/unwindctl/parser.c
@@ -51,13 +51,11 @@ 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}
 };
 
@@ -78,11 +76,6 @@ 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 usr.sbin/unwindctl/unwindctl.8 usr.sbin/unwindctl/unwindctl.8
index b1b26112421..1c1ff9656cb 100644
--- usr.sbin/unwindctl/unwindctl.8
+++ usr.sbin/unwindctl/unwindctl.8
@@ -53,8 +53,6 @@ Enable verbose logging.
 Enable very noisy debug logging.
 .It Cm reload
 Reload the configuration file.
-.It Cm recheck portal
-Run the captive portal detection.
 .It Cm status Op Cm recursor | dhcp | DoT | forwarder
 Show a status summary.
 If one of
diff --git usr.sbin/unwindctl/unwindctl.c usr.sbin/unwindctl/unwindctl.c
index 791a3d5fa15..6658941ce52 100644
--- usr.sbin/unwindctl/unwindctl.c
+++ usr.sbin/unwindctl/unwindctl.c
@@ -39,7 +39,6 @@
 #include <unistd.h>
 
 #include "unwind.h"
-#include "captiveportal.h"
 #include "frontend.h"
 #include "resolver.h"
 #include "parser.h"
@@ -142,12 +141,6 @@ 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 = UW_RES_RECURSOR;
                imsg_compose(ibuf, IMSG_CTL_STATUS, 0, 0, -1, &type,
@@ -225,34 +218,16 @@ show_status_msg(struct imsg *imsg)
        static int                       header, autoconf_forwarders;
        struct ctl_resolver_info        *cri;
        struct ctl_forwarder_info       *cfi;
-       enum captive_portal_state        captive_portal_state;
        char                             ifnamebuf[IFNAMSIZ];
        char                            *if_name;
 
-       if (imsg->hdr.type != IMSG_CTL_CAPTIVEPORTAL_INFO && !header++)
-               printf("%8s %16s %s\n", "selected", "type", "status");
+       if (!header++)
+               printf("preference:\n");
 
        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%s\n", cri->selected ? "*" : " ",
-                   uw_resolver_type_str[cri->type],
+               printf("%-10s %s%s\n", uw_resolver_type_str[cri->type],
                    uw_resolver_state_str[cri->state],
                    cri->oppdot ? " (opportunistic DoT)" : "");
                break;


-- 
I'm not entirely sure you are real.

Reply via email to