client-side TCP support
authorKen Raeburn <raeburn@mit.edu>
Tue, 11 Jun 2002 01:13:50 +0000 (01:13 +0000)
committerKen Raeburn <raeburn@mit.edu>
Tue, 11 Jun 2002 01:13:50 +0000 (01:13 +0000)
git-svn-id: svn://anonsvn.mit.edu/krb5/trunk@14492 dc483132-0cff-0310-8789-dd5450dbe970

src/include/ChangeLog
src/include/k5-int.h
src/lib/krb5/krb/ChangeLog
src/lib/krb5/krb/get_in_tkt.c
src/lib/krb5/krb/send_tgs.c
src/lib/krb5/os/ChangeLog
src/lib/krb5/os/sendto_kdc.c

index b228e57b64ffd80d691d9452d1f155ca0b8baac3..9d199427a7de5628b9d7c3d8c9b4d4c24294e8d5 100644 (file)
@@ -1,3 +1,10 @@
+2002-06-10  Ken Raeburn  <raeburn@mit.edu>
+
+       * k5-int.h (KRB_AP_PATH_NOT_ACCEPTED, KRB_ERR_RESPONSE_TOO_BIG):
+       New error number macros.
+       (krb5int_sendto_kdc): Update for new argument.
+       (krb5int_sendto_tcp): Declare.
+
 2002-06-04  Ken Raeburn  <raeburn@mit.edu>
 
        * k5-int.h (krb5int_sendto_udp): Declare.
index b384315ff61f8b26b59b2c317e6c81a77cfee5dd..7ec32d1512d3e1148c0a5f95f35e2bca435f9df7 100644 (file)
@@ -258,6 +258,9 @@ struct sockaddr;
                                        /* in message */
 #define KRB_AP_ERR_INAPP_CKSUM 50      /* Inappropriate type of */
                                        /* checksum in message */
+#define KRB_AP_PATH_NOT_ACCEPTED 51    /* Policy rejects transited path */
+#define KRB_ERR_RESPONSE_TOO_BIG 52    /* Response too big for UDP, */
+                                       /*   retry with TCP */
 
 /* other errors */
 #define KRB_ERR_GENERIC                60      /* Generic error (description */
@@ -470,9 +473,11 @@ struct addrlist;
 krb5_error_code krb5_lock_file (krb5_context, int, int);
 krb5_error_code krb5_unlock_file (krb5_context, int);
 krb5_error_code krb5_sendto_kdc (krb5_context, const krb5_data *,
-                                const krb5_data *, krb5_data *, int);
+                                const krb5_data *, krb5_data *, int, int);
 krb5_error_code krb5int_sendto_udp (krb5_context, const krb5_data *,
                                    const struct addrlist *, krb5_data *);
+krb5_error_code krb5int_sendto_tcp (krb5_context context, const krb5_data *,
+                                   const struct addrlist *, krb5_data *);
 krb5_error_code krb5_get_krbhst (krb5_context, const krb5_data *, char *** );
 krb5_error_code krb5_free_krbhst (krb5_context, char * const * );
 krb5_error_code krb5_create_secure_file (krb5_context, const char * pathname);
index 563cf8f9088261dacb754ae9fcd303ce95b6ed67..68a0a5d67fd5cff4e02dca45dea202df989b456e 100644 (file)
@@ -1,3 +1,12 @@
+2002-06-10  Ken Raeburn  <raeburn@mit.edu>
+
+       * get_in_tkt.c (send_as_request): Update arg list for
+       sendto_kdc.  If a RESPONSE_TOO_BIG error is returned from the KDC,
+       use a TCP connection.
+       * send_tgs.c (krb5_send_tgs): Update arg list for sendto_kdc.  If
+       a RESPONSE_TOO_BIG error is returned from the KDC, use a TCP
+       connection.
+
 2002-04-12  Ezra Peisach  <epeisach@bu.edu>
 
        * Makefile.in (clean): Remove t_expand and t_expand.o
