s4:heimdal: import lorikeet-heimdal-202201172009 (commit 5a0b45cd723628b3690ea848548b...
[samba.git] / source4 / heimdal / lib / krb5 / send_to_kdc.c
index ee8f6aaff1f50e3e7d1b801f36c81986fab5270e..704b095b5355bab4fe10ca20230272fd8f4220d7 100644 (file)
@@ -3,6 +3,8 @@
  * (Royal Institute of Technology, Stockholm, Sweden).
  * All rights reserved.
  *
+ * Portions Copyright (c) 2010 - 2013 Apple Inc. All rights reserved.
+ *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
  * are met:
 #include "krb5_locl.h"
 #include "send_to_kdc_plugin.h"
 
-struct send_to_kdc {
-    krb5_send_to_kdc_func func;
+/**
+ * @section send_to_kdc Locating and sending packets to the KDC
+ *
+ * The send to kdc code is responsible to request the list of KDC from
+ * the locate-kdc subsystem and then send requests to each of them.
+ *
+ * - Each second a new hostname is tried.
+ * - If the hostname have several addresses, the first will be tried
+ *   directly then in turn the other will be tried every 3 seconds
+ *   (host_timeout).
+ * - UDP requests are tried 3 times, and it tried with a individual timeout of kdc_timeout / 3.
+ * - TCP and HTTP requests are tried 1 time.
+ *
+ *  Total wait time shorter then (number of addresses * 3) + kdc_timeout seconds.
+ *
+ */
+
+static int
+init_port(const char *s, int fallback)
+{
+    int tmp;
+
+    if (s && sscanf(s, "%d", &tmp) == 1)
+        return htons(tmp);
+    return fallback;
+}
+
+struct send_via_plugin_s {
+    krb5_const_realm realm;
+    krb5_krbhst_info *hi;
+    time_t timeout;
+    const krb5_data *send_data;
+    krb5_data *receive;
+};
+    
+
+static krb5_error_code KRB5_LIB_CALL
+kdccallback(krb5_context context, const void *plug, void *plugctx, void *userctx)
+{
+    const krb5plugin_send_to_kdc_ftable *service = (const krb5plugin_send_to_kdc_ftable *)plug;
+    struct send_via_plugin_s *ctx = userctx;
+
+    if (service->send_to_kdc == NULL)
+       return KRB5_PLUGIN_NO_HANDLE;
+    return service->send_to_kdc(context, plugctx, ctx->hi, ctx->timeout,
+                               ctx->send_data, ctx->receive);
+}
+
+static krb5_error_code KRB5_LIB_CALL
+realmcallback(krb5_context context, const void *plug, void *plugctx, void *userctx)
+{
+    const krb5plugin_send_to_kdc_ftable *service = (const krb5plugin_send_to_kdc_ftable *)plug;
+    struct send_via_plugin_s *ctx = userctx;
+
+    if (service->send_to_realm == NULL)
+       return KRB5_PLUGIN_NO_HANDLE;
+    return service->send_to_realm(context, plugctx, ctx->realm, ctx->timeout,
+                                 ctx->send_data, ctx->receive);
+}
+
+static const char *send_to_kdc_plugin_deps[] = { "krb5", NULL };
+
+static struct heim_plugin_data
+send_to_kdc_plugin_data = {
+    "krb5",
+    KRB5_PLUGIN_SEND_TO_KDC,
+    KRB5_PLUGIN_SEND_TO_KDC_VERSION_0,
+    send_to_kdc_plugin_deps,
+    krb5_get_instance
+};
+
+static krb5_error_code
+kdc_via_plugin(krb5_context context,
+              krb5_krbhst_info *hi,
+              time_t timeout,
+              const krb5_data *send_data,
+              krb5_data *receive)
+{
+    struct send_via_plugin_s userctx;
+
+    userctx.realm = NULL;
+    userctx.hi = hi;
+    userctx.timeout = timeout;
+    userctx.send_data = send_data;
+    userctx.receive = receive;
+
+    return _krb5_plugin_run_f(context, &send_to_kdc_plugin_data, 0,
+                             &userctx, kdccallback);
+}
+
+static krb5_error_code
+realm_via_plugin(krb5_context context,
+                krb5_const_realm realm,
+                time_t timeout,
+                const krb5_data *send_data,
+                krb5_data *receive)
+{
+    struct send_via_plugin_s userctx;
+
+    userctx.realm = realm;
+    userctx.hi = NULL;
+    userctx.timeout = timeout;
+    userctx.send_data = send_data;
+    userctx.receive = receive;
+
+    return _krb5_plugin_run_f(context, &send_to_kdc_plugin_data, 0,
+                             &userctx, realmcallback);
+}
+
+struct krb5_sendto_ctx_data {
+    int flags;
+    int type;
+    krb5_sendto_ctx_func func;
     void *data;
+    char *hostname;
+    char *sitename;
+    krb5_krbhst_handle krbhst;
+
+    /* context2 */
+    const krb5_data *send_data;
+    krb5_data response;
+    heim_array_t hosts;
+    int stateflags;
+#define KRBHST_COMPLETED       1
+
+    /* prexmit */
+    krb5_sendto_prexmit prexmit_func;
+    void *prexmit_ctx;
+
+    /* stats */
+    struct {
+       struct timeval start_time;
+       struct timeval name_resolution;
+       struct timeval krbhst;
+       unsigned long sent_packets;
+       unsigned long num_hosts;
+    } stats;
+    unsigned int stid;
 };
 
-/*
- * send the data in `req' on the socket `fd' (which is datagram iff udp)
- * waiting `tmout' for a reply and returning the reply in `rep'.
- * iff limit read up to this many bytes
- * returns 0 and data in `rep' if succesful, otherwise -1
- */
+static void
+dealloc_sendto_ctx(void *ptr)
+{
+    krb5_sendto_ctx ctx = (krb5_sendto_ctx)ptr;
+    if (ctx->hostname)
+       free(ctx->hostname);
+    if (ctx->sitename)
+       free(ctx->sitename);
+    heim_release(ctx->hosts);
+    heim_release(ctx->krbhst);
+}
 
-static int
-recv_loop (krb5_socket_t fd,
-          time_t tmout,
-          int udp,
-          size_t limit,
-          krb5_data *rep)
+KRB5_LIB_FUNCTION krb5_error_code KRB5_LIB_CALL
+krb5_sendto_ctx_alloc(krb5_context context, krb5_sendto_ctx *ctx)
 {
-     fd_set fdset;
-     struct timeval timeout;
-     int ret;
-     int nbytes;
+    *ctx = heim_alloc(sizeof(**ctx), "sendto-context", dealloc_sendto_ctx);
+    if (*ctx == NULL)
+       return krb5_enomem(context);
+    (*ctx)->hosts = heim_array_create();
 
-#ifndef NO_LIMIT_FD_SETSIZE
-     if (fd >= FD_SETSIZE) {
-        return -1;
-     }
-#endif
+    return 0;
+}
 
-     krb5_data_zero(rep);
-     do {
-        FD_ZERO(&fdset);
-        FD_SET(fd, &fdset);
-        timeout.tv_sec  = tmout;
-        timeout.tv_usec = 0;
-        ret = select (fd + 1, &fdset, NULL, NULL, &timeout);
-        if (ret < 0) {
-            if (errno == EINTR)
-                continue;
-            return -1;
-        } else if (ret == 0) {
-            return 0;
-        } else {
-            void *tmp;
-
-            if (rk_SOCK_IOCTL (fd, FIONREAD, &nbytes) < 0) {
-                krb5_data_free (rep);
-                return -1;
-            }
-            if(nbytes <= 0)
-                return 0;
-
-            if (limit)
-                nbytes = min((size_t)nbytes, limit - rep->length);
-
-            tmp = realloc (rep->data, rep->length + nbytes);
-            if (tmp == NULL) {
-                krb5_data_free (rep);
-                return -1;
-            }
-            rep->data = tmp;
-            ret = recv (fd, (char*)tmp + rep->length, nbytes, 0);
-            if (ret < 0) {
-                krb5_data_free (rep);
-                return -1;
-            }
-            rep->length += ret;
-        }
-     } while(!udp && (limit == 0 || rep->length < limit));
-     return 0;
+KRB5_LIB_FUNCTION void KRB5_LIB_CALL
+krb5_sendto_ctx_add_flags(krb5_sendto_ctx ctx, int flags)
+{
+    ctx->flags |= flags;
 }
 
-/*
- * Send kerberos requests and receive a reply on a udp or any other kind
- * of a datagram socket.  See `recv_loop'.
- */
+KRB5_LIB_FUNCTION int KRB5_LIB_CALL
+krb5_sendto_ctx_get_flags(krb5_sendto_ctx ctx)
+{
+    return ctx->flags;
+}
 
-static int
-send_and_recv_udp(krb5_socket_t fd,
-                 time_t tmout,
-                 const krb5_data *req,
-                 krb5_data *rep)
+KRB5_LIB_FUNCTION void KRB5_LIB_CALL
+krb5_sendto_ctx_set_type(krb5_sendto_ctx ctx, int type)
 {
-    if (send (fd, req->data, req->length, 0) < 0)
-       return -1;
+    ctx->type = type;
+}
 
-    return recv_loop(fd, tmout, 1, 0, rep);
+KRB5_LIB_FUNCTION void KRB5_LIB_CALL
+krb5_sendto_ctx_set_func(krb5_sendto_ctx ctx,
+                        krb5_sendto_ctx_func func,
+                        void *data)
+{
+    ctx->func = func;
+    ctx->data = data;
 }
 
-/*
- * `send_and_recv' for a TCP (or any other stream) socket.
- * Since there are no record limits on a stream socket the protocol here
- * is to prepend the request with 4 bytes of its length and the reply
- * is similarly encoded.
- */
+KRB5_LIB_FUNCTION void KRB5_LIB_CALL
+_krb5_sendto_ctx_set_prexmit(krb5_sendto_ctx ctx,
+                            krb5_sendto_prexmit prexmit,
+                            void *data)
+{
+    ctx->prexmit_func = prexmit;
+    ctx->prexmit_ctx = data;
+}
 
-static int
-send_and_recv_tcp(krb5_socket_t fd,
-                 time_t tmout,
-                 const krb5_data *req,
-                 krb5_data *rep)
+KRB5_LIB_FUNCTION krb5_error_code KRB5_LIB_CALL
+krb5_sendto_set_hostname(krb5_context context,
+                        krb5_sendto_ctx ctx,
+                        const char *hostname)
 {
-    unsigned char len[4];
-    unsigned long rep_len;
-    krb5_data len_data;
+    char *newname;
+
+    /*
+     * Handle the case where hostname == ctx->hostname by copying it first, and
+     * disposing of any previous value after.
+     */
+    newname = strdup(hostname);
+    if (newname == NULL)
+       return krb5_enomem(context);
+    free(ctx->hostname);
+    ctx->hostname = newname;
+    return 0;
+}
 
-    _krb5_put_int(len, req->length, 4);
-    if(net_write (fd, len, sizeof(len)) < 0)
-       return -1;
-    if(net_write (fd, req->data, req->length) < 0)
-       return -1;
-    if (recv_loop (fd, tmout, 0, 4, &len_data) < 0)
-       return -1;
-    if (len_data.length != 4) {
-       krb5_data_free (&len_data);
-       return -1;
-    }
-    _krb5_get_int(len_data.data, &rep_len, 4);
-    krb5_data_free (&len_data);
-    if (recv_loop (fd, tmout, 0, rep_len, rep) < 0)
-       return -1;
-    if(rep->length != rep_len) {
-       krb5_data_free (rep);
-       return -1;
-    }
+KRB5_LIB_FUNCTION krb5_error_code KRB5_LIB_CALL
+krb5_sendto_set_sitename(krb5_context context,
+                        krb5_sendto_ctx ctx,
+                        const char *sitename)
+{
+    char *newname;
+
+    newname = strdup(sitename);
+    if (newname == NULL)
+       return krb5_enomem(context);
+    free(ctx->sitename);
+    ctx->sitename = newname;
     return 0;
 }
 
-int
-_krb5_send_and_recv_tcp(krb5_socket_t fd,
-                       time_t tmout,
-                       const krb5_data *req,
-                       krb5_data *rep)
+KRB5_LIB_FUNCTION void KRB5_LIB_CALL
+_krb5_sendto_ctx_set_krb5hst(krb5_context context,
+                            krb5_sendto_ctx ctx,
+                            krb5_krbhst_handle handle)
+{
+    heim_release(ctx->krbhst);
+    ctx->krbhst = heim_retain(handle);
+}
+
+KRB5_LIB_FUNCTION void KRB5_LIB_CALL
+krb5_sendto_ctx_free(krb5_context context, krb5_sendto_ctx ctx)
 {
-    return send_and_recv_tcp(fd, tmout, req, rep);
+    heim_release(ctx);
+}
+
+KRB5_LIB_FUNCTION krb5_error_code KRB5_LIB_CALL
+_krb5_kdc_retry(krb5_context context, krb5_sendto_ctx ctx, void *data,
+               const krb5_data *reply, int *action)
+{
+    krb5_error_code ret;
+    KRB_ERROR error;
+
+    if(krb5_rd_error(context, reply, &error))
+       return 0;
+
+    ret = krb5_error_from_rd_error(context, &error, NULL);
+    krb5_free_error_contents(context, &error);
+
+    switch(ret) {
+    case KRB5KRB_ERR_RESPONSE_TOO_BIG: {
+       if (krb5_sendto_ctx_get_flags(ctx) & KRB5_KRBHST_FLAGS_LARGE_MSG)
+           break;
+       krb5_sendto_ctx_add_flags(ctx, KRB5_KRBHST_FLAGS_LARGE_MSG);
+       *action = KRB5_SENDTO_RESET;
+       break;
+    }
+    case KRB5KDC_ERR_SVC_UNAVAILABLE:
+       *action = KRB5_SENDTO_RESET;
+       break;
+    }
+    return 0;
 }
 
 /*
- * `send_and_recv' tailored for the HTTP protocol.
+ *
  */
 
-static int
-send_and_recv_http(krb5_socket_t fd,
-                  time_t tmout,
-                  const char *prefix,
-                  const krb5_data *req,
-                  krb5_data *rep)
-{
-    char *request = NULL;
-    char *str;
+struct host;
+
+struct host_fun {
+    krb5_error_code (*prepare)(krb5_context, struct host *, const krb5_data *);
+    krb5_error_code (*send_fn)(krb5_context, struct host *);
+    krb5_error_code (*recv_fn)(krb5_context, struct host *, krb5_data *);
+    int ntries;
+};
+
+struct host {
+    enum host_state { CONNECT, CONNECTING, CONNECTED, WAITING_REPLY, DEAD } state;
+    krb5_krbhst_info *hi;
+    struct addrinfo *ai;
+    rk_socket_t fd;
+    struct host_fun *fun;
+    unsigned int tries;
+    time_t timeout;
+    krb5_data data;
+    unsigned int tid;
+};
+
+static void
+debug_host(krb5_context context, int level, struct host *host, const char *fmt, ...)
+       __attribute__ ((__format__ (__printf__, 4, 5)));
+
+static void
+debug_host(krb5_context context, int level, struct host *host, const char *fmt, ...)
+{
+    const char *proto = "unknown";
+    const char *state;
+    char name[NI_MAXHOST], port[NI_MAXSERV];
+    char *text = NULL;
+    va_list ap;
     int ret;
-    int len = base64_encode(req->data, req->length, &str);
 
-    if(len < 0)
-       return -1;
-    ret = asprintf(&request, "GET %s%s HTTP/1.0\r\n\r\n", prefix, str);
-    free(str);
-    if (ret < 0 || request == NULL)
+    if (!_krb5_have_debug(context, 5))
+       return;
+
+    va_start(ap, fmt);
+    ret = vasprintf(&text, fmt, ap);
+    va_end(ap);
+    if (ret == -1 || text == NULL)
+       return;
+
+    if (host->hi->proto == KRB5_KRBHST_HTTP)
+       proto = "http";
+    else if (host->hi->proto == KRB5_KRBHST_TCP)
+       proto = "tcp";
+    else if (host->hi->proto == KRB5_KRBHST_UDP)
+       proto = "udp";
+
+    if (getnameinfo(host->ai->ai_addr, host->ai->ai_addrlen,
+                   name, sizeof(name), port, sizeof(port), NI_NUMERICHOST) != 0)
+       name[0] = '\0';
+
+    switch (host->state) {
+    case CONNECT:      state = "CONNECT";              break;
+    case CONNECTING:   state = "CONNECTING";           break;
+    case CONNECTED:    state = "CONNECTED";            break;
+    case WAITING_REPLY:        state = "WAITING_REPLY";        break;
+    case DEAD:         state = "DEAD";                 break;
+    default:           state = "unknown";              break;
+    }
+
+    _krb5_debug(context, level, "%s: %s %s:%s (%s) state=%s tid: %08x", text,
+               proto, name, port, host->hi->hostname, state, host->tid);
+    free(text);
+}
+
+
+static void
+deallocate_host(void *ptr)
+{
+    struct host *host = ptr;
+    if (!rk_IS_BAD_SOCKET(host->fd))
+       rk_closesocket(host->fd);
+    krb5_data_free(&host->data);
+    host->ai = NULL;
+}
+
+static void
+host_dead(krb5_context context, struct host *host, const char *msg)
+{
+    debug_host(context, 5, host, "%s", msg);
+    rk_closesocket(host->fd);
+    host->fd = rk_INVALID_SOCKET;
+    host->state = DEAD;
+}
+
+static krb5_error_code
+send_stream(krb5_context context, struct host *host)
+{
+    ssize_t len;
+
+    len = krb5_net_write(context, &host->fd, host->data.data, host->data.length);
+
+    if (len < 0)
+       return errno;
+    else if (len < host->data.length) {
+       host->data.length -= len;
+       memmove(host->data.data, ((uint8_t *)host->data.data) + len, host->data.length - len);
        return -1;
-    ret = net_write (fd, request, strlen(request));
-    free (request);
-    if (ret < 0)
+    } else {
+       krb5_data_free(&host->data);
+       return 0;
+    }
+}
+
+static krb5_error_code
+recv_stream(krb5_context context, struct host *host)
+{
+    krb5_error_code ret;
+    size_t oldlen;
+    ssize_t sret;
+    int nbytes;
+
+    if (rk_SOCK_IOCTL(host->fd, FIONREAD, &nbytes) != 0 || nbytes <= 0)
+       return HEIM_NET_CONN_REFUSED;
+
+    if (context->max_msg_size - host->data.length < nbytes) {
+       krb5_set_error_message(context, KRB5KRB_ERR_FIELD_TOOLONG,
+                              N_("TCP message from KDC too large %d", ""),
+                              (int)(host->data.length + nbytes));
+       return KRB5KRB_ERR_FIELD_TOOLONG;
+    }
+
+    oldlen = host->data.length;
+
+    ret = krb5_data_realloc(&host->data, oldlen + nbytes + 1 /* NUL */);
+    if (ret)
        return ret;
-    ret = recv_loop(fd, tmout, 0, 0, rep);
-    if(ret)
+
+    sret = krb5_net_read(context, &host->fd, ((uint8_t *)host->data.data) + oldlen, nbytes);
+    if (sret <= 0) {
+       ret = errno;
        return ret;
-    {
-       unsigned long rep_len;
-       char *s, *p;
-
-       s = realloc(rep->data, rep->length + 1);
-       if (s == NULL) {
-           krb5_data_free (rep);
-           return -1;
-       }
-       s[rep->length] = 0;
-       p = strstr(s, "\r\n\r\n");
-       if(p == NULL) {
-           krb5_data_zero(rep);
-           free(s);
-           return -1;
-       }
-       p += 4;
-       rep->data = s;
-       rep->length -= p - s;
-       if(rep->length < 4) { /* remove length */
-           krb5_data_zero(rep);
-           free(s);
-           return -1;
-       }
-       rep->length -= 4;
-       _krb5_get_int(p, &rep_len, 4);
-       if (rep_len != rep->length) {
-           krb5_data_zero(rep);
-           free(s);
-           return -1;
-       }
-       memmove(rep->data, p + 4, rep->length);
     }
+    host->data.length = oldlen + sret;
+    /* zero terminate for http transport */
+    ((uint8_t *)host->data.data)[host->data.length] = '\0';
+
     return 0;
 }
 
-static int
-init_port(const char *s, int fallback)
+/*
+ *
+ */
+
+static void
+host_next_timeout(krb5_context context, struct host *host)
 {
-    if (s) {
-       int tmp;
+    host->timeout = context->kdc_timeout / host->fun->ntries;
+    if (host->timeout == 0)
+       host->timeout = 1;
 
-       sscanf (s, "%d", &tmp);
-       return htons(tmp);
-    } else
-       return fallback;
+    host->timeout += time(NULL);
 }
 
 /*
- * Return 0 if succesful, otherwise 1
+ * connected host
  */
 
-static int
-send_via_proxy (krb5_context context,
-               const krb5_krbhst_info *hi,
-               const krb5_data *send_data,
-               krb5_data *receive)
-{
-    char *proxy2 = strdup(context->http_proxy);
-    char *proxy  = proxy2;
-    char *prefix = NULL;
-    char *colon;
-    struct addrinfo hints;
-    struct addrinfo *ai, *a;
-    int ret;
-    krb5_socket_t s = rk_INVALID_SOCKET;
-    char portstr[NI_MAXSERV];
+static void
+host_connected(krb5_context context, krb5_sendto_ctx ctx, struct host *host)
+{
+    krb5_error_code ret;
 
-    if (proxy == NULL)
-       return ENOMEM;
-    if (strncmp (proxy, "http://", 7) == 0)
-       proxy += 7;
-
-    colon = strchr(proxy, ':');
-    if(colon != NULL)
-       *colon++ = '\0';
-    memset (&hints, 0, sizeof(hints));
-    hints.ai_family   = PF_UNSPEC;
-    hints.ai_socktype = SOCK_STREAM;
-    snprintf (portstr, sizeof(portstr), "%d",
-             ntohs(init_port (colon, htons(80))));
-    ret = getaddrinfo (proxy, portstr, &hints, &ai);
-    free (proxy2);
+    host->state = CONNECTED; 
+    /*
+     * Now prepare data to send to host
+     */
+    if (ctx->prexmit_func) {
+       krb5_data data;
+           
+       krb5_data_zero(&data);
+
+       ret = ctx->prexmit_func(context, host->hi->proto,
+                               ctx->prexmit_ctx, host->fd, &data);
+       if (ret == 0) {
+           if (data.length == 0) {
+               host_dead(context, host, "prexmit function didn't send data");
+               return;
+           }
+           ret = host->fun->prepare(context, host, &data);
+           krb5_data_free(&data);
+       }
+       
+    } else {
+       ret = host->fun->prepare(context, host, ctx->send_data);
+    }
     if (ret)
-       return krb5_eai_to_heim_errno(ret, errno);
+       debug_host(context, 5, host, "failed to prexmit/prepare");
+}
 
-    for (a = ai; a != NULL; a = a->ai_next) {
-       s = socket (a->ai_family, a->ai_socktype | SOCK_CLOEXEC, a->ai_protocol);
-       if (s < 0)
-           continue;
-       rk_cloexec(s);
-       if (connect (s, a->ai_addr, a->ai_addrlen) < 0) {
-           rk_closesocket (s);
-           continue;
+/*
+ * connect host
+ */
+
+static void
+host_connect(krb5_context context, krb5_sendto_ctx ctx, struct host *host)
+{
+    krb5_krbhst_info *hi = host->hi;
+    struct addrinfo *ai = host->ai;
+
+    debug_host(context, 5, host, "connecting to host");
+
+    if (connect(host->fd, ai->ai_addr, ai->ai_addrlen) < 0) {
+#ifdef HAVE_WINSOCK
+       if (WSAGetLastError() == WSAEWOULDBLOCK)
+           errno = EINPROGRESS;
+#endif /* HAVE_WINSOCK */
+       if (errno == EINPROGRESS && (hi->proto == KRB5_KRBHST_HTTP || hi->proto == KRB5_KRBHST_TCP)) {
+           debug_host(context, 5, host, "connecting to %d", host->fd);
+           host->state = CONNECTING;
+       } else {
+           host_dead(context, host, "failed to connect");
        }
-       break;
+    } else {
+       host_connected(context, ctx, host);
     }
-    if (a == NULL) {
-       freeaddrinfo (ai);
-       return 1;
-    }
-    freeaddrinfo (ai);
 
-    ret = asprintf(&prefix, "http://%s/", hi->hostname);
-    if(ret < 0 || prefix == NULL) {
-       close(s);
-       return 1;
-    }
-    ret = send_and_recv_http(s, context->kdc_timeout,
-                            prefix, send_data, receive);
-    rk_closesocket (s);
-    free(prefix);
-    if(ret == 0 && receive->length != 0)
-       return 0;
-    return 1;
+    host_next_timeout(context, host);
 }
 
+/*
+ * HTTP transport
+ */
+
 static krb5_error_code
-send_via_plugin(krb5_context context,
-               krb5_krbhst_info *hi,
-               time_t timeout,
-               const krb5_data *send_data,
-               krb5_data *receive)
+prepare_http(krb5_context context, struct host *host, const krb5_data *data)
 {
-    struct krb5_plugin *list = NULL, *e;
+    char *str = NULL, *request = NULL;
     krb5_error_code ret;
+    int len;
 
-    ret = _krb5_plugin_find(context, PLUGIN_TYPE_DATA, KRB5_PLUGIN_SEND_TO_KDC, &list);
-    if(ret != 0 || list == NULL)
-       return KRB5_PLUGIN_NO_HANDLE;
+    heim_assert(host->data.length == 0, "prepare_http called twice");
 
-    for (e = list; e != NULL; e = _krb5_plugin_get_next(e)) {
-       krb5plugin_send_to_kdc_ftable *service;
-       void *ctx;
+    len = rk_base64_encode(data->data, data->length, &str);
+    if(len < 0)
+       return ENOMEM;
 
-       service = _krb5_plugin_get_symbol(e);
-       if (service->minor_version != 0)
-           continue;
+    if (context->http_proxy)
+       ret = asprintf(&request, "GET http://%s/%s HTTP/1.0\r\n\r\n", host->hi->hostname, str);
+    else
+       ret = asprintf(&request, "GET /%s HTTP/1.0\r\n\r\n", str);
+    free(str);
+    if(ret < 0 || request == NULL)
+       return ENOMEM;
+    
+    host->data.data = request;
+    host->data.length = strlen(request);
 
-       (*service->init)(context, &ctx);
-       ret = (*service->send_to_kdc)(context, ctx, hi,
-                                     timeout, send_data, receive);
-       (*service->fini)(ctx);
-       if (ret == 0)
-           break;
-       if (ret != KRB5_PLUGIN_NO_HANDLE) {
-           krb5_set_error_message(context, ret,
-                                  N_("Plugin send_to_kdc failed to "
-                                     "lookup with error: %d", ""), ret);
-           break;
-       }
+    return 0;
+}
+
+static krb5_error_code
+recv_http(krb5_context context, struct host *host, krb5_data *data)
+{
+    krb5_error_code ret;
+    unsigned long rep_len;
+    size_t len;
+    char *p;
+
+    /*
+     * recv_stream returns a NUL terminated stream
+     */
+
+    ret = recv_stream(context, host);
+    if (ret)
+       return ret;
+
+    p = strstr(host->data.data, "\r\n\r\n");
+    if (p == NULL)
+       return -1;
+    p += 4;
+
+    len = host->data.length - (p - (char *)host->data.data);
+    if (len < 4)
+       return -1;
+
+    _krb5_get_int(p, &rep_len, 4);
+    if (len < rep_len)
+       return -1;
+
+    p += 4;
+
+    memmove(host->data.data, p, rep_len);
+    host->data.length = rep_len;
+
+    *data = host->data;
+    krb5_data_zero(&host->data);
+
+    return 0;
+}
+
+/*
+ * TCP transport
+ */
+
+static krb5_error_code
+prepare_tcp(krb5_context context, struct host *host, const krb5_data *data)
+{
+    krb5_error_code ret;
+    krb5_storage *sp;
+
+    heim_assert(host->data.length == 0, "prepare_tcp called twice");
+
+    sp = krb5_storage_emem();
+    if (sp == NULL)
+       return ENOMEM;
+    
+    ret = krb5_store_data(sp, *data);
+    if (ret) {
+       krb5_storage_free(sp);
+       return ret;
     }
-    _krb5_plugin_free(list);
-    return KRB5_PLUGIN_NO_HANDLE;
+    ret = krb5_storage_to_data(sp, &host->data);
+    krb5_storage_free(sp);
+
+    return ret;
 }
 
+static krb5_error_code
+recv_tcp(krb5_context context, struct host *host, krb5_data *data)
+{
+    krb5_error_code ret;
+    unsigned long pktlen;
+
+    ret = recv_stream(context, host);
+    if (ret)
+       return ret;
+
+    if (host->data.length < 4)
+       return -1;
+
+    _krb5_get_int(host->data.data, &pktlen, 4);
+    
+    if (pktlen > host->data.length - 4)
+       return -1;
+
+    memmove(host->data.data, ((uint8_t *)host->data.data) + 4, host->data.length - 4);
+    host->data.length -= 4;
+
+    *data = host->data;
+    krb5_data_zero(&host->data);
+    
+    return 0;
+}
 
 /*
- * Send the data `send' to one host from `handle` and get back the reply
- * in `receive'.
+ * UDP transport
  */
 
-KRB5_LIB_FUNCTION krb5_error_code KRB5_LIB_CALL
-krb5_sendto (krb5_context context,
-            const krb5_data *send_data,
-            krb5_krbhst_handle handle,
-            krb5_data *receive)
-{
-     krb5_error_code ret;
-     krb5_socket_t fd;
-     size_t i;
-
-     krb5_data_zero(receive);
-
-     while (!krb5_krbhst_retry_exceeded(context, handle)) {
-        krb5_krbhst_info *hi;
-
-        while (krb5_krbhst_next(context, handle, &hi) == 0) {
-            struct addrinfo *ai, *a;
-
-            _krb5_debug(context, 2,
-                        "trying to communicate with host %s in realm %s",
-                        hi->hostname, _krb5_krbhst_get_realm(handle));
-
-            if (context->send_to_kdc) {
-                struct send_to_kdc *s = context->send_to_kdc;
-
-                ret = (*s->func)(context, s->data, hi,
-                                 context->kdc_timeout, send_data, receive);
-                if (ret == 0 && receive->length != 0)
-                    goto out;
-                continue;
-            }
-
-            ret = send_via_plugin(context, hi, context->kdc_timeout,
-                                  send_data, receive);
-            if (ret == 0 && receive->length != 0)
-                goto out;
-            else if (ret != KRB5_PLUGIN_NO_HANDLE)
-                continue;
-
-            if(hi->proto == KRB5_KRBHST_HTTP && context->http_proxy) {
-                if (send_via_proxy (context, hi, send_data, receive) == 0) {
-                    ret = 0;
-                    goto out;
-                }
-                continue;
-            }
-
-            ret = krb5_krbhst_get_addrinfo(context, hi, &ai);
-            if (ret)
-                continue;
-
-            for (a = ai; a != NULL; a = a->ai_next) {
-                fd = socket (a->ai_family, a->ai_socktype | SOCK_CLOEXEC, a->ai_protocol);
-                if (rk_IS_BAD_SOCKET(fd))
-                    continue;
-                rk_cloexec(fd);
-                if (connect (fd, a->ai_addr, a->ai_addrlen) < 0) {
-                    rk_closesocket (fd);
-                    continue;
-                }
-                switch (hi->proto) {
-                case KRB5_KRBHST_HTTP :
-                    ret = send_and_recv_http(fd, context->kdc_timeout,
-                                             "", send_data, receive);
-                    break;
-                case KRB5_KRBHST_TCP :
-                    ret = send_and_recv_tcp (fd, context->kdc_timeout,
-                                             send_data, receive);
-                    break;
-                case KRB5_KRBHST_UDP :
-                    ret = send_and_recv_udp (fd, context->kdc_timeout,
-                                             send_data, receive);
-                    break;
-                }
-                rk_closesocket (fd);
-                if(ret == 0 && receive->length != 0)
-                    goto out;
-            }
-        }
-        krb5_krbhst_reset(context, handle);
-        krb5_krbhst_retry(context, handle);
-     }
-     krb5_clear_error_message (context);
-     ret = KRB5_KDC_UNREACH;
-out:
-     _krb5_debug(context, 2,
-                "result of trying to talk to realm %s = %d",
-                _krb5_krbhst_get_realm(handle), ret);
-     return ret;
+static krb5_error_code
+prepare_udp(krb5_context context, struct host *host, const krb5_data *data)
+{
+    return krb5_data_copy(&host->data, data->data, data->length);
 }
 
-KRB5_LIB_FUNCTION krb5_error_code KRB5_LIB_CALL
-krb5_sendto_kdc(krb5_context context,
-               const krb5_data *send_data,
-               const krb5_realm *realm,
-               krb5_data *receive)
+static krb5_error_code
+send_udp(krb5_context context, struct host *host)
 {
-    return krb5_sendto_kdc_flags(context, send_data, realm, receive, 0);
+    if (send(host->fd, host->data.data, host->data.length, 0) < 0)
+       return errno;
+    return 0;
 }
 
-KRB5_LIB_FUNCTION krb5_error_code KRB5_LIB_CALL
-krb5_sendto_kdc_flags(krb5_context context,
-                     const krb5_data *send_data,
-                     const krb5_realm *realm,
-                     krb5_data *receive,
-                     int flags)
+static krb5_error_code
+recv_udp(krb5_context context, struct host *host, krb5_data *data)
 {
     krb5_error_code ret;
-    krb5_sendto_ctx ctx;
+    int nbytes;
+
+
+    if (rk_SOCK_IOCTL(host->fd, FIONREAD, &nbytes) != 0 || nbytes <= 0)
+       return HEIM_NET_CONN_REFUSED;
 
-    ret = krb5_sendto_ctx_alloc(context, &ctx);
+    if (context->max_msg_size < nbytes) {
+       krb5_set_error_message(context, KRB5KRB_ERR_FIELD_TOOLONG,
+                              N_("UDP message from KDC too large %d", ""),
+                              (int)nbytes);
+       return KRB5KRB_ERR_FIELD_TOOLONG;
+    }
+
+    ret = krb5_data_alloc(data, nbytes);
     if (ret)
        return ret;
-    krb5_sendto_ctx_add_flags(ctx, flags);
-    krb5_sendto_ctx_set_func(ctx, _krb5_kdc_retry, NULL);
 
-    ret = krb5_sendto_context(context, ctx, send_data, *realm, receive);
-    krb5_sendto_ctx_free(context, ctx);
-    return ret;
+    ret = recv(host->fd, data->data, data->length, 0);
+    if (ret < 0) {
+       ret = errno;
+       krb5_data_free(data);
+       return ret;
+    }
+    data->length = ret;
+
+    return 0;
 }
 
-KRB5_LIB_FUNCTION krb5_error_code KRB5_LIB_CALL
-krb5_set_send_to_kdc_func(krb5_context context,
-                         krb5_send_to_kdc_func func,
-                         void *data)
+static struct host_fun http_fun = {
+    prepare_http,
+    send_stream,
+    recv_http,
+    1
+};
+static struct host_fun tcp_fun = {
+    prepare_tcp,
+    send_stream,
+    recv_tcp,
+    1
+};
+static struct host_fun udp_fun = {
+    prepare_udp,
+    send_udp,
+    recv_udp,
+    3
+};
+
+
+/*
+ * Host state machine
+ */
+
+static int
+eval_host_state(krb5_context context,
+               krb5_sendto_ctx ctx,
+               struct host *host,
+               int readable, int writeable)
 {
-    free(context->send_to_kdc);
-    if (func == NULL) {
-       context->send_to_kdc = NULL;
+    krb5_error_code ret;
+
+    if (host->state == CONNECT) {
+       /* check if its this host time to connect */
+       if (host->timeout < time(NULL))
+           host_connect(context, ctx, host);
        return 0;
     }
 
-    context->send_to_kdc = malloc(sizeof(*context->send_to_kdc));
-    if (context->send_to_kdc == NULL) {
-       krb5_set_error_message(context, ENOMEM,
-                              N_("malloc: out of memory", ""));
-       return ENOMEM;
+    if (host->state == CONNECTING && writeable)
+       host_connected(context, ctx, host);
+
+    if (readable) {
+
+       debug_host(context, 5, host, "reading packet");
+
+       ret = host->fun->recv_fn(context, host, &ctx->response);
+       if (ret == -1) {
+           /* not done yet */
+       } else if (ret == 0) {
+           /* if recv_foo function returns 0, we have a complete reply */
+           debug_host(context, 5, host, "host completed");
+           return 1;
+       } else {
+           host_dead(context, host, "host disconnected");
+       }
+    }
+
+    /* check if there is anything to send, state might DEAD after read */
+    if (writeable && host->state == CONNECTED) {
+
+       ctx->stats.sent_packets++;
+
+       debug_host(context, 5, host, "writing packet");
+
+       ret = host->fun->send_fn(context, host);
+       if (ret == -1) {
+           /* not done yet */
+       } else if (ret) {
+           host_dead(context, host, "host dead, write failed");
+       } else
+           host->state = WAITING_REPLY;
     }
 
-    context->send_to_kdc->func = func;
-    context->send_to_kdc->data = data;
     return 0;
 }
 
-KRB5_LIB_FUNCTION krb5_error_code KRB5_LIB_CALL
-_krb5_copy_send_to_kdc_func(krb5_context context, krb5_context to)
+/*
+ *
+ */
+
+static krb5_error_code
+submit_request(krb5_context context, krb5_sendto_ctx ctx, krb5_krbhst_info *hi)
 {
-    if (context->send_to_kdc)
-       return krb5_set_send_to_kdc_func(to,
-                                        context->send_to_kdc->func,
-                                        context->send_to_kdc->data);
-    else
-       return krb5_set_send_to_kdc_func(to, NULL, NULL);
-}
+    unsigned long submitted_host = 0;
+    krb5_boolean freeai = FALSE;
+    struct timeval nrstart, nrstop;
+    krb5_error_code ret;
+    struct addrinfo *ai = NULL, *a;
+    struct host *host;
 
+    ret = kdc_via_plugin(context, hi, context->kdc_timeout,
+                        ctx->send_data, &ctx->response);
+    if (ret == 0) {
+       return 0;
+    } else if (ret != KRB5_PLUGIN_NO_HANDLE) {
+       _krb5_debug(context, 5, "send via plugin failed %s: %d",
+                   hi->hostname, ret);
+       return ret;
+    }
 
+    /*
+     * If we have a proxy, let use the address of the proxy instead of
+     * the KDC and let the proxy deal with the resolving of the KDC.
+     */
+
+    gettimeofday(&nrstart, NULL);
+
+    if (hi->proto == KRB5_KRBHST_HTTP && context->http_proxy) {
+       char *proxy2 = strdup(context->http_proxy);
+       char *el, *proxy  = proxy2;
+       struct addrinfo hints;
+       char portstr[NI_MAXSERV];
+       unsigned short nport;
+       
+       if (proxy == NULL)
+           return ENOMEM;
+       if (strncmp(proxy, "http://", 7) == 0)
+           proxy += 7;
+       
+       /* check for url terminating slash */
+       el = strchr(proxy, '/');
+       if (el != NULL)
+           *el = '\0';
+
+       /* check for port in hostname, used below as port */
+       el = strchr(proxy, ':');
+       if(el != NULL)
+           *el++ = '\0';
+
+       memset(&hints, 0, sizeof(hints));
+       hints.ai_family   = PF_UNSPEC;
+       hints.ai_socktype = SOCK_STREAM;
+
+       /* On some systems ntohs(foo(..., htons(...))) causes shadowing */
+       nport = init_port(el, htons(80));
+       snprintf(portstr, sizeof(portstr), "%d", ntohs(nport));
+
+       ret = getaddrinfo(proxy, portstr, &hints, &ai);
+       free(proxy2);
+       if (ret)
+           return krb5_eai_to_heim_errno(ret, errno);
+       
+       freeai = TRUE;
 
-struct krb5_sendto_ctx_data {
-    int flags;
-    int type;
-    krb5_sendto_ctx_func func;
-    void *data;
-};
+    } else {
+       ret = krb5_krbhst_get_addrinfo(context, hi, &ai);
+       if (ret)
+           return ret;
+    }
 
-KRB5_LIB_FUNCTION krb5_error_code KRB5_LIB_CALL
-krb5_sendto_ctx_alloc(krb5_context context, krb5_sendto_ctx *ctx)
-{
-    *ctx = calloc(1, sizeof(**ctx));
-    if (*ctx == NULL) {
-       krb5_set_error_message(context, ENOMEM,
-                              N_("malloc: out of memory", ""));
-       return ENOMEM;
+    /* add up times */
+    gettimeofday(&nrstop, NULL);
+    timevalsub(&nrstop, &nrstart);
+    timevaladd(&ctx->stats.name_resolution, &nrstop);
+
+    ctx->stats.num_hosts++;
+
+    for (a = ai; a != NULL; a = a->ai_next) {
+       rk_socket_t fd;
+
+       fd = socket(a->ai_family, a->ai_socktype | SOCK_CLOEXEC, a->ai_protocol);
+       if (rk_IS_BAD_SOCKET(fd))
+           continue;
+       rk_cloexec(fd);
+
+#ifndef NO_LIMIT_FD_SETSIZE
+       if (fd >= FD_SETSIZE) {
+           _krb5_debug(context, 0, "fd too large for select");
+           rk_closesocket(fd);
+           continue;
+       }
+#endif
+       socket_set_nonblocking(fd, 1);
+
+       host = heim_alloc(sizeof(*host), "sendto-host", deallocate_host);
+       if (host == NULL) {
+            if (freeai)
+                freeaddrinfo(ai);
+           rk_closesocket(fd);
+           return ENOMEM;
+       }
+       host->hi = hi;
+       host->fd = fd;
+       host->ai = a;
+       /* next version of stid */
+       host->tid = ctx->stid = (ctx->stid & 0xffff0000) | ((ctx->stid & 0xffff) + 1);
+
+       host->state = CONNECT;
+
+       switch (host->hi->proto) {
+       case KRB5_KRBHST_HTTP :
+           host->fun = &http_fun;
+           break;
+       case KRB5_KRBHST_TCP :
+           host->fun = &tcp_fun;
+           break;
+       case KRB5_KRBHST_UDP :
+           host->fun = &udp_fun;
+           break;
+       default:
+           heim_abort("undefined http transport protocol: %d", (int)host->hi->proto);
+       }
+
+       host->tries = host->fun->ntries;
+
+       /*
+        * Connect directly next host, wait a host_timeout for each next address.
+        * We try host_connect() here, checking the return code because as we do
+        * non-blocking connects, any error here indicates that the address is just
+        * offline.  That is, it's something like "No route to host" which is not
+        * worth retrying.  And so, we fail directly and immediately to the next
+        * address for this host without enqueueing the address for retries.
+        */
+       if (submitted_host == 0) {
+           host_connect(context, ctx, host);
+           if (host->state == DEAD)
+               continue;
+       } else {
+           debug_host(context, 5, host,
+                      "Queuing host in future (in %ds), its the %lu address on the same name",
+                      (int)(context->host_timeout * submitted_host), submitted_host + 1);
+           host->timeout = time(NULL) + (submitted_host * context->host_timeout);
+       }
+
+       heim_array_append_value(ctx->hosts, host);
+       heim_release(host);
+       submitted_host++;
     }
+
+    if (freeai)
+       freeaddrinfo(ai);
+
+    if (submitted_host == 0)
+       return KRB5_KDC_UNREACH;
+
     return 0;
 }
 
-KRB5_LIB_FUNCTION void KRB5_LIB_CALL
-krb5_sendto_ctx_add_flags(krb5_sendto_ctx ctx, int flags)
+struct wait_ctx {
+    krb5_context context;
+    krb5_sendto_ctx ctx;
+    fd_set rfds;
+    fd_set wfds;
+    rk_socket_t max_fd;
+    int got_reply;
+    time_t timenow;
+};
+
+static void
+wait_setup(heim_object_t obj, void *iter_ctx, int *stop)
 {
-    ctx->flags |= flags;
+    struct wait_ctx *wait_ctx = iter_ctx;
+    struct host *h = (struct host *)obj;
+
+    if (h->state == CONNECT) {
+       if (h->timeout >= wait_ctx->timenow)
+           return;
+       host_connect(wait_ctx->context, wait_ctx->ctx, h);
+    }
+
+    /* skip dead hosts */
+    if (h->state == DEAD)
+       return;
+
+    /* if host timed out, dec tries and (retry or kill host) */
+    if (h->timeout < wait_ctx->timenow) {
+       heim_assert(h->tries != 0, "tries should not reach 0");
+       h->tries--;
+       if (h->tries == 0) {
+           host_dead(wait_ctx->context, h, "host timed out");
+           return;
+       } else {
+           debug_host(wait_ctx->context, 5, h, "retrying sending to");
+           host_next_timeout(wait_ctx->context, h);
+           host_connected(wait_ctx->context, wait_ctx->ctx, h);
+       }
+    }
+    
+#ifndef NO_LIMIT_FD_SETSIZE
+    heim_assert(h->fd < FD_SETSIZE, "fd too large");
+#endif
+    switch (h->state) {
+    case WAITING_REPLY:
+       FD_SET(h->fd, &wait_ctx->rfds);
+       break;
+    case CONNECTING:
+    case CONNECTED:
+       FD_SET(h->fd, &wait_ctx->rfds);
+       FD_SET(h->fd, &wait_ctx->wfds);
+       break;
+    default:
+       debug_host(wait_ctx->context, 5, h, "invalid sendto host state");
+       heim_abort("invalid sendto host state");
+    }
+    if (h->fd > wait_ctx->max_fd || wait_ctx->max_fd == rk_INVALID_SOCKET)
+       wait_ctx->max_fd = h->fd;
 }
 
-KRB5_LIB_FUNCTION int KRB5_LIB_CALL
-krb5_sendto_ctx_get_flags(krb5_sendto_ctx ctx)
+static int
+wait_filter_dead(heim_object_t obj, void *ctx)
 {
-    return ctx->flags;
+    struct host *h = (struct host *)obj;
+    return (int)((h->state == DEAD) ? true : false);
 }
 
-KRB5_LIB_FUNCTION void KRB5_LIB_CALL
-krb5_sendto_ctx_set_type(krb5_sendto_ctx ctx, int type)
+static void
+wait_accelerate(heim_object_t obj, void *ctx, int *stop)
 {
-    ctx->type = type;
+    struct host *h = (struct host *)obj;
+
+    if (h->state == CONNECT && h->timeout > 0)
+       h->timeout--;
 }
 
+static void
+wait_process(heim_object_t obj, void *ctx, int *stop)
+{
+    struct wait_ctx *wait_ctx = ctx;
+    struct host *h = (struct host *)obj;
+    int readable, writeable;
+    heim_assert(h->state != DEAD, "dead host resurected");
 
-KRB5_LIB_FUNCTION void KRB5_LIB_CALL
-krb5_sendto_ctx_set_func(krb5_sendto_ctx ctx,
-                        krb5_sendto_ctx_func func,
-                        void *data)
+#ifndef NO_LIMIT_FD_SETSIZE
+    heim_assert(h->fd < FD_SETSIZE, "fd too large");
+#endif
+    readable = FD_ISSET(h->fd, &wait_ctx->rfds);
+    writeable = FD_ISSET(h->fd, &wait_ctx->wfds);
+
+    if (readable || writeable || h->state == CONNECT)
+       wait_ctx->got_reply |= eval_host_state(wait_ctx->context, wait_ctx->ctx, h, readable, writeable);
+
+    /* if there is already a reply, just fall though the array */
+    if (wait_ctx->got_reply)
+       *stop = 1;
+}
+
+static krb5_error_code
+wait_response(krb5_context context, int *action, krb5_sendto_ctx ctx)
 {
-    ctx->func = func;
-    ctx->data = data;
+    struct wait_ctx wait_ctx;
+    struct timeval tv;
+    int ret;
+
+    wait_ctx.context = context;
+    wait_ctx.ctx = ctx;
+    FD_ZERO(&wait_ctx.rfds);
+    FD_ZERO(&wait_ctx.wfds);
+    wait_ctx.max_fd = rk_INVALID_SOCKET;
+
+    /* oh, we have a reply, it must be a plugin that got it for us */
+    if (ctx->response.length) {
+       *action = KRB5_SENDTO_FILTER;
+       return 0;
+    }
+
+    wait_ctx.timenow = time(NULL);
+
+    heim_array_iterate_f(ctx->hosts, &wait_ctx, wait_setup);
+    heim_array_filter_f(ctx->hosts, &wait_ctx, wait_filter_dead);
+
+    if (heim_array_get_length(ctx->hosts) == 0) {
+       if (ctx->stateflags & KRBHST_COMPLETED) {
+           _krb5_debug(context, 5, "no more hosts to send/recv packets to/from "
+                        "trying to pulling more hosts");
+           *action = KRB5_SENDTO_FAILED;
+       } else {
+           _krb5_debug(context, 5, "no more hosts to send/recv packets to/from "
+                        "and no more hosts -> failure");
+           *action = KRB5_SENDTO_TIMEOUT;
+       }
+       return 0;
+    }
+
+    if (wait_ctx.max_fd == rk_INVALID_SOCKET) {
+       /*
+        * If we don't find a host which can make progress, then
+        * we accelerate the process by moving all of the contestants
+        * up by 1s.
+        */
+       _krb5_debug(context, 5, "wait_response: moving the contestants forward");
+       heim_array_iterate_f(ctx->hosts, &wait_ctx, wait_accelerate);
+       return 0;
+    }
+
+    tv.tv_sec = 1;
+    tv.tv_usec = 0;
+
+    ret = select(wait_ctx.max_fd + 1, &wait_ctx.rfds, &wait_ctx.wfds, NULL, &tv);
+    if (ret < 0)
+       return errno;
+    if (ret == 0) {
+       *action = KRB5_SENDTO_TIMEOUT;
+       return 0;
+    }
+
+    wait_ctx.got_reply = 0;
+    heim_array_iterate_f(ctx->hosts, &wait_ctx, wait_process);
+    if (wait_ctx.got_reply)
+       *action = KRB5_SENDTO_FILTER;
+    else
+       *action = KRB5_SENDTO_CONTINUE;
+
+    return 0;
 }
 
-KRB5_LIB_FUNCTION void KRB5_LIB_CALL
-krb5_sendto_ctx_free(krb5_context context, krb5_sendto_ctx ctx)
+static void
+reset_context(krb5_context context, krb5_sendto_ctx ctx)
 {
-    memset(ctx, 0, sizeof(*ctx));
-    free(ctx);
+    krb5_data_free(&ctx->response);
+    heim_release(ctx->hosts);
+    ctx->hosts = heim_array_create();
+    ctx->stateflags = 0;
 }
 
+
+/*
+ *
+ */
+
 KRB5_LIB_FUNCTION krb5_error_code KRB5_LIB_CALL
 krb5_sendto_context(krb5_context context,
                    krb5_sendto_ctx ctx,
                    const krb5_data *send_data,
-                   const krb5_realm realm,
+                   krb5_const_realm realm,
                    krb5_data *receive)
 {
-    krb5_error_code ret;
+    krb5_error_code ret = 0;
     krb5_krbhst_handle handle = NULL;
+    struct timeval nrstart, nrstop, stop_time;
     int type, freectx = 0;
     int action;
+    int numreset = 0;
 
     krb5_data_zero(receive);
-
+    
     if (ctx == NULL) {
-       freectx = 1;
        ret = krb5_sendto_ctx_alloc(context, &ctx);
        if (ret)
-           return ret;
+           goto out;
+       freectx = 1;
     }
 
+    ctx->stid = (context->num_kdc_requests++) << 16;
+
+    memset(&ctx->stats, 0, sizeof(ctx->stats));
+    gettimeofday(&ctx->stats.start_time, NULL);
+
     type = ctx->type;
     if (type == 0) {
        if ((ctx->flags & KRB5_KRBHST_FLAGS_MASTER) || context->use_admin_kdc)
@@ -603,76 +1171,187 @@ krb5_sendto_context(krb5_context context,
            type = KRB5_KRBHST_KDC;
     }
 
+    ctx->send_data = send_data;
+
     if ((int)send_data->length > context->large_msg_size)
        ctx->flags |= KRB5_KRBHST_FLAGS_LARGE_MSG;
 
     /* loop until we get back a appropriate response */
 
-    do {
-       action = KRB5_SENDTO_DONE;
+    action = KRB5_SENDTO_INITIAL;
 
-       krb5_data_free(receive);
+    while (action != KRB5_SENDTO_DONE && action != KRB5_SENDTO_FAILED) {
+       krb5_krbhst_info *hi;
 
-       if (handle == NULL) {
-           ret = krb5_krbhst_init_flags(context, realm, type,
-                                        ctx->flags, &handle);
-           if (ret) {
-               if (freectx)
-                   krb5_sendto_ctx_free(context, ctx);
-               return ret;
+       switch (action) {
+       case KRB5_SENDTO_INITIAL:
+           ret = realm_via_plugin(context, realm, context->kdc_timeout,
+                                  send_data, &ctx->response);
+           if (ret == 0 || ret != KRB5_PLUGIN_NO_HANDLE) {
+               action = KRB5_SENDTO_DONE;
+               break;
+           }
+           action = KRB5_SENDTO_KRBHST;
+           /* FALLTHROUGH */
+       case KRB5_SENDTO_KRBHST:
+           if (ctx->krbhst == NULL) {
+               ret = krb5_krbhst_init_flags(context, realm, type,
+                                            ctx->flags, &handle);
+               if (ret)
+                   goto out;
+
+               if (ctx->hostname) {
+                   ret = krb5_krbhst_set_hostname(context, handle, ctx->hostname);
+                   if (ret)
+                       goto out;
+               }
+               if (ctx->sitename) {
+                   ret = krb5_krbhst_set_sitename(context, handle, ctx->sitename);
+                   if (ret)
+                       goto out;
+               }
+           } else {
+               handle = heim_retain(ctx->krbhst);
+           }
+           action = KRB5_SENDTO_TIMEOUT;
+           /* FALLTHROUGH */
+       case KRB5_SENDTO_TIMEOUT:
+
+           /*
+            * If we completed, just got to next step
+            */
+
+           if (ctx->stateflags & KRBHST_COMPLETED) {
+               action = KRB5_SENDTO_CONTINUE;
+               break;
+           }
+
+           /*
+            * Pull out next host, if there is no more, close the
+            * handle and mark as completed.
+            *
+            * Collect time spent in krbhst (dns, plugin, etc)
+            */
+
+
+           gettimeofday(&nrstart, NULL);
+
+           ret = krb5_krbhst_next(context, handle, &hi);
+
+           gettimeofday(&nrstop, NULL);
+           timevalsub(&nrstop, &nrstart);
+           timevaladd(&ctx->stats.krbhst, &nrstop);
+
+           action = KRB5_SENDTO_CONTINUE;
+           if (ret == 0) {
+               _krb5_debug(context, 5, "submitting new requests to new host");
+               if (submit_request(context, ctx, hi) != 0)
+                   action = KRB5_SENDTO_TIMEOUT;
+           } else {
+               _krb5_debug(context, 5, "out of hosts, waiting for replies");
+               ctx->stateflags |= KRBHST_COMPLETED;
            }
-       }
 
-       ret = krb5_sendto(context, send_data, handle, receive);
-       if (ret)
            break;
-       if (ctx->func) {
-           ret = (*ctx->func)(context, ctx, ctx->data, receive, &action);
+       case KRB5_SENDTO_CONTINUE:
+
+           ret = wait_response(context, &action, ctx);
            if (ret)
-               break;
-       }
-       if (action != KRB5_SENDTO_CONTINUE) {
-           krb5_krbhst_free(context, handle);
-           handle = NULL;
+               goto out;
+
+           break;
+       case KRB5_SENDTO_RESET:
+           /* start over */
+           _krb5_debug(context, 5,
+                       "krb5_sendto trying over again (reset): %d",
+                       numreset);
+           reset_context(context, ctx);
+           if (handle) {
+               krb5_krbhst_free(context, handle);
+               handle = NULL;
+           }
+           numreset++;
+           if (numreset >= 3)
+               action = KRB5_SENDTO_FAILED;
+           else
+               action = KRB5_SENDTO_KRBHST;
+
+           break;
+       case KRB5_SENDTO_FILTER:
+           /* default to next state, the filter function might modify this */
+           action = KRB5_SENDTO_DONE;
+
+           if (ctx->func) {
+               ret = (*ctx->func)(context, ctx, ctx->data,
+                                  &ctx->response, &action);
+               if (ret)
+                   goto out;
+
+               /*
+                * If we are not done, ask to continue/reset
+                */
+               switch (action) {
+               case KRB5_SENDTO_DONE:
+                   break;
+               case KRB5_SENDTO_RESET:
+               case KRB5_SENDTO_CONTINUE:
+                   /* free response to clear it out so we don't loop */
+                   krb5_data_free(&ctx->response);
+                   break;
+               default:
+                   ret = KRB5_KDC_UNREACH;
+                   krb5_set_error_message(context, ret,
+                                          "sendto filter funcation return unsupported state: %d", (int)action);
+                   goto out;
+               }
+           }
+           break;
+       case KRB5_SENDTO_FAILED:
+           ret = KRB5_KDC_UNREACH;
+           break;
+       case KRB5_SENDTO_DONE:
+           ret = 0;
+           break;
+       default:
+           heim_abort("invalid krb5_sendto_context state");
        }
-    } while (action != KRB5_SENDTO_DONE);
-    if (handle)
-       krb5_krbhst_free(context, handle);
-    if (ret == KRB5_KDC_UNREACH)
+    }
+
+out:
+    gettimeofday(&stop_time, NULL);
+    timevalsub(&stop_time, &ctx->stats.start_time);
+    if (ret == 0 && ctx->response.length) {
+       *receive = ctx->response;
+       krb5_data_zero(&ctx->response);
+    } else {
+       krb5_data_free(&ctx->response);
+       krb5_clear_error_message (context);
+       ret = KRB5_KDC_UNREACH;
        krb5_set_error_message(context, ret,
                               N_("unable to reach any KDC in realm %s", ""),
                               realm);
-    if (ret)
-       krb5_data_free(receive);
-    if (freectx)
-       krb5_sendto_ctx_free(context, ctx);
-    return ret;
-}
+    }
 
-krb5_error_code KRB5_CALLCONV
-_krb5_kdc_retry(krb5_context context, krb5_sendto_ctx ctx, void *data,
-               const krb5_data *reply, int *action)
-{
-    krb5_error_code ret;
-    KRB_ERROR error;
+    _krb5_debug(context, 1,
+               "%s %s done: %d hosts: %lu packets: %lu"
+               " wc: %lld.%06lu nr: %lld.%06lu kh: %lld.%06lu tid: %08x",
+               __func__, realm, ret,
+               ctx->stats.num_hosts, ctx->stats.sent_packets,
+               (long long)stop_time.tv_sec,
+               (unsigned long)stop_time.tv_usec,
+               (long long)ctx->stats.name_resolution.tv_sec,
+               (unsigned long)ctx->stats.name_resolution.tv_usec,
+               (long long)ctx->stats.krbhst.tv_sec,
+               (unsigned long)ctx->stats.krbhst.tv_usec, ctx->stid);
 
-    if(krb5_rd_error(context, reply, &error))
-       return 0;
 
-    ret = krb5_error_from_rd_error(context, &error, NULL);
-    krb5_free_error_contents(context, &error);
+    if (freectx)
+       krb5_sendto_ctx_free(context, ctx);
+    else
+       reset_context(context, ctx);
 
-    switch(ret) {
-    case KRB5KRB_ERR_RESPONSE_TOO_BIG: {
-       if (krb5_sendto_ctx_get_flags(ctx) & KRB5_KRBHST_FLAGS_LARGE_MSG)
-           break;
-       krb5_sendto_ctx_add_flags(ctx, KRB5_KRBHST_FLAGS_LARGE_MSG);
-       *action = KRB5_SENDTO_RESTART;
-       break;
-    }
-    case KRB5KDC_ERR_SVC_UNAVAILABLE:
-       *action = KRB5_SENDTO_CONTINUE;
-       break;
-    }
-    return 0;
+    if (handle)
+       krb5_krbhst_free(context, handle);
+
+    return ret;
 }