index d6e469d8d84a8e2d7865b3438b6d3ee0fbfe13fd..7d8ff93cb03ed668499364475dc66b7da8c4f426 100644 (file)
@@ -96,9 +96,10 @@ send_as_request(context, request, time_now, ret_err_reply, ret_as_reply,
 {
     krb5_kdc_rep *as_reply = 0;
     krb5_error_code retval;
-    krb5_data *packet;
+    krb5_data *packet = 0;
     krb5_data reply;
     char k4_version;           /* same type as *(krb5_data::data) */
+    int tcp_only = 0;
 
     reply.data = 0;
     
@@ -116,10 +117,10 @@ send_as_request(context, request, time_now, ret_err_reply, ret_as_reply,
        goto cleanup;
 
     k4_version = packet->data[0];
+send_again:
     retval = krb5_sendto_kdc(context, packet, 
                             krb5_princ_realm(context, request->client),
-                            &reply, use_master);
-    krb5_free_data(context, packet);
+                            &reply, use_master, tcp_only);
     if (retval)
        goto cleanup;
 
@@ -131,9 +132,17 @@ send_as_request(context, request, time_now, ret_err_reply, ret_as_reply,
            /* some other error code--??? */        
            goto cleanup;
     
-       if (ret_err_reply)
+       if (ret_err_reply) {
+           if (err_reply->error == KRB_ERR_RESPONSE_TOO_BIG
+               && tcp_only == 0) {
+               tcp_only = 1;
+               krb5_free_error(context, err_reply);
+               free(reply.data);
+               reply.data = 0;
+               goto send_again;
+           }
            *ret_err_reply = err_reply;
-       else
+       else
            krb5_free_error(context, err_reply);
        goto cleanup;
     }
@@ -181,6 +190,8 @@ send_as_request(context, request, time_now, ret_err_reply, ret_as_reply,
        krb5_free_kdc_rep(context, as_reply);
 
 cleanup:
+    if (packet)
+       krb5_free_data(context, packet);
     if (reply.data)
        free(reply.data);
     return retval;
index 341296cb33e09b1e9e4a5895be2d611bff7bfedb..05c4b632279353c6c2402352e9f9f4a11a2d3aa0 100644 (file)
@@ -149,6 +149,7 @@ krb5_send_tgs(context, kdcoptions, timestruct, ktypes, sname, addrs,
     krb5_timestamp time_now;
     krb5_pa_data **combined_padata;
     krb5_pa_data ap_req_padata;
+    int tcp_only = 0;
 
     /* 
      * in_creds MUST be a valid credential NOT just a partially filled in
@@ -270,15 +271,27 @@ krb5_send_tgs(context, kdcoptions, timestruct, ktypes, sname, addrs,
     krb5_xfree(combined_padata);
 
     /* now send request & get response from KDC */
+send_again:
     retval = krb5_sendto_kdc(context, scratch, 
                             krb5_princ_realm(context, sname),
-                            &rep->response, 0);
-    krb5_free_data(context, scratch);
-
+                            &rep->response, 0, tcp_only);
     if (retval == 0) {
-        if (krb5_is_tgs_rep(&rep->response))
+       if (krb5_is_krb_error(&rep->response)) {
+           if (!tcp_only) {
+               krb5_error *err_reply;
+               retval = decode_krb5_error(&rep->response, &err_reply);
+               if (err_reply->error == KRB_ERR_RESPONSE_TOO_BIG) {
+                   tcp_only = 1;
+                   krb5_free_error(context, err_reply);
+                   free(rep->response.data);
+                   rep->response.data = 0;
+                   goto send_again;
+               }
+               krb5_free_error(context, err_reply);
+           }
+       } else if (krb5_is_tgs_rep(&rep->response))
            rep->message_type = KRB5_TGS_REP;
-        else /* assume it's an error */
+        else /* XXX: assume it's an error */
            rep->message_type = KRB5_ERROR;
     }
 
@@ -295,6 +308,5 @@ send_tgs_error_1:;
        krb5_xfree(tgsreq.authorization_data.ciphertext.data);
     }
 
-
     return retval;
 }
index 43e911aa7f8237a90ad9f276c568840600bffde9..60633a972ce01b06c2b8c051682064a3afac1fb1 100644 (file)
@@ -1,5 +1,19 @@
 2002-06-10  Ken Raeburn  <raeburn@mit.edu>
 
+       * sendto_kdc.c: Include sys/ioctl.h and sys/filio.h if
+       available.  Define ENABLE_TCP as 0 on Windows, 1 otherwise.
+       (debug_log_connect): New function.
+       (dperror, dfprintf): New macros.
+       (krb5int_sendto_udp): Use dperror, dfprintf, and
+       debug_log_connect.
+       (state_strings): New variable.
+       (struct conn_state, struct select_state): New types.
+       (print_fdsets, call_select, make_nonblocking,
+       start_tcp_connection, iov_advance, service_tcp_fd, service_fds,
+       krb5int_sendto_tcp): New functions.
+       (krb5_sendto_kdc): New argument TCP_ONLY.  Try UDP first unless
+       told otherwise, then try TCP.
+
        * t_std_conf.c: Include fake-addrinfo.h.
        (test_locate_kdc): Update arg list to locate_kdc.  Print IPv6 as
        well as IPv4 addresses.
index bf05742402e0996a9b3fef39ecc49c2c00ba886f..a5ff0abc76628c28e0d2da12069cf834d9a35ab0 100644 (file)
 #include <sys/select.h>
 #endif
 
+/* For FIONBIO.  */
+#include <sys/ioctl.h>
+#ifdef HAVE_SYS_FILIO_H
+#include <sys/filio.h>
+#endif
+
+#ifdef _WIN32
+#define ENABLE_TCP 0 /* for now */
+#else
+#define ENABLE_TCP 1
+#endif
+
 /*
  * send the formatted request 'message' to a KDC for realm 'realm' and
  * return the response (if any) in 'reply'.
  */
 
 krb5_error_code
-krb5_sendto_kdc (context, message, realm, reply, use_master)
+krb5_sendto_kdc (context, message, realm, reply, use_master, tcp_only)
     krb5_context context;
     const krb5_data * message;
     const krb5_data * realm;
     krb5_data * reply;
     int use_master;
+    int tcp_only;
 {
     krb5_error_code retval;
     struct addrlist addrs;
@@ -70,15 +83,65 @@ krb5_sendto_kdc (context, message, realm, reply, use_master)
      * find KDC location(s) for realm
      */
 
-    if ((retval = krb5_locate_kdc(context, realm, &addrs, use_master, SOCK_DGRAM)))
-       return retval;
-    if (addrs.naddrs == 0)
-       return (use_master ? KRB5_KDC_UNREACH : KRB5_REALM_UNKNOWN);
-    retval = krb5int_sendto_udp (context, message, &addrs, reply);
-    krb5int_free_addrlist (&addrs);
+    /*
+     * DO NOT depend on this staying as two separate loops.  We may change
+     * the order, or we may integrate them into one loop.
+     *
+     * BUG: This code won't return "interesting" errors (e.g., out of mem,
+     * bad config file) from locate_kdc.  KRB5_REALM_CANT_RESOLVE can be
+     * ignored from one query of two, but if only one query is done, or
+     * both return that error, it should be returned to the caller.  Also,
+     * "interesting" errors (not KRB5_KDC_UNREACH) from sendto_{udp,tcp}
+     * should probably be returned as well.
+     */
+
+    retval = (use_master ? KRB5_KDC_UNREACH : KRB5_REALM_UNKNOWN);
+
+    if (!tcp_only
+       && ! krb5_locate_kdc(context, realm, &addrs, use_master, SOCK_DGRAM)) {
+       if (addrs.naddrs > 0) {
+           retval = krb5int_sendto_udp (context, message, &addrs, reply);
+           krb5int_free_addrlist (&addrs);
+           if (retval == 0)
+               return 0;
+       }
+    }
+
+#if ENABLE_TCP
+    if (! krb5_locate_kdc(context, realm, &addrs, use_master, SOCK_STREAM)) {
+       if (addrs.naddrs > 0) {
+           retval = krb5int_sendto_tcp (context, message, &addrs, reply);
+           krb5int_free_addrlist (&addrs);
+           if (retval == 0)
+               return 0;
+       }
+    }
+#endif
+
     return retval;
 }
 
+static void debug_log_connect (int fd, struct addrinfo *ai)
+{
+#ifdef DEBUG
+    char addrbuf[NI_MAXHOST], portbuf[NI_MAXSERV];
+    if (0 != getnameinfo (ai->ai_addr, ai->ai_addrlen,
+                         addrbuf, sizeof (addrbuf), portbuf, sizeof (portbuf),
+                         NI_NUMERICHOST | NI_NUMERICSERV))
+       strcpy (addrbuf, "??"), strcpy (portbuf, "??");
+    fprintf (stderr, " fd %d; connecting to %s port %s...\n", fd,
+            addrbuf, portbuf);
+#endif
+}
+
+#ifdef DEBUG
+#define dperror(MSG) perror(MSG)
+#define dfprintf(ARGLIST) fprintf ARGLIST
+#else
+#define dperror(MSG) ((void)(MSG))
+#define dfprintf(ARGLIST) ((void)0)
+#endif
+
 krb5_error_code
 krb5int_sendto_udp (krb5_context context, const krb5_data *message,
                    const struct addrlist *addrs, krb5_data *reply)
@@ -126,7 +189,8 @@ krb5int_sendto_udp (krb5_context context, const krb5_data *message,
         timeout <<= krb5_skdc_timeout_shift) {
        sent = 0;
        for (host = 0; host < addrs->naddrs; host++) {
-           if (addrs->addrs[host]->ai_socktype != SOCK_DGRAM)
+           struct addrinfo *ai = addrs->addrs[host];
+           if (ai->ai_socktype != SOCK_DGRAM)
                continue;
            /* Send to the host, wait timeout seconds for a response,
               then move on. */
@@ -142,60 +206,35 @@ krb5int_sendto_udp (krb5_context context, const krb5_data *message,
                 * protocol exists to support a particular socket type
                 * within a given protocol family.
                 */
-#ifdef DEBUG
-               fprintf (stderr, "getting dgram socket in family %d...",
-                        addrs->addrs[host]->ai_family);
-#endif
-               socklist[host] = socket(addrs->addrs[host]->ai_family,
-                                       SOCK_DGRAM, 0);
+               dfprintf((stderr, "getting dgram socket in family %d...",
+                         ai->ai_family));
+               socklist[host] = socket(ai->ai_family, SOCK_DGRAM, 0);
                if (socklist[host] == INVALID_SOCKET) {
-#ifdef DEBUG
-                   perror ("socket");
-                   fprintf (stderr, "af was %d\n", addrs->addrs[host]->ai_family);
-#endif
+                   dfprintf((stderr, "socket: %s\naf was %d\n",
+                             strerror(errno), ai->ai_family));
                    continue;           /* try other hosts */
                }
-#ifdef DEBUG
-               {
-                   char addrbuf[NI_MAXHOST], portbuf[NI_MAXSERV];
-                   if (0 != getnameinfo (addrs->addrs[host]->ai_addr,
-                                         addrs->addrs[host]->ai_addrlen,
-                                         addrbuf, sizeof (addrbuf),
-                                         portbuf, sizeof (portbuf),
-                                         NI_NUMERICHOST | NI_NUMERICSERV))
-                       strcpy (addrbuf, "??"), strcpy (portbuf, "??");
-                   fprintf (stderr, " fd %d; connecting to %s port %s...",
-                            socklist[host], addrbuf, portbuf);
-               }
-#endif
+               debug_log_connect(socklist[host], ai);
                /* have a socket to send/recv from */
                /* On BSD systems, a connected UDP socket will get connection
                   refused and net unreachable errors while an unconnected
                   socket will time out, so use connect, send, recv instead of
                   sendto, recvfrom.  The connect here may return an error if
                   the destination host is known to be unreachable. */
-               if (connect(socklist[host], addrs->addrs[host]->ai_addr,
-                           addrs->addrs[host]->ai_addrlen) == SOCKET_ERROR) {
-#ifdef DEBUG
-                   perror ("connect");
-#endif
+               if (connect(socklist[host], ai->ai_addr,
+                           ai->ai_addrlen) == SOCKET_ERROR) {
+                   dperror ("connect");
                    continue;
                }
            }
-#ifdef DEBUG
-           fprintf (stderr, "sending...");
-#endif
+           dfprintf((stderr, "sending..."));
            if (send(socklist[host],
                       message->data, message->length, 0) 
                != message->length) {
-#ifdef DEBUG
-               perror ("sendto");
-#endif
+               dperror ("sendto");
                continue;
            }
-#ifdef DEBUG
-           fprintf (stderr, "\n");
-#endif
+           dfprintf((stderr, "\n"));
        retry:
            waitlen.tv_usec = 0;
            waitlen.tv_sec = timeout;
@@ -243,6 +282,7 @@ krb5int_sendto_udp (krb5_context context, const krb5_data *message,
 
                reply->length = cc;
                retval = 0;
+               dfprintf((stderr, "got answer on fd %d\n", socklist[host]));
                goto out;
            } else if (nready == 0) {
                /* timeout */
@@ -253,10 +293,12 @@ krb5int_sendto_udp (krb5_context context, const krb5_data *message,
        if (!sent) {
            /* never were able to send to any servers; give up */
            retval = KRB5_KDC_UNREACH;
-           break;
+           dfprintf((stderr, "no KDCs to contact\n"));
+           goto out;
        }
     }
     retval = KRB5_KDC_UNREACH;
+    dfprintf((stderr, "no answer\n"));
  out:
     for (i = 0; i < addrs->naddrs; i++)
        if (socklist[i] != INVALID_SOCKET)
@@ -272,3 +314,499 @@ krb5int_sendto_udp (krb5_context context, const krb5_data *message,
     }
     return retval;
 }
+
+#if ENABLE_TCP
+/*
+ * To do:
+ * - TCP NOPUSH/CORK socket options?
+ * - error codes that don't suck
+ * - getsockopt(SO_ERROR) to check connect status
+ */
+
+#ifdef DEBUG
+static const char *state_strings[] = {
+    "CONNECTING", "WRITING", "READING", "FAILED"
+};
+#endif
+struct conn_state {
+    SOCKET fd;
+    krb5_error_code err;
+    enum { CONNECTING, WRITING, READING, FAILED } state;
+    char *buf;
+    size_t bufsize;
+    unsigned char bufsizebytes[4];
+    size_t bufsizebytes_read;
+    struct iovec iov[2];
+    struct iovec *iovp;
+    int iov_count;
+};
+
+struct select_state {
+    int max, nfds;
+    fd_set rfds, wfds;
+    struct timeval end_time;
+};
+
+#ifdef DEBUG
+static void
+print_fdsets (FILE *f, fd_set *rfds, fd_set *wfds, int maxfd)
+{
+    int i;
+    for (i = 0; i < maxfd; i++) {
+       int r = FD_ISSET(i, rfds);
+       int w = FD_ISSET(i, wfds);
+       if (r || w) {
+           fprintf(f, " %d", i);
+           if (r)
+               fprintf(f, "r");
+           if (w)
+               fprintf(f, "w");
+       }
+    }
+}
+#endif
+
+/*
+ * Call select and return results.
+ * Input: interesting file descriptors and absolute timeout
+ * Output: select return value (-1 or num fds ready) and fd_sets
+ * Return: 0 (for i/o available or timeout) or error code.
+ */
+static krb5_error_code
+call_select (struct select_state *in, struct select_state *out, int *sret)
+{
+    struct timeval now;
+    krb5_error_code e;
+
+    *out = *in;
+    if (gettimeofday(&now, 0)) {
+       e = errno;
+       dperror("gettimeofday");
+       return e;
+    }
+    out->end_time.tv_sec -= now.tv_sec;
+    out->end_time.tv_usec -= now.tv_usec;
+    if (out->end_time.tv_usec < 0) {
+       out->end_time.tv_usec += 1000000;
+       out->end_time.tv_sec--;
+    }
+    if (out->end_time.tv_sec < 0) {
+       *sret = 0;
+       return 0;
+    }
+#ifdef DEBUG
+    fprintf(stderr, "selecting on %d sockets [", out->nfds);
+    print_fdsets(stderr, &out->rfds, &out->wfds, out->max);
+    fprintf(stderr, " ] timeout %ld.%06ld\n", (long) out->end_time.tv_sec,
+           (long) out->end_time.tv_usec);
+#endif
+    *sret = select(out->max, &out->rfds, &out->wfds, 0, &out->end_time);
+    e = errno;
+#ifdef DEBUG
+    fprintf(stderr, "select returns %d", *sret);
+    if (*sret < 0)
+       fprintf(stderr, ", errno = %d/%s\n", e, strerror(e));
+    else if (*sret == 0)
+       fprintf(stderr, " (timeout)\n");
+    else {
+       fprintf(stderr, ":");
+       print_fdsets(stderr, &out->rfds, &out->wfds, out->max);
+       fprintf(stderr, "\n");
+    }
+#endif
+    if (*sret < 0)
+       return errno;
+    return 0;
+}
+
+static int
+make_nonblocking (int fd)
+{
+    static const int one = 1;
+
+    if (ioctlsocket (fd, FIONBIO, &one)) {
+       dperror("sendto_kdc_tcp: ioctl(FIONBIO)");
+       return errno;
+    }
+    return 0;
+}
+
+static int
+start_tcp_connection (struct conn_state *state, struct addrinfo *ai)
+{
+    int fd, e;
+
+    state->err = 0;
+    state->buf = 0;
+    state->bufsizebytes_read = 0;
+    state->iovp = state->iov;
+    state->iov[0].iov_base = (char *) state->bufsizebytes;
+    state->iov[0].iov_len = 4;
+    state->iov[1].iov_base = 0;
+    state->iov[1].iov_len = 0;
+    state->iov_count = 2;
+
+    dfprintf((stderr, "getting stream socket in family %d...", ai->ai_family));
+    fd = socket(ai->ai_family, SOCK_STREAM, 0);
+    if (fd == INVALID_SOCKET) {
+       state->err = errno;
+       dfprintf((stderr, "socket: %s connecting with af %d\n",
+                 strerror (state->err), ai->ai_family));
+       return -1;              /* try other hosts */
+    }
+    debug_log_connect(fd, ai);
+    /* Make it non-blocking.  */
+    make_nonblocking(fd);
+
+    /* Start connecting to KDC.  */
+    e = connect(fd, ai->ai_addr, ai->ai_addrlen);
+    if (e != 0) {
+       /*
+        * This is the path that should be followed for non-blocking
+        * connections.
+        */
+       if (SOCKET_ERRNO == EINPROGRESS || SOCKET_ERRNO == EWOULDBLOCK) {
+           state->state = CONNECTING;
+       } else {
+           state->err = SOCKET_ERRNO;
+           dfprintf((stderr, "socket error %d in connect()\n", state->err));
+           state->state = FAILED;
+           return -2;
+       }
+    } else {
+       /*
+        * Connect returned zero even though we tried to make it
+        * non-blocking, which should have caused it to return before
+        * finishing the connection.  Oh well.  Someone's network
+        * stack is broken, but if they gave us a connection, use it.
+        */
+       state->state = WRITING;
+    }
+
+    state->fd = fd;
+    return 0;
+}
+
+static void
+iov_advance (struct iovec *iov, int how_far)
+{
+    if (iov->iov_len < how_far)
+       abort();
+    /* Can't do math on void pointers.  */
+    iov->iov_base = (char *) iov->iov_base + how_far;
+    iov->iov_len -= how_far;
+}
+
+/* Return nonzero only if we're finished and the caller should exit
+   its loop.  This happens in two cases: We have a complete message,
+   or the socket has closed and no others are open.  */
+static int
+service_tcp_fd (struct conn_state *conn, struct select_state *selstate,
+               int can_read, int can_write)
+{
+    krb5_error_code e = 0;
+    int nwritten, nread;
+
+#ifdef DEBUG
+    /* index with can_read + 2 * can_write */
+    static const char *const select_states[] = {
+       "cannot_read_or_write??",
+       "can_read", "can_write", "can_read/can_write",
+    };
+    fprintf(stderr, "handling %s on fd %d in state %s\n",
+           select_states[can_read + 2 * can_write],
+           conn->fd, state_strings[(int) conn->state]);
+#endif
+
+    if (!can_read && !can_write)
+       abort();
+    switch (conn->state) {
+
+    case CONNECTING:
+       if (can_read) {
+           /* Bad -- the KDC shouldn't be sending to us first.  */
+           e = EINVAL /* ?? */;
+       kill_conn:
+           conn->state = FAILED;
+           shutdown(conn->fd, 2);
+           FD_CLR(conn->fd, &selstate->rfds);
+           FD_CLR(conn->fd, &selstate->wfds);
+           conn->err = e;
+           dfprintf((stderr, "abandoning connection %d: %s\n",
+                    conn->fd, strerror(e)));
+           /* Fix up max fd for next select call.  */
+           if (selstate->max == 1 + conn->fd) {
+               while (selstate->max > 0
+                      && ! FD_ISSET(selstate->max-1, &selstate->rfds)
+                      && ! FD_ISSET(selstate->max-1, &selstate->wfds))
+                   selstate->max--;
+               dfprintf((stderr, "new max_fd + 1 is %d\n", selstate->max));
+           }
+           selstate->nfds--;
+           if (e == EINVAL) {
+               closesocket(conn->fd);
+               conn->fd = INVALID_SOCKET;
+           }
+           return e == 0;
+       }
+
+       /*
+        * Connect finished -- but did it succeed or fail?
+        * Try writing, I guess, and find out.
+        */
+       conn->state = WRITING;
+       goto try_writing;
+
+    case WRITING:
+       if (can_read) {
+           e = E2BIG;
+           /* Bad -- the KDC shouldn't be sending anything yet.  */
+           goto kill_conn;
+       }
+
+       try_writing:
+       dfprintf((stderr, "trying to writev %d (%d bytes) to fd %d\n",
+                 conn->iov_count,
+                 ((conn->iov_count == 2 ? conn->iovp[1].iov_len : 0)
+                  + conn->iovp[0].iov_len),
+                 conn->fd));
+       nwritten = writev(conn->fd, conn->iovp,
+                         conn->iov_count);
+       if (nwritten < 0) {
+           e = SOCKET_ERRNO;
+           dfprintf((stderr, "failed: %s\n", strerror(e)));
+           goto kill_conn;
+       }
+       dfprintf((stderr, "wrote %d bytes\n", nwritten));
+       while (nwritten) {
+           struct iovec *iovp = conn->iovp;
+           if (nwritten < iovp->iov_len) {
+               iov_advance(iovp, nwritten);
+               nwritten = 0;
+           } else {
+               nwritten -= conn->iovp->iov_len;
+               conn->iovp++;
+               conn->iov_count--;
+               if (conn->iov_count == 0 && nwritten != 0)
+                   /* Wrote more than we wanted to?  */
+                   abort();
+           }
+       }
+       if (conn->iov_count == 0) {
+           /* Done writing, switch to reading.  */
+           shutdown(conn->fd, 1);
+           FD_CLR(conn->fd, &selstate->wfds);
+           /* Q: How do we detect failures to send the remaining data
+              to the remote side, since we're in non-blocking mode?
+              Will we always get errors on the reading side?  */
+           dfprintf((stderr, "switching fd %d to READING\n", conn->fd));
+           conn->state = READING;
+           conn->bufsizebytes_read = 0;
+       }
+       return 0;
+
+    case READING:
+       if (conn->bufsizebytes_read == 4) {
+           /* Reading data.  */
+           dfprintf((stderr, "reading %ld bytes of data from fd %d\n",
+                     (long) conn->iov[0].iov_len, conn->fd));
+           nread = read(conn->fd, conn->iov[0].iov_base,
+                        conn->iov[0].iov_len);
+           if (nread <= 0) {
+               e = nread ? SOCKET_ERRNO : ECONNRESET;
+               free(conn->buf);
+               conn->buf = 0;
+               goto kill_conn;
+           }
+           iov_advance(conn->iov, nread);
+           if (conn->iov[0].iov_len == 0) {
+               /* We win!  */
+               return 1;
+           }
+       } else {
+           /* Reading length.  */
+           nread = read(conn->fd,
+                        conn->bufsizebytes + conn->bufsizebytes_read,
+                        4 - conn->bufsizebytes_read);
+           if (nread < 0) {
+               e = SOCKET_ERRNO;
+               goto kill_conn;
+           }
+           conn->bufsizebytes_read += nread;
+           if (conn->bufsizebytes_read == 4) {
+               unsigned long len;
+               len = conn->bufsizebytes[0];
+               len = (len << 8) + conn->bufsizebytes[1];
+               len = (len << 8) + conn->bufsizebytes[2];
+               len = (len << 8) + conn->bufsizebytes[3];
+               dfprintf((stderr, "received length on fd %d is %lu\n",
+                         conn->fd, len));
+               /* Arbitrary 1M cap.  */
+               if (len > 1 * 1024 * 1024) {
+                   e = E2BIG;
+                   goto kill_conn;
+               }
+               conn->bufsize = conn->iov[0].iov_len = len;
+               conn->buf = conn->iov[0].iov_base = malloc(len);
+               dfprintf((stderr, "allocated %lu byte buffer at %p\n",
+                         len, conn->buf));
+               if (conn->iov[0].iov_base == 0) {
+                   e = errno;
+                   goto kill_conn;
+               }
+           }
+       }
+       break;
+
+    default:
+       abort();
+    }
+    return 0;
+}
+
+static int
+service_fds (struct select_state *selstate,
+            struct conn_state *conns, size_t n_conns, int *winning_conn)
+{
+    int e, selret;
+    struct select_state sel_results;
+
+    e = 0;
+    while (selstate->nfds > 0
+          && (e = call_select(selstate, &sel_results, &selret)) == 0) {
+       int i;
+
+       dfprintf((stderr, "service_fds examining results, selret=%d\n",
+                 selret));
+
+       if (selret == 0)
+           /* Timeout, return to caller.  */
+           return 0;
+
+       /* Got something on a socket, process it.  */
+       for (i = 0; i <= selstate->max && selret > 0; i++) {
+           int can_read, can_write;
+           if (conns[i].fd == INVALID_SOCKET)
+               continue;
+           can_read = FD_ISSET(conns[i].fd, &sel_results.rfds);
+           can_write = FD_ISSET(conns[i].fd, &sel_results.wfds);
+           if (!can_read && !can_write)
+               continue;
+
+           selret--;
+           if (service_tcp_fd(&conns[i], selstate, can_read, can_write)) {
+               dfprintf((stderr, "service_tcp_fd says we're done\n"));
+               *winning_conn = i;
+               return 1;
+           }
+       }
+    }
+    if (e != 0) {
+       dfprintf((stderr, "select returned %s\n", strerror(e)));
+       *winning_conn = -1;
+       return 1;
+    }
+    return 0;
+}
+
+krb5_error_code
+krb5int_sendto_tcp (krb5_context context, const krb5_data *message,
+                   const struct addrlist *addrs, krb5_data *reply)
+{
+    int i;
+    krb5_error_code retval;
+    struct conn_state *conns;
+    size_t n_conns, host;
+    struct select_state select_state;
+    struct timeval now;
+    int winning_conn = -1, e = 0;
+    unsigned char message_len_buf[4];
+
+    n_conns = addrs->naddrs;
+    conns = malloc(n_conns * sizeof(struct conn_state));
+    if (conns == NULL) {
+       return ENOMEM;
+    }
+    for (i = 0; i < n_conns; i++) {
+       memset(&conns[i], 0, sizeof(conns[i]));
+       conns[i].fd = INVALID_SOCKET;
+    }
+
+    select_state.max = 0;
+    select_state.nfds = 0;
+    FD_ZERO(&select_state.rfds);
+    FD_ZERO(&select_state.wfds);
+
+    message_len_buf[0] = (message->length >> 24) & 0xff;
+    message_len_buf[1] = (message->length >> 16) & 0xff;
+    message_len_buf[2] = (message->length >>  8) & 0xff;
+    message_len_buf[3] =  message->length        & 0xff;
+
+    /* Set up connections.  */
+    for (host = 0; host < n_conns; host++) {
+       if (addrs->addrs[host]->ai_socktype != SOCK_STREAM)
+           continue;
+       /* Send to the host, wait timeout seconds for a response,
+          then move on. */
+       if (start_tcp_connection (&conns[host], addrs->addrs[host]))
+           continue;
+       conns[host].iov[0].iov_base = (char *) message_len_buf;
+       conns[host].iov[0].iov_len = 4;
+       conns[host].iov[1].iov_base = message->data;
+       conns[host].iov[1].iov_len = message->length;
+           
+       FD_SET(conns[host].fd, &select_state.rfds);
+       FD_SET(conns[host].fd, &select_state.wfds);
+       if (select_state.max <= conns[host].fd)
+           select_state.max = conns[host].fd + 1;
+       select_state.nfds++;
+
+       if (gettimeofday(&now, 0)) {
+           retval = errno;
+           dperror("gettimeofday");
+           goto egress;
+       }
+       select_state.end_time = now;
+       select_state.end_time.tv_sec++;
+       e = service_fds(&select_state, conns, host+1, &winning_conn);
+       if (e)
+           break;
+    }
+
+    if (select_state.nfds == 0) {
+       /* No addresses?  */
+       free(conns);
+       return KRB5_KDC_UNREACH;
+    }
+    if (e == 0) {
+       if (gettimeofday(&now, 0)) {
+           retval = errno;
+           dperror("gettimeofday");
+           goto egress;
+       }
+       select_state.end_time = now;
+       select_state.end_time.tv_sec += 30;
+       e = service_fds(&select_state, conns, n_conns, &winning_conn);
+    }
+    if (e == 0 || winning_conn < 0) {
+       retval = KRB5_KDC_UNREACH;
+       goto egress;
+    }
+    /* Success!  */
+    reply->data = conns[winning_conn].buf;
+    reply->length = conns[winning_conn].bufsize;
+    dfprintf((stderr, "returning %lu bytes in buffer %p\n",
+             (unsigned long) reply->length, reply->data));
+    retval = 0;
+    conns[winning_conn].buf = 0;
+egress:
+    for (i = 0; i < n_conns; i++) {
+       if (conns[i].fd != INVALID_SOCKET)
+           close(conns[i].fd);
+       if (conns[i].buf != 0)
+           free(conns[i].buf);
+    }
+    free(conns);
+    return retval;
+}
+#endif