Imported Upstream version 2.5.0
[debian/amanda] / common-src / krb5-security.c
diff --git a/common-src/krb5-security.c b/common-src/krb5-security.c
new file mode 100644 (file)
index 0000000..e1e78a0
--- /dev/null
@@ -0,0 +1,2233 @@
+/*
+ * Amanda, The Advanced Maryland Automatic Network Disk Archiver
+ * Copyright (c) 1999 University of Maryland
+ * All Rights Reserved.
+ *
+ * Permission to use, copy, modify, distribute, and sell this software and its
+ * documentation for any purpose is hereby granted without fee, provided that
+ * the above copyright notice appear in all copies and that both that
+ * copyright notice and this permission notice appear in supporting
+ * documentation, and that the name of U.M. not be used in advertising or
+ * publicity pertaining to distribution of the software without specific,
+ * written prior permission.  U.M. makes no representations about the
+ * suitability of this software for any purpose.  It is provided "as is"
+ * without express or implied warranty.
+ *
+ * U.M. DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL U.M.
+ * BE LIABLE FOR ANY SPECIAL, 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.
+ *
+ * Authors: the Amanda Development Team.  Its members are listed in a
+ * file named AUTHORS, in the root directory of this distribution.
+ */
+
+/*
+ * $Id: krb5-security.c,v 1.12 2006/02/21 04:13:55 ktill Exp $
+ *
+ * krb5-security.c - kerberos V5 security module
+ */
+
+#include "config.h"
+#ifdef KRB5_SECURITY
+#include "amanda.h"
+#include "arglist.h"
+#include "event.h"
+#include "packet.h"
+#include "queue.h"
+#include "security.h"
+#include "stream.h"
+#include "version.h"
+
+#define        BROKEN_MEMORY_CCACHE
+
+#ifdef BROKEN_MEMORY_CCACHE
+/*
+ * If you don't have atexit() or on_exit(), you could just consider
+ * making atexit() empty and clean up your ticket files some other
+ * way
+ */
+#ifndef HAVE_ATEXIT
+#ifdef HAVE_ON_EXIT
+#define atexit(func)    on_exit(func, 0)
+#else
+#define atexit(func)    (you must to resolve lack of atexit)
+#endif  /* HAVE_ON_EXIT */
+#endif  /* ! HAVE_ATEXIT */
+#endif
+
+#ifndef KRB5_HEIMDAL_INCLUDES
+#include <gssapi/gssapi_generic.h>
+#else
+#include <gssapi/gssapi.h>
+#endif
+#include <krb5.h>
+
+#ifndef KRB5_ENV_CCNAME
+#define        KRB5_ENV_CCNAME "KRB5CCNAME"
+#endif
+
+/*#define      KRB5_DEBUG*/
+
+#ifdef KRB5_DEBUG
+#define        k5printf(x)     dbprintf(x)
+#else
+#define        k5printf(x)
+#endif
+
+/*
+ * consider undefining when kdestroy() is fixed.  The current version does
+ * not work under krb5-1.2.4 in rh7.3, perhaps others.
+ */
+#define KDESTROY_VIA_UNLINK    1
+
+/*
+ * Define this if you want all network traffic encrypted.  This will
+ * extract a serious performance hit.
+ *
+ * It would be nice if we could do this on a filesystem-by-filesystem basis.
+ */
+/*#define      AMANDA_KRB5_ENCRYPT*/
+
+/*
+ * Where the keytab lives, if defined.  Otherwise it expects something in the
+ * config file.
+ */
+/* #define AMANDA_KEYTAB       "/.amanda-v5-keytab" */
+
+/*
+ * The name of the principal we authenticate with, if defined.  Otherwise
+ * it expects something in the config file.
+ */
+/* #define     AMANDA_PRINCIPAL        "service/amanda" */
+
+/*
+ * The lifetime of our tickets in seconds.  This may or may not need to be
+ * configurable.
+ */
+#define        AMANDA_TKT_LIFETIME     (12*60*60)
+
+/*
+ * The name of the service in /etc/services.  This probably shouldn't be
+ * configurable.
+ */
+#define        AMANDA_KRB5_SERVICE_NAME        "k5amanda"
+
+/*
+ * The default port to use if above entry in /etc/services doesn't exist
+ */
+#define        AMANDA_KRB5_DEFAULT_PORT        10082
+
+/*
+ * The timeout in seconds for each step of the GSS negotiation phase
+ */
+#define        GSS_TIMEOUT                     30
+
+/*
+ * The largest buffer we can send/receive.
+ */
+#define        AMANDA_MAX_TOK_SIZE             (MAX_TAPE_BLOCK_BYTES * 4)
+
+/*
+ * Magic values for krb5_conn->handle
+ */
+#define        H_EOF   -1              /* this connection has been shut down */
+
+/*
+ * This is the tcp stream buffer size
+ */
+#define        KRB5_STREAM_BUFSIZE     (MAX_TAPE_BLOCK_BYTES * 2)
+
+/*
+ * This is the max number of outgoing connections we can have at once.
+ * planner/amcheck/etc will open a bunch of connections as it tries
+ * to contact everything.  We need to limit this to avoid blowing
+ * the max number of open file descriptors a process can have.
+ */
+#define        AMANDA_KRB5_MAXCONN     40
+
+/*
+ * This is a frame read off of the connection.  Each frame has an
+ * associated handle and a gss_buffer which contains a len,value pair.
+ */
+struct krb5_frame {
+    int handle;                                /* proto handle */
+    gss_buffer_desc tok;               /* token */
+    TAILQ_ENTRY(krb5_frame) tq;                /* queue handle */
+};
+
+/*
+ * This is a krb5 connection to a host.  We should only have
+ * one connection per host.
+ */
+struct krb5_conn {
+    int fd;                                    /* tcp connection */
+    struct {                                   /* buffer read() calls */
+       char buf[KRB5_STREAM_BUFSIZE];          /* buffer */
+       size_t left;                    /* unread data */
+       ssize_t size;                   /* size of last read */
+    } readbuf;
+    enum { unauthed, authed } state;
+    event_handle_t *ev_read;                   /* read (EV_READFD) handle */
+    int ev_read_refcnt;                                /* number of readers */
+    char hostname[MAX_HOSTNAME_LENGTH+1];      /* human form of above */
+    char *errmsg;                              /* error passed up */
+    gss_ctx_id_t gss_context;                  /* GSSAPI context */
+    int refcnt;                                        /* number of handles using */
+    TAILQ_HEAD(, krb5_frame) frameq;           /* queue of read frames */
+    TAILQ_ENTRY(krb5_conn) tq;                 /* queue handle */
+};
+
+
+struct krb5_stream;
+
+/*
+ * This is the private handle data.
+ */
+struct krb5_handle {
+    security_handle_t sech;            /* MUST be first */
+    char *hostname;                    /* ptr to kc->hostname */
+    struct krb5_stream *ks;            /* virtual stream we xmit over */
+
+    union {
+       void (*recvpkt) P((void *, pkt_t *, security_status_t));
+                                       /* func to call when packet recvd */
+       void (*connect) P((void *, security_handle_t *, security_status_t));
+                                       /* func to call when connected */
+    } fn;
+    void *arg;                         /* argument to pass function */
+    event_handle_t *ev_wait;           /* wait handle for connects */
+    char *(*conf_fn) P((char *, void *)); /* used to get config info */
+    event_handle_t *ev_timeout;                /* timeout handle for recv */
+};
+
+/*
+ * This is the internal security_stream data for krb5.
+ */
+struct krb5_stream {
+    security_stream_t secstr;          /* MUST be first */
+    struct krb5_conn *kc;              /* physical connection */
+    int handle;                                /* protocol handle */
+    event_handle_t *ev_read;           /* read (EV_WAIT) event handle */
+    void (*fn) P((void *, void *, int));       /* read event fn */
+    void *arg;                         /* arg for previous */
+};
+
+/*
+ * Interface functions
+ */
+static int krb5_sendpkt P((void *, pkt_t *));
+static int krb5_stream_accept P((void *));
+static int krb5_stream_auth P((void *));
+static int krb5_stream_id P((void *));
+static int krb5_stream_write P((void *, const void *, size_t));
+static void *krb5_stream_client P((void *, int));
+static void *krb5_stream_server P((void *));
+static void krb5_accept P((int, int,
+    void (*)(security_handle_t *, pkt_t *)));
+static void krb5_close P((void *));
+static void krb5_connect P((const char *,
+    char *(*)(char *, void *),
+    void (*)(void *, security_handle_t *, security_status_t), void *));
+static void krb5_recvpkt P((void *,
+    void (*)(void *, pkt_t *, security_status_t), void *, int));
+static void krb5_recvpkt_cancel P((void *));
+static void krb5_stream_close P((void *));
+static void krb5_stream_read P((void *, void (*)(void *, void *, int),
+    void *));
+static void krb5_stream_read_cancel P((void *));
+
+/*
+ * This is our interface to the outside world.
+ */
+const security_driver_t krb5_security_driver = {
+    "krb5",
+    krb5_connect,
+    krb5_accept,
+    krb5_close,
+    krb5_sendpkt,
+    krb5_recvpkt,
+    krb5_recvpkt_cancel,
+    krb5_stream_server,
+    krb5_stream_accept,
+    krb5_stream_client,
+    krb5_stream_close,
+    krb5_stream_auth,
+    krb5_stream_id,
+    krb5_stream_write,
+    krb5_stream_read,
+    krb5_stream_read_cancel,
+};
+
+/*
+ * Cache the local hostname
+ */
+static char hostname[MAX_HOSTNAME_LENGTH+1];
+
+/*
+ * This is a queue of open connections
+ */
+static struct {
+    TAILQ_HEAD(, krb5_conn) tailq;
+    int qlength;
+} connq = {
+    TAILQ_HEAD_INITIALIZER(connq.tailq), 0
+};
+#define        connq_first()           TAILQ_FIRST(&connq.tailq)
+#define        connq_next(kc)          TAILQ_NEXT(kc, tq)
+#define        connq_append(kc)        do {                                    \
+    TAILQ_INSERT_TAIL(&connq.tailq, kc, tq);                           \
+    connq.qlength++;                                                   \
+} while (0)
+#define        connq_remove(kc)        do {                                    \
+    assert(connq.qlength > 0);                                         \
+    TAILQ_REMOVE(&connq.tailq, kc, tq);                                        \
+    connq.qlength--;                                                   \
+} while (0)
+
+static int newhandle = 1;
+
+/*
+ * This is a function that should be called if a new security_handle_t is
+ * created.  If NULL, no new handles are created.
+ * It is passed the new handle and the received pkt
+ */
+static void (*accept_fn) P((security_handle_t *, pkt_t *));
+
+/*
+ * Local functions
+ */
+static void init P((void));
+#ifdef BROKEN_MEMORY_CCACHE
+static void cleanup P((void));
+#endif
+static const char *get_tgt P((char *, char *));
+static void open_callback P((void *));
+static void connect_callback P((void *));
+static void connect_timeout P((void *));
+static int send_token P((struct krb5_conn *, int, const gss_buffer_desc *));
+static int recv_token P((struct krb5_conn *, int *, gss_buffer_desc *, int));
+static void recvpkt_callback P((void *, void *, ssize_t));
+static void recvpkt_timeout P((void *));
+static void stream_read_callback P((void *));
+static int gss_server P((struct krb5_conn *));
+static int gss_client P((struct krb5_handle *));
+static const char *gss_error P((OM_uint32, OM_uint32));
+
+#ifdef AMANDA_KRB5_ENCRYPT
+static int kdecrypt P((struct krb5_stream *, gss_buffer_desc *,
+    gss_buffer_desc *));
+static int kencrypt P((struct krb5_stream *, gss_buffer_desc *,
+    gss_buffer_desc *));
+#endif
+static struct krb5_conn *conn_get P((const char *));
+static void conn_put P((struct krb5_conn *));
+static void conn_read P((struct krb5_conn *));
+static void conn_read_cancel P((struct krb5_conn *));
+static void conn_read_callback P((void *));
+static int conn_run_frameq P((struct krb5_conn *, struct krb5_stream *));
+static int net_writev P((int, struct iovec *, int));
+static ssize_t net_read P((struct krb5_conn *, void *, size_t, int));
+static int net_read_fillbuf P((struct krb5_conn *, int));
+static char *krb5_checkuser(char *, char *, char *);
+static void parse_pkt P((pkt_t *, const void *, size_t));
+
+
+/*
+ * krb5 version of a security handle allocator.  Logically sets
+ * up a network "connection".
+ */
+static void
+krb5_connect(hostname, conf_fn, fn, arg)
+    const char *hostname;
+    char *(*conf_fn) P((char *, void *));
+    void (*fn) P((void *, security_handle_t *, security_status_t));
+    void *arg;
+{
+    struct krb5_handle *kh;
+    struct hostent *he;
+    struct servent *se;
+    int port, fd;
+    const char *err;
+    char *keytab_name = NULL;
+    char *principal_name = NULL;
+
+    assert(hostname != NULL);
+
+    k5printf(("krb5_connect: %s\n", hostname));
+
+    /*
+     * Make sure we're initted
+     */
+    init();
+
+    kh = alloc(sizeof(*kh));
+    security_handleinit(&kh->sech, &krb5_security_driver);
+    kh->hostname = NULL;
+    kh->ks = NULL;
+    kh->ev_wait = NULL;
+    kh->ev_timeout = NULL;
+
+#ifdef AMANDA_KEYTAB
+    keytab_name = AMANDA_KEYTAB;
+#else
+    if(conf_fn) {
+       keytab_name = conf_fn("krb5keytab", arg);
+    }
+#endif
+#ifdef AMANDA_PRINCIPAL
+    principal_name = AMANDA_PRINCIPAL;
+#else
+    if(conf_fn) {
+       principal_name = conf_fn("krb5principal", arg);
+    }
+#endif
+
+    if ((err = get_tgt(keytab_name, principal_name)) != NULL) {
+       security_seterror(&kh->sech, "%s: could not get TGT: %s",
+           hostname, err);
+       (*fn)(arg, &kh->sech, S_ERROR);
+       return;
+    }
+
+    if ((he = gethostbyname(hostname)) == NULL) {
+       security_seterror(&kh->sech,
+           "%s: could not resolve hostname", hostname);
+       (*fn)(arg, &kh->sech, S_ERROR);
+       return;
+    }
+    kh->fn.connect = fn;
+    kh->conf_fn = conf_fn;
+    kh->arg = arg;
+    kh->hostname = stralloc(he->h_name);
+    kh->ks = krb5_stream_client(kh, newhandle++);
+
+    if (kh->ks == NULL)
+       goto error;
+
+    fd = kh->ks->kc->fd;
+
+    if (fd < 0) {
+       /*
+        * We need to open a new connection.  See if we have too
+        * many connections open.
+        */
+       if (connq.qlength > AMANDA_KRB5_MAXCONN) {
+           k5printf(("krb5_connect: too many conections (%d), delaying %s\n",
+               connq.qlength, kh->hostname));
+           krb5_stream_close(kh->ks);
+           kh->ev_wait = event_register((event_id_t)open_callback,
+               EV_WAIT, open_callback, kh);
+           return;
+       }
+
+       if ((se = getservbyname(AMANDA_KRB5_SERVICE_NAME, "tcp")) == NULL)
+           port = htons(AMANDA_KRB5_DEFAULT_PORT);
+       else
+           port = se->s_port;
+
+       /*
+        * Get a non-blocking socket.
+        */
+       fd = stream_client(kh->hostname, ntohs(port), KRB5_STREAM_BUFSIZE,
+           KRB5_STREAM_BUFSIZE, NULL, 1);
+       if (fd < 0) {
+           security_seterror(&kh->sech,
+               "can't connect to %s:%d: %s", hostname, ntohs(port),
+               strerror(errno));
+           goto error;
+       }
+       kh->ks->kc->fd = fd;
+    }
+    /*
+     * The socket will be opened async so hosts that are down won't
+     * block everything.  We need to register a write event
+     * so we will know when the socket comes alive.
+     * We also register a timeout.
+     */
+    kh->ev_wait = event_register(fd, EV_WRITEFD, connect_callback, kh);
+    kh->ev_timeout = event_register(GSS_TIMEOUT, EV_TIME, connect_timeout, kh);
+
+    return;
+
+error:
+    (*fn)(arg, &kh->sech, S_ERROR);
+}
+
+/*
+ * Called when there are not too many connections open such that
+ * we can open more.
+ */
+static void
+open_callback(cookie)
+    void *cookie;
+{
+    struct krb5_handle *kh = cookie;
+
+    event_release(kh->ev_wait);
+
+    k5printf(("krb5: open_callback: possible connections available, retry %s\n",
+       kh->hostname));
+    krb5_connect(kh->hostname, kh->conf_fn, kh->fn.connect, kh->arg);
+    amfree(kh->hostname);
+    amfree(kh);
+}
+
+/*
+ * Called when a tcp connection is finished connecting and is ready
+ * to be authenticated.
+ */
+static void
+connect_callback(cookie)
+    void *cookie;
+{
+    struct krb5_handle *kh = cookie;
+
+    event_release(kh->ev_wait);
+    kh->ev_wait = NULL;
+    event_release(kh->ev_timeout);
+    kh->ev_timeout = NULL;
+
+    if (kh->ks->kc->state == unauthed) {
+       if (gss_client(kh) < 0) {
+           (*kh->fn.connect)(kh->arg, &kh->sech, S_ERROR);
+           return;
+       }
+       kh->ks->kc->state = authed;
+    }
+    assert(kh->ks->kc->gss_context != GSS_C_NO_CONTEXT);
+
+    (*kh->fn.connect)(kh->arg, &kh->sech, S_OK);
+}
+
+/*
+ * Called if a connection times out before completion.
+ */
+static void
+connect_timeout(cookie)
+    void *cookie;
+{
+    struct krb5_handle *kh = cookie;
+
+    event_release(kh->ev_wait);
+    kh->ev_wait = NULL;
+    event_release(kh->ev_timeout);
+    kh->ev_timeout = NULL;
+
+    (*kh->fn.connect)(kh->arg, &kh->sech, S_TIMEOUT);
+}
+
+/*
+ * Setup to handle new incoming connections
+ */
+static void
+krb5_accept(in, out, fn)
+    int in, out;
+    void (*fn) P((security_handle_t *, pkt_t *));
+{
+    struct sockaddr_in sin;
+    size_t len;
+    struct krb5_conn *kc;
+    struct hostent *he;
+
+    /*
+     * Make sure we're initted
+     */
+    init();
+
+    len = sizeof(sin);
+    if (getpeername(in, (struct sockaddr *)&sin, &len) < 0)
+       return;
+    he = gethostbyaddr((void *)&sin.sin_addr, sizeof(sin.sin_addr), AF_INET);
+    if (he == NULL)
+       return;
+
+    kc = conn_get(he->h_name);
+    kc->fd = in;
+    if (gss_server(kc) < 0)
+       error("gss_server failed: %s\n", kc->errmsg);
+    kc->state = authed;
+    accept_fn = fn;
+    conn_read(kc);
+}
+
+/*
+ * Locate an existing connection to the given host, or create a new,
+ * unconnected entry if none exists.  The caller is expected to check
+ * for the lack of a connection (kc->fd == -1) and set one up.
+ */
+static struct krb5_conn *
+conn_get(hostname)
+    const char *hostname;
+{
+    struct krb5_conn *kc;
+
+    k5printf(("krb5: conn_get: %s\n", hostname));
+
+    for (kc = connq_first(); kc != NULL; kc = connq_next(kc)) {
+       if (strcasecmp(hostname, kc->hostname) == 0)
+           break;
+    }
+
+    if (kc != NULL) {
+       kc->refcnt++;
+       k5printf(("krb5: conn_get: exists, refcnt to %s is now %d\n",
+           kc->hostname, kc->refcnt));
+       return (kc);
+    }
+
+    k5printf(("krb5: conn_get: creating new handle\n"));
+    /*
+     * We can't be creating a new handle if we are the client
+     */
+    assert(accept_fn == NULL);
+    kc = alloc(sizeof(*kc));
+    kc->fd = -1;
+    kc->readbuf.left = 0;
+    kc->readbuf.size = 0;
+    kc->state = unauthed;
+    kc->ev_read = NULL;
+    strncpy(kc->hostname, hostname, sizeof(kc->hostname) - 1);
+    kc->hostname[sizeof(kc->hostname) - 1] = '\0';
+    kc->errmsg = NULL;
+    kc->gss_context = GSS_C_NO_CONTEXT;
+    /*
+     * [XXX] this is set to 2 in order to force the connection to stay
+     * open and process more protocol requests.  (basically consistant
+     * with bsd-security.c, and theoretically krb4-security.c.   This
+     * needs to be addressed in a cleaner way.
+     */
+    kc->refcnt = 2;
+    TAILQ_INIT(&kc->frameq);
+    connq_append(kc);
+    return (kc);
+}
+
+/*
+ * Delete a reference to a connection, and close it if it is the last
+ * reference.
+ */
+static void
+conn_put(kc)
+    struct krb5_conn *kc;
+{
+    OM_uint32 min_stat;
+    struct krb5_frame *kf;
+
+    assert(kc->refcnt > 0);
+    if (--kc->refcnt > 0) {
+       k5printf(("krb5: conn_put: decrementing refcnt for %s to %d\n",
+           kc->hostname, kc->refcnt));
+       return;
+    }
+    k5printf(("krb5: conn_put: closing connection to %s\n", kc->hostname));
+    if (kc->fd != -1)
+       aclose(kc->fd);
+    if (kc->ev_read != NULL)
+       event_release(kc->ev_read);
+    if (kc->errmsg != NULL)
+       amfree(kc->errmsg);
+    gss_delete_sec_context(&min_stat, &kc->gss_context, GSS_C_NO_BUFFER);
+    while ((kf = TAILQ_FIRST(&kc->frameq)) != NULL) {
+       TAILQ_REMOVE(&kc->frameq, kf, tq);
+       if (kf->tok.value != NULL)
+           amfree(kf->tok.value);
+       amfree(kf);
+    }
+    connq_remove(kc);
+    amfree(kc);
+    /* signal that a connection is available */
+    event_wakeup((event_id_t)open_callback);
+}
+
+/*
+ * Turn on read events for a conn.  Or, increase a refcnt if we are
+ * already receiving read events.
+ */
+static void
+conn_read(kc)
+    struct krb5_conn *kc;
+{
+
+    if (kc->ev_read != NULL) {
+       kc->ev_read_refcnt++;
+       k5printf(("krb5: conn_read: incremented refcnt to %d for %s\n",
+           kc->ev_read_refcnt, kc->hostname));
+       return;
+    }
+    k5printf(("krb5: conn_read registering event handler for %s\n",
+       kc->hostname));
+    kc->ev_read = event_register(kc->fd, EV_READFD, conn_read_callback, kc);
+    kc->ev_read_refcnt = 1;
+}
+
+static void
+conn_read_cancel(kc)
+    struct krb5_conn *kc;
+{
+
+    if (--kc->ev_read_refcnt > 0) {
+       k5printf(("krb5: conn_read_cancel: decremented refcnt to %d for %s\n",
+           kc->ev_read_refcnt, kc->hostname));
+       return;
+    }
+    k5printf(("krb5: conn_read_cancel: releasing event handler for %s\n",
+       kc->hostname));
+    event_release(kc->ev_read);
+    kc->ev_read = NULL;
+}
+
+/*
+ * frees a handle allocated by the above
+ */
+static void
+krb5_close(inst)
+    void *inst;
+{
+    struct krb5_handle *kh = inst;
+
+    assert(kh != NULL);
+
+    k5printf(("krb5: closing handle to %s\n", kh->hostname));
+
+    if (kh->ks != NULL) {
+       /* This may be null if we get here on an error */
+       krb5_recvpkt_cancel(kh);
+       security_stream_close(&kh->ks->secstr);
+    }
+    amfree(kh->hostname);
+    amfree(kh);
+}
+
+/*
+ * Transmit a packet.  Encrypt first.
+ */
+static int
+krb5_sendpkt(cookie, pkt)
+    void *cookie;
+    pkt_t *pkt;
+{
+    struct krb5_handle *kh = cookie;
+    gss_buffer_desc tok;
+    int rval;
+    unsigned char c, *buf;
+
+    assert(kh != NULL);
+    assert(pkt != NULL);
+
+    k5printf(("krb5: sendpkt: enter\n"));
+
+    if (pkt->body[0] == '\0') {
+       c = (unsigned char)pkt->type;
+       tok.length = 1;
+       tok.value = &c;
+    } else {
+       tok.length = strlen(pkt->body) + 2;
+       tok.value = alloc(tok.length);
+       buf = tok.value;
+       *buf++ = (unsigned char)pkt->type;
+       strncpy(buf, pkt->body, tok.length - 2);
+       buf[tok.length - 2] = '\0';
+    }
+
+    k5printf(("krb5: sendpkt: %s (%d) pkt_t (len %d) contains:\n\n\"%s\"\n\n",
+       pkt_type2str(pkt->type), pkt->type, strlen(pkt->body), pkt->body));
+
+    rval = krb5_stream_write(kh->ks, tok.value, tok.length);
+    if (rval < 0)
+       security_seterror(&kh->sech, security_stream_geterror(&kh->ks->secstr));
+    if (tok.length > 1)
+       amfree(tok.value);
+    return (rval);
+}
+
+/*
+ * Set up to receive a packet asyncronously, and call back when
+ * it has been read.
+ */
+static void
+krb5_recvpkt(cookie, fn, arg, timeout)
+    void *cookie, *arg;
+    void (*fn) P((void *, pkt_t *, security_status_t));
+    int timeout;
+{
+    struct krb5_handle *kh = cookie;
+
+    assert(kh != NULL);
+
+    k5printf(("krb5: recvpkt registered for %s\n", kh->hostname));
+
+    /*
+     * Reset any pending timeout on this handle
+     */
+    if (kh->ev_timeout != NULL)
+       event_release(kh->ev_timeout);
+
+    /*
+     * Negative timeouts mean no timeout
+     */
+    if (timeout < 0)
+       kh->ev_timeout = NULL;
+    else
+       kh->ev_timeout = event_register(timeout, EV_TIME, recvpkt_timeout, kh);
+
+    kh->fn.recvpkt = fn;
+    kh->arg = arg;
+    krb5_stream_read(kh->ks, recvpkt_callback, kh);
+}
+
+/*
+ * Remove a async receive request from the queue
+ */
+static void
+krb5_recvpkt_cancel(cookie)
+    void *cookie;
+{
+    struct krb5_handle *kh = cookie;
+
+    k5printf(("krb5: cancelling recvpkt for %s\n", kh->hostname));
+
+    assert(kh != NULL);
+
+    krb5_stream_read_cancel(kh->ks);
+    if (kh->ev_timeout != NULL) {
+       event_release(kh->ev_timeout);
+       kh->ev_timeout = NULL;
+    }
+}
+
+/*
+ * This is called when a handle is woken up because data read off of the
+ * net is for it.
+ */
+static void
+recvpkt_callback(cookie, buf, bufsize)
+    void *cookie, *buf;
+    ssize_t bufsize;
+{
+    pkt_t pkt;
+    struct krb5_handle *kh = cookie;
+
+    assert(kh != NULL);
+
+    /*
+     * We need to cancel the recvpkt request before calling
+     * the callback because the callback may reschedule us.
+     */
+    krb5_recvpkt_cancel(kh);
+
+    switch (bufsize) {
+    case 0:
+       security_seterror(&kh->sech,
+           "EOF on read from %s", kh->hostname);
+       (*kh->fn.recvpkt)(kh->arg, NULL, S_ERROR);
+       return;
+    case -1:
+       security_seterror(&kh->sech, security_stream_geterror(&kh->ks->secstr));
+       (*kh->fn.recvpkt)(kh->arg, NULL, S_ERROR);
+       return;
+    default:
+       parse_pkt(&pkt, buf, bufsize);
+       k5printf(("krb5: received %s pkt (%d) from %s, contains:\n\n\"%s\"\n\n",
+           pkt_type2str(pkt.type), pkt.type, kh->hostname, pkt.body));
+       (*kh->fn.recvpkt)(kh->arg, &pkt, S_OK);
+       return;
+    }
+}
+
+/*
+ * This is called when a handle times out before receiving a packet.
+ */
+static void
+recvpkt_timeout(cookie)
+    void *cookie;
+{
+    struct krb5_handle *kh = cookie;
+
+    assert(kh != NULL);
+
+    k5printf(("krb5: recvpkt timeout for %s\n", kh->hostname));
+
+    krb5_recvpkt_cancel(kh);
+    (*kh->fn.recvpkt)(kh->arg, NULL, S_TIMEOUT);
+}
+
+/*
+ * Create the server end of a stream.  For krb5, this means setup a stream
+ * object and allocate a new handle for it.
+ */
+static void *
+krb5_stream_server(h)
+    void *h;
+{
+    struct krb5_handle *kh = h;
+    struct krb5_stream *ks;
+
+    assert(kh != NULL);
+
+    ks = alloc(sizeof(*ks));
+    security_streaminit(&ks->secstr, &krb5_security_driver);
+    ks->kc = conn_get(kh->hostname);
+    /*
+     * Stream should already be setup!
+     */
+    if (ks->kc->fd < 0) {
+       conn_put(ks->kc);
+       amfree(ks);
+       security_seterror(&kh->sech, "lost connection");
+       return (NULL);
+    }
+    /*
+     * so as not to conflict with the amanda server's handle numbers,
+     * we start at 5000 and work down
+     */
+    ks->handle = 5000 - newhandle++;
+    ks->ev_read = NULL;
+    k5printf(("krb5: stream_server: created stream %d\n", ks->handle));
+    return (ks);
+}
+
+/*
+ * Accept an incoming connection on a stream_server socket
+ * Nothing needed for krb5.
+ */
+static int
+krb5_stream_accept(s)
+    void *s;
+{
+
+    return (0);
+}
+
+/*
+ * Return a connected stream.  For krb5, this means setup a stream
+ * with the supplied handle.
+ */
+static void *
+krb5_stream_client(h, id)
+    void *h;
+    int id;
+{
+    struct krb5_handle *kh = h;
+    struct krb5_stream *ks;
+
+    assert(kh != NULL);
+
+    if (id <= 0) {
+       security_seterror(&kh->sech,
+           "%d: invalid security stream id", id);
+       return (NULL);
+    }
+
+    ks = alloc(sizeof(*ks));
+    security_streaminit(&ks->secstr, &krb5_security_driver);
+    ks->handle = id;
+    ks->ev_read = NULL;
+    ks->kc = conn_get(kh->hostname);
+
+    k5printf(("krb5: stream_client: connected to stream %d\n", id));
+
+    return (ks);
+}
+
+/*
+ * Close and unallocate resources for a stream.
+ */
+static void
+krb5_stream_close(s)
+    void *s;
+{
+    struct krb5_stream *ks = s;
+
+    assert(ks != NULL);
+
+    k5printf(("krb5: stream_close: closing stream %d\n", ks->handle));
+
+    krb5_stream_read_cancel(ks);
+    conn_put(ks->kc);
+    amfree(ks);
+}
+
+/*
+ * Authenticate a stream
+ * Nothing needed for krb5.  The tcp connection is authenticated
+ * on startup.
+ */
+static int
+krb5_stream_auth(s)
+    void *s;
+{
+
+    return (0);
+}
+
+/*
+ * Returns the stream id for this stream.  This is just the local
+ * port.
+ */
+static int
+krb5_stream_id(s)
+    void *s;
+{
+    struct krb5_stream *ks = s;
+
+    assert(ks != NULL);
+
+    return (ks->handle);
+}
+
+/*
+ * Write a chunk of data to a stream.  Blocks until completion.
+ */
+static int
+krb5_stream_write(s, buf, size)
+    void *s;
+    const void *buf;
+    size_t size;
+{
+    struct krb5_stream *ks = s;
+    gss_buffer_desc tok;
+#ifdef AMANDA_KRB5_ENCRYPT
+    gss_buffer_desc enctok;
+    OM_uint32 min_stat;
+#endif
+    int rc;
+
+    assert(ks != NULL);
+
+    k5printf(("krb5: stream_write: writing %d bytes to %s:%d\n", size,
+       ks->kc->hostname, ks->handle));
+
+    tok.length = size;
+    tok.value = (void *)buf;   /* safe to discard const */
+#ifdef AMANDA_KRB5_ENCRYPT
+    if (kencrypt(ks, &tok, &enctok) < 0)
+       return (-1);
+    rc = send_token(ks->kc, ks->handle, &enctok);
+#else
+    rc = send_token(ks->kc, ks->handle, &tok);
+#endif
+    if (rc < 0)
+       security_stream_seterror(&ks->secstr, ks->kc->errmsg);
+#ifdef AMANDA_KRB5_ENCRYPT
+    gss_release_buffer(&min_stat, &enctok);
+#endif
+    return (rc);
+}
+
+/*
+ * Submit a request to read some data.  Calls back with the given
+ * function and arg when completed.
+ */
+static void
+krb5_stream_read(s, fn, arg)
+    void *s, *arg;
+    void (*fn) P((void *, void *, int));
+{
+    struct krb5_stream *ks = s;
+
+    assert(ks != NULL);
+
+    /*
+     * Only one read request can be active per stream.
+     */
+    ks->fn = fn;
+    ks->arg = arg;
+
+    /*
+     * First see if there's any queued frames for this stream.
+     * If so, we're done.
+     */
+    if (conn_run_frameq(ks->kc, ks) > 0)
+       return;
+
+    if (ks->ev_read == NULL) {
+       ks->ev_read = event_register((event_id_t)ks->kc, EV_WAIT,
+           stream_read_callback, ks);
+       conn_read(ks->kc);
+    }
+}
+
+/*
+ * Cancel a previous stream read request.  It's ok if we didn't have a read
+ * scheduled.
+ */
+static void
+krb5_stream_read_cancel(s)
+    void *s;
+{
+    struct krb5_stream *ks = s;
+
+    assert(ks != NULL);
+
+    if (ks->ev_read != NULL) {
+       event_release(ks->ev_read);
+       ks->ev_read = NULL;
+       conn_read_cancel(ks->kc);
+    }
+}
+
+/*
+ * Callback for krb5_stream_read
+ */
+static void
+stream_read_callback(arg)
+    void *arg;
+{
+    struct krb5_stream *ks = arg;
+
+    assert(ks != NULL);
+
+    k5printf(("krb5: stream_read_callback: handle %d\n", ks->handle));
+
+    conn_run_frameq(ks->kc, ks);
+}
+
+/*
+ * Run down a list of queued frames for a krb5_conn, and if we find one
+ * that matches the passed handle, fire the read event.  Only
+ * process one frame.
+ *
+ * Returns 1 if a frame was found and processed.
+ */
+static int
+conn_run_frameq(kc, ks)
+    struct krb5_conn *kc;
+    struct krb5_stream *ks;
+{
+    struct krb5_frame *kf, *nextkf;
+    gss_buffer_desc *enctok, *dectok;
+#ifdef AMANDA_KRB5_ENCRYPT
+    OM_uint32 min_stat;
+    gss_buffer_desc tok;
+#endif
+
+    /*
+     * Iterate through all of the frames in the queue.  If one
+     * is for us, process it.  If we hit an EOF frame, shut down.
+     * Stop after processing one frame, because we are only supposed
+     * to return one read request.
+     */
+    for (kf = TAILQ_FIRST(&kc->frameq); kf != NULL; kf = nextkf) {
+       nextkf = TAILQ_NEXT(kf, tq);
+
+       if (kf->handle != ks->handle && kf->handle != H_EOF) {
+           k5printf(("krb5: conn_frameq_run: not for us (handle %d)\n",
+               kf->handle));
+           continue;
+       }
+       /*
+        * We want all listeners to see the EOF, so never remove it.
+        * It will get cleaned up when the connection is closed
+        * in conn_put().
+        */
+       if (kf->handle != H_EOF)
+           TAILQ_REMOVE(&kc->frameq, kf, tq);
+
+       /*
+        * Remove the event first, and then call the callback.
+        * We remove it first because we don't want to get in their
+        * way if they reschedule it.
+        */
+       krb5_stream_read_cancel(ks);
+
+       enctok = &kf->tok;
+
+       if (enctok->length == 0) {
+           assert(kf->handle == H_EOF);
+           k5printf(("krb5: stream_read_callback: EOF\n"));
+           (*ks->fn)(ks->arg, NULL, 0);
+           return (1); /* stop after EOF */
+       }
+
+#ifdef AMANDA_KRB5_ENCRYPT
+       dectok = &tok;
+       if (kdecrypt(ks, enctok, &tok) < 0) {
+           k5printf(("krb5: stream_read_callback: kdecrypt error\n"));
+           (*ks->fn)(ks->arg, NULL, -1);
+       } else
+#else
+       dectok = enctok;
+#endif
+       {
+           k5printf(("krb5: stream_read_callback: read %d bytes from %s:%d\n",
+               dectok->length, ks->kc->hostname, ks->handle));
+           (*ks->fn)(ks->arg, dectok->value, dectok->length);
+#ifdef AMANDA_KRB5_ENCRYPT
+           gss_release_buffer(&min_stat, dectok);
+#endif
+       }
+       amfree(enctok->value);
+       amfree(kf);
+       return (1);             /* stop after one frame */
+    }
+    return (0);
+}
+
+/*
+ * The callback for the netfd for the event handler
+ * Determines if this packet is for this security handle,
+ * and does the real callback if so.
+ */
+static void
+conn_read_callback(cookie)
+    void *cookie;
+{
+    struct krb5_conn *kc = cookie;
+    struct krb5_handle *kh;
+    struct krb5_frame *kf;
+    pkt_t pkt;
+    gss_buffer_desc *dectok;
+    int rc;
+#ifdef AMANDA_KRB5_ENCRYPT
+    gss_buffer_desc tok;
+    OM_uint32 min_stat;
+#endif
+
+    assert(cookie != NULL);
+
+    k5printf(("krb5: conn_read_callback\n"));
+
+    kf = alloc(sizeof(*kf));
+    TAILQ_INSERT_TAIL(&kc->frameq, kf, tq);
+
+    /* Read the data off the wire.  If we get errors, shut down. */
+    rc = recv_token(kc, &kf->handle, &kf->tok, 5);
+    k5printf(("krb5: conn_read_callback: recv_token returned %d handle = %d\n",
+       rc, kf->handle));
+    if (rc <= 0) {
+       kf->tok.value = NULL;
+       kf->tok.length = 0;
+       kf->handle = H_EOF;
+       rc = event_wakeup((event_id_t)kc);
+       k5printf(("krb5: conn_read_callback: event_wakeup return %d\n", rc));
+       return;
+    }
+
+    /* If there are events waiting on this handle, we're done */
+    rc = event_wakeup((event_id_t)kc);
+    k5printf(("krb5: conn_read_callback: event_wakeup return %d\n", rc));
+    if (rc > 0)
+       return;
+
+    /*
+     * If there is no accept fn registered, then just leave the
+     * packet queued.  The caller may register a function later.
+     */
+    if (accept_fn == NULL) {
+       k5printf(("krb5: no accept_fn so leaving packet queued.\n"));
+       return;
+    }
+
+    kh = alloc(sizeof(*kh));
+    security_handleinit(&kh->sech, &krb5_security_driver);
+    kh->hostname = stralloc(kc->hostname);
+    kh->ks = krb5_stream_client(kh, kf->handle);
+    kh->ev_wait = NULL;
+    kh->ev_timeout = NULL;
+
+    TAILQ_REMOVE(&kc->frameq, kf, tq);
+    k5printf(("krb5: new connection\n"));
+#ifdef AMANDA_KRB5_ENCRYPT
+    dectok = &tok;
+    rc = kdecrypt(kh->ks, &kf->tok, dectok);
+#else
+    dectok = &kf->tok;
+#endif
+
+#ifdef AMANDA_KRB5_ENCRYPT
+    if (rc < 0) {
+       security_seterror(&kh->sech, security_geterror(&kh->ks->secstr));
+       (*accept_fn)(&kh->sech, NULL);
+    } else
+#endif
+    {
+       parse_pkt(&pkt, dectok->value, dectok->length);
+#ifdef AMANDA_KRB5_ENCRYPT
+       gss_release_buffer(&min_stat, dectok);
+#endif
+       (*accept_fn)(&kh->sech, &pkt);
+    }
+    amfree(kf->tok.value);
+    amfree(kf);
+
+    /*
+     * We can only accept one connection per process, since we're tcp
+     * based and run out of inetd.  So, delete our accept reference once
+     * we've gotten the first connection.
+     */
+
+    /*
+     * [XXX] actually, the protocol has been changed to have multiple
+     * requests in one session be possible.  By not resetting accept_fn,
+     * this will caused them to be properly processed.  this needs to be
+     * addressed in a much cleaner way.
+     */
+    if (accept_fn != NULL)
+       conn_put(kc);
+    /* accept_fn = NULL; */
+}
+
+/*
+ * Negotiate a krb5 gss context from the client end.
+ */
+static int
+gss_client(kh)
+    struct krb5_handle *kh;
+{
+    struct krb5_stream *ks = kh->ks;
+    struct krb5_conn *kc = ks->kc;
+    gss_buffer_desc send_tok, recv_tok;
+    OM_uint32 maj_stat, min_stat;
+    unsigned int ret_flags;
+    int rc, rval = -1;
+    gss_name_t gss_name;
+
+    k5printf(("gss_client\n"));
+
+    send_tok.value = vstralloc("host/", ks->kc->hostname, NULL);
+    send_tok.length = strlen(send_tok.value) + 1;
+    maj_stat = gss_import_name(&min_stat, &send_tok, GSS_C_NULL_OID,
+       &gss_name);
+    if (maj_stat != GSS_S_COMPLETE) {
+       security_seterror(&kh->sech, "can't import name %s: %s",
+           (char *)send_tok.value, gss_error(maj_stat, min_stat));
+       amfree(send_tok.value);
+       return (-1);
+    }
+    amfree(send_tok.value);
+    kc->gss_context = GSS_C_NO_CONTEXT;
+
+    /*
+     * Perform the context-establishement loop.
+     *
+     * Every generated token is stored in send_tok which is then
+     * transmitted to the server; every received token is stored in
+     * recv_tok (empty on the first pass) to be processed by
+     * the next call to gss_init_sec_context.
+     * 
+     * GSS-API guarantees that send_tok's length will be non-zero
+     * if and only if the server is expecting another token from us,
+     * and that gss_init_sec_context returns GSS_S_CONTINUE_NEEDED if
+     * and only if the server has another token to send us.
+     */
+
+    for (recv_tok.length = 0;;) {
+       min_stat = 0;
+       maj_stat = gss_init_sec_context(&min_stat,
+           GSS_C_NO_CREDENTIAL,
+           &kc->gss_context,
+           gss_name,
+           GSS_C_NULL_OID,
+           GSS_C_MUTUAL_FLAG|GSS_C_REPLAY_FLAG,
+           0, NULL,    /* no channel bindings */
+           (recv_tok.length == 0 ? GSS_C_NO_BUFFER : &recv_tok),
+           NULL,       /* ignore mech type */
+           &send_tok,
+           &ret_flags,
+           NULL);      /* ignore time_rec */
+
+       if (recv_tok.length != 0) {
+           amfree(recv_tok.value);
+           recv_tok.length = 0;
+       }
+
+       if (maj_stat != GSS_S_COMPLETE && maj_stat != GSS_S_CONTINUE_NEEDED) {
+           security_seterror(&kh->sech,
+               "error getting gss context: %s",
+               gss_error(maj_stat, min_stat));
+           goto done;
+       }
+
+       /*
+        * Send back the response
+        */
+       if (send_tok.length != 0 && send_token(kc, ks->handle, &send_tok) < 0) {
+           security_seterror(&kh->sech, kc->errmsg);
+           gss_release_buffer(&min_stat, &send_tok);
+           goto done;
+       }
+       gss_release_buffer(&min_stat, &send_tok);
+
+       /*
+        * If we need to continue, then register for more packets
+        */
+       if (maj_stat != GSS_S_CONTINUE_NEEDED)
+           break;
+
+       if ((rc = recv_token(kc, NULL, &recv_tok, GSS_TIMEOUT)) <= 0) {
+           if (rc < 0)
+               security_seterror(&kh->sech,
+                   "recv error in gss loop: %s", kc->errmsg);
+           else
+               security_seterror(&kh->sech, "EOF in gss loop");
+           goto done;
+       }
+    }
+
+    rval = 0;
+done:
+    gss_release_name(&min_stat, &gss_name);
+    return (rval);
+}
+
+/*
+ * Negotiate a krb5 gss context from the server end.
+ */
+static int
+gss_server(kc)
+    struct krb5_conn *kc;
+{
+    OM_uint32 maj_stat, min_stat, ret_flags;
+    gss_buffer_desc send_tok, recv_tok;
+    gss_OID doid;
+    gss_name_t gss_name;
+    gss_cred_id_t gss_creds;
+    char *p, *realm, *msg;
+    uid_t euid;
+    int rc, rval = -1;
+    char errbuf[256];
+
+    k5printf(("gss_server\n"));
+
+    assert(kc != NULL);
+
+    /*
+     * We need to be root while in gss_acquire_cred() to read the host key
+     * out of the default keytab.  We also need to be root in
+     * gss_accept_context() thanks to the replay cache code.
+     */
+    euid = geteuid();
+    if (getuid() != 0) {
+       snprintf(errbuf, sizeof(errbuf),
+           "real uid is %ld, needs to be 0 to read krb5 host key",
+           (long)getuid());
+       goto out;
+    }
+    if (seteuid(0) < 0) {
+       snprintf(errbuf, sizeof(errbuf),
+           "can't seteuid to uid 0: %s", strerror(errno));
+       goto out;
+    }
+
+    send_tok.value = vstralloc("host/", hostname, NULL);
+    send_tok.length = strlen(send_tok.value) + 1;
+    for (p = send_tok.value; *p != '\0'; p++) {
+       if (isupper((int)*p))
+           *p = tolower(*p);
+    }
+    maj_stat = gss_import_name(&min_stat, &send_tok, GSS_C_NULL_OID,
+       &gss_name);
+    if (maj_stat != GSS_S_COMPLETE) {
+       seteuid(euid);
+       snprintf(errbuf, sizeof(errbuf),
+           "can't import name %s: %s", (char *)send_tok.value,
+           gss_error(maj_stat, min_stat));
+       amfree(send_tok.value);
+       goto out;
+    }
+    amfree(send_tok.value);
+
+    maj_stat = gss_acquire_cred(&min_stat, gss_name, 0,
+       GSS_C_NULL_OID_SET, GSS_C_ACCEPT, &gss_creds, NULL, NULL);
+    if (maj_stat != GSS_S_COMPLETE) {
+       snprintf(errbuf, sizeof(errbuf),
+           "can't acquire creds for host key host/%s: %s", hostname,
+           gss_error(maj_stat, min_stat));
+       gss_release_name(&min_stat, &gss_name);
+       seteuid(euid);
+       goto out;
+    }
+    gss_release_name(&min_stat, &gss_name);
+
+    for (recv_tok.length = 0;;) {
+       if ((rc = recv_token(kc, NULL, &recv_tok, GSS_TIMEOUT)) <= 0) {
+           if (rc < 0) {
+               snprintf(errbuf, sizeof(errbuf),
+                   "recv error in gss loop: %s", kc->errmsg);
+               amfree(kc->errmsg);
+           } else
+               snprintf(errbuf, sizeof(errbuf), "EOF in gss loop");
+           goto out;
+       }
+
+       maj_stat = gss_accept_sec_context(&min_stat, &kc->gss_context,
+           gss_creds, &recv_tok, GSS_C_NO_CHANNEL_BINDINGS,
+           &gss_name, &doid, &send_tok, &ret_flags, NULL, NULL);
+
+       if (maj_stat != GSS_S_COMPLETE &&
+           maj_stat != GSS_S_CONTINUE_NEEDED) {
+           snprintf(errbuf, sizeof(errbuf),
+               "error accepting context: %s", gss_error(maj_stat, min_stat));
+           amfree(recv_tok.value);
+           goto out;
+       }
+       amfree(recv_tok.value);
+
+       if (send_tok.length > 0 && send_token(kc, 0, &send_tok) < 0) {
+           strncpy(errbuf, kc->errmsg, sizeof(errbuf) - 1);
+           errbuf[sizeof(errbuf) - 1] = '\0';
+           amfree(kc->errmsg);
+           gss_release_buffer(&min_stat, &send_tok);
+           goto out;
+       }
+       gss_release_buffer(&min_stat, &send_tok);
+
+
+       /*
+        * If we need to get more from the client, then register for
+        * more packets.
+        */
+       if (maj_stat != GSS_S_CONTINUE_NEEDED)
+           break;
+    }
+
+    maj_stat = gss_display_name(&min_stat, gss_name, &send_tok, &doid);
+    if (maj_stat != GSS_S_COMPLETE) {
+       snprintf(errbuf, sizeof(errbuf),
+           "can't display gss name: %s", gss_error(maj_stat, min_stat));
+       gss_release_name(&min_stat, &gss_name);
+       goto out;
+    }
+    gss_release_name(&min_stat, &gss_name);
+
+    /* get rid of the realm */
+    if ((p = strchr(send_tok.value, '@')) == NULL) {
+       snprintf(errbuf, sizeof(errbuf),
+           "malformed gss name: %s", (char *)send_tok.value);
+       amfree(send_tok.value);
+       goto out;
+    }
+    *p = '\0';
+    realm = ++p;
+
+    /* 
+     * If the principal doesn't match, complain
+     */
+    if ((msg = krb5_checkuser(kc->hostname, send_tok.value, realm)) != NULL) {
+       snprintf(errbuf, sizeof(errbuf),
+           "access not allowed from %s: %s", (char *)send_tok.value, msg);
+       amfree(send_tok.value);
+       goto out;
+    }
+    amfree(send_tok.value);
+
+    rval = 0;
+out:
+    seteuid(euid);
+    if (rval != 0)
+       kc->errmsg = stralloc(errbuf);
+    k5printf(("gss_server returning %d\n", rval));
+    return (rval);
+}
+
+/*
+ * Setup some things about krb5.  This should only be called once.
+ */
+static void
+init()
+{
+    static int beenhere = 0;
+    struct hostent *he;
+    char *p;
+    int krb5_setenv P((const char *, const char *, int));
+
+    if (beenhere)
+       return;
+    beenhere = 1;
+
+#ifndef BROKEN_MEMORY_CCACHE
+    krb5_setenv(KRB5_ENV_CCNAME, "MEMORY:amanda_ccache", 1);
+#else
+    /*
+     * MEMORY ccaches seem buggy and cause a lot of internal heap
+     * corruption.  malloc has been known to core dump.  This behavior
+     * has been witnessed in Cygnus' kerbnet 1.2, MIT's krb V 1.0.5 and
+     * MIT's krb V -current as of 3/17/1999.
+     *
+     * We just use a lame ccache scheme with a uid suffix.
+     */
+    atexit(cleanup);
+    {
+       char ccache[64];
+       snprintf(ccache, sizeof(ccache), "FILE:/tmp/amanda_ccache.%ld.%ld",
+           (long)geteuid(), (long)getpid());
+       krb5_setenv(KRB5_ENV_CCNAME, ccache, 1);
+    }
+#endif
+
+    gethostname(hostname, sizeof(hostname) - 1);
+    hostname[sizeof(hostname) - 1] = '\0';
+    /*
+     * In case it isn't fully qualified, do a DNS lookup.
+     */
+    if ((he = gethostbyname(hostname)) != NULL)
+       strncpy(hostname, he->h_name, sizeof(hostname) - 1);
+
+    /*
+     * Lowercase the results.  We assume all host/ principals will be
+     * lowercased.
+     */
+    for (p = hostname; *p != '\0'; p++) {
+       if (isupper((int)*p))
+           *p = tolower(*p);
+    }
+}
+
+#ifdef BROKEN_MEMORY_CCACHE
+static void
+cleanup()
+{
+#ifdef KDESTROY_VIA_UNLINK
+    char ccache[64];
+    snprintf(ccache, sizeof(ccache), "/tmp/amanda_ccache.%ld.%ld",
+        (long)geteuid(), (long)getpid());
+    unlink(ccache);
+#else
+    kdestroy();
+#endif
+}
+#endif
+
+/*
+ * Get a ticket granting ticket and stuff it in the cache
+ */
+static const char *
+get_tgt(keytab_name, principal_name)
+       char *keytab_name, *principal_name;
+{
+    krb5_context context;
+    krb5_error_code ret;
+    krb5_principal client = NULL, server = NULL;
+    krb5_creds creds;
+    krb5_keytab keytab = NULL;
+    krb5_ccache ccache;
+    krb5_timestamp now;
+    krb5_data tgtname = { 0, KRB5_TGS_NAME_SIZE, KRB5_TGS_NAME };
+    static char *error = NULL;
+
+    if (error != NULL) {
+       amfree(error);
+       error = NULL;
+    }
+
+    if ((ret = krb5_init_context(&context)) != 0) {
+       error = vstralloc("error initializing krb5 context: ",
+           error_message(ret), NULL);
+       return (error);
+    }
+
+    krb5_init_ets(context);
+
+    if(!keytab_name) {
+        error = vstralloc("error  -- no krb5 keytab defined", NULL);
+        return(error);
+    }
+
+    if(!principal_name) {
+        error = vstralloc("error  -- no krb5 principal defined", NULL);
+        return(error);
+    }
+
+    /*
+     * Resolve keytab file into a keytab object
+     */
+    if ((ret = krb5_kt_resolve(context, keytab_name, &keytab)) != 0) {
+       error = vstralloc("error resolving keytab ", keytab, ": ",
+           error_message(ret), NULL);
+       return (error);
+    }
+
+    /*
+     * Resolve the amanda service held in the keytab into a principal
+     * object
+     */
+    ret = krb5_parse_name(context, principal_name, &client);
+    if (ret != 0) {
+       error = vstralloc("error parsing ", principal_name, ": ",
+           error_message(ret), NULL);
+       return (error);
+    }
+
+    ret = krb5_build_principal_ext(context, &server,
+       krb5_princ_realm(context, client)->length,
+       krb5_princ_realm(context, client)->data,
+       tgtname.length, tgtname.data,
+       krb5_princ_realm(context, client)->length,
+       krb5_princ_realm(context, client)->data,
+       0);
+    if (ret != 0) {
+       error = vstralloc("error while building server name: ",
+           error_message(ret), NULL);
+       return (error);
+    }
+
+    ret = krb5_timeofday(context, &now);
+    if (ret != 0) {
+       error = vstralloc("error getting time of day: ", error_message(ret),
+           NULL);
+       return (error);
+    }
+
+    memset(&creds, 0, sizeof(creds));
+    creds.times.starttime = 0;
+    creds.times.endtime = now + AMANDA_TKT_LIFETIME;
+
+    creds.client = client;
+    creds.server = server;
+
+    /*
+     * Get a ticket for the service, using the keytab
+     */
+    ret = krb5_get_in_tkt_with_keytab(context, 0, NULL, NULL, NULL,
+       keytab, 0, &creds, 0);
+
+    if (ret != 0) {
+       error = vstralloc("error getting ticket for ", principal_name,
+           ": ", error_message(ret), NULL);
+       goto cleanup2;
+    }
+
+    if ((ret = krb5_cc_default(context, &ccache)) != 0) {
+       error = vstralloc("error initializing ccache: ", error_message(ret),
+           NULL);
+       goto cleanup;
+    }
+    if ((ret = krb5_cc_initialize(context, ccache, client)) != 0) {
+       error = vstralloc("error initializing ccache: ", error_message(ret),
+           NULL);
+       goto cleanup;
+    }
+    if ((ret = krb5_cc_store_cred(context, ccache, &creds)) != 0) {
+       error = vstralloc("error storing creds in ccache: ",
+           error_message(ret), NULL);
+       /* FALLTHROUGH */
+    }
+    krb5_cc_close(context, ccache);
+cleanup:
+    krb5_free_cred_contents(context, &creds);
+cleanup2:
+#if 0
+    krb5_free_principal(context, client);
+    krb5_free_principal(context, server);
+#endif
+    krb5_free_context(context);
+    return (error);
+}
+
+/*
+ * get rid of tickets
+ */
+kdestroy()
+{
+    krb5_context context;
+    krb5_ccache ccache;
+
+    if ((krb5_init_context(&context)) != 0) {
+       return;
+    }
+    if ((krb5_cc_default(context, &ccache)) != 0) {
+       goto cleanup;
+    }
+
+    krb5_cc_destroy(context, ccache);
+    krb5_cc_close(context, ccache);
+
+cleanup:
+     krb5_free_context(context);
+     return;
+}
+
+static void
+parse_pkt(pkt, buf, bufsize)
+    pkt_t *pkt;
+    const void *buf;
+    size_t bufsize;
+{
+    const unsigned char *bufp = buf;
+
+    k5printf(("krb5: parse_pkt: parsing buffer of %d bytes\n", bufsize));
+
+    pkt->type = (pktype_t)*bufp++;
+    bufsize--;
+
+    if (bufsize == 0) {
+       pkt->body[0] = '\0';
+    } else {
+       if (bufsize > sizeof(pkt->body) - 1)
+           bufsize = sizeof(pkt->body) - 1;
+       memcpy(pkt->body, bufp, bufsize);
+       pkt->body[sizeof(pkt->body) - 1] = '\0';
+    }
+
+    k5printf(("krb5: parse_pkt: %s (%d): \"%s\"\n", pkt_type2str(pkt->type),
+       pkt->type, pkt->body));
+}
+
+
+/*
+ * Formats an error from the gss api
+ */
+static const char *
+gss_error(major, minor)
+    OM_uint32 major, minor;
+{
+    static gss_buffer_desc msg;
+    OM_uint32 min_stat, msg_ctx;
+
+    if (msg.length > 0)
+       gss_release_buffer(&min_stat, &msg);
+
+    msg_ctx = 0;
+    if (major == GSS_S_FAILURE)
+       gss_display_status(&min_stat, minor, GSS_C_MECH_CODE, GSS_C_NULL_OID,
+           &msg_ctx, &msg);
+    else
+       gss_display_status(&min_stat, major, GSS_C_GSS_CODE, GSS_C_NULL_OID,
+           &msg_ctx, &msg);
+    return ((const char *)msg.value);
+}
+
+/*
+ * Transmits a gss_buffer_desc over a krb5_handle, adding
+ * the necessary headers to allow the remote end to decode it.
+ * Encryption must be done by the caller.
+ */
+static int
+send_token(kc, handle, tok)
+    struct krb5_conn *kc;
+    int handle;
+    const gss_buffer_desc *tok;
+{
+    OM_uint32 netlength, nethandle;
+    struct iovec iov[3];
+
+    k5printf(("krb5: send_token: writing %d bytes to %s\n", tok->length,
+       kc->hostname));
+
+    if (tok->length > AMANDA_MAX_TOK_SIZE) {
+       kc->errmsg = newvstralloc(kc->errmsg, "krb5 write error to ",
+           kc->hostname, ": token too large", NULL);
+       return (-1);
+    }
+
+    /*
+     * Format is:
+     *   32 bit length (network byte order)
+     *   32 bit handle (network byte order)
+     *   data
+     */
+    netlength = htonl(tok->length);
+    iov[0].iov_base = (void *)&netlength;
+    iov[0].iov_len = sizeof(netlength);
+
+    nethandle = htonl(handle);
+    iov[1].iov_base = (void *)&nethandle;
+    iov[1].iov_len = sizeof(nethandle);
+
+    iov[2].iov_base = (void *)tok->value;
+    iov[2].iov_len = tok->length;
+
+    if (net_writev(kc->fd, iov, 3) < 0) {
+       kc->errmsg = newvstralloc(kc->errmsg, "krb5 write error to ",
+           kc->hostname, ": ", strerror(errno), NULL);
+       return (-1);
+    }
+    return (0);
+}
+
+static int
+recv_token(kc, handle, gtok, timeout)
+    struct krb5_conn *kc;
+    int *handle;
+    gss_buffer_desc *gtok;
+    int timeout;
+{
+    OM_uint32 netint;
+
+    assert(kc->fd >= 0);
+    assert(gtok != NULL);
+
+    k5printf(("krb5: recv_token: reading from %s\n", kc->hostname));
+
+    switch (net_read(kc, &netint, sizeof(netint), timeout)) {
+    case -1:
+       kc->errmsg = newvstralloc(kc->errmsg, "recv error: ", strerror(errno),
+           NULL);
+       k5printf(("krb5 recv_token error return: %s\n", kc->errmsg));
+       return (-1);
+    case 0:
+       gtok->length = 0;
+       return (0);
+    default:
+       break;
+    }
+    gtok->length = ntohl(netint);
+
+    if (gtok->length > AMANDA_MAX_TOK_SIZE) {
+       kc->errmsg = newstralloc(kc->errmsg, "recv error: buffer too large");
+       k5printf(("krb5 recv_token error return: %s\n", kc->errmsg));
+       return (-1);
+    }
+
+    switch (net_read(kc, &netint, sizeof(netint), timeout)) {
+    case -1:
+       kc->errmsg = newvstralloc(kc->errmsg, "recv error: ", strerror(errno),
+           NULL);
+       k5printf(("krb5 recv_token error return: %s\n", kc->errmsg));
+       return (-1);
+    case 0:
+       gtok->length = 0;
+       return (0);
+    default:
+       break;
+    }
+    if (handle != NULL)
+       *handle = ntohl(netint);
+
+    gtok->value = alloc(gtok->length);
+    switch (net_read(kc, gtok->value, gtok->length, timeout)) {
+    case -1:
+       kc->errmsg = newvstralloc(kc->errmsg, "recv error: ", strerror(errno),
+           NULL);
+       k5printf(("krb5 recv_token error return: %s\n", kc->errmsg));
+       amfree(gtok->value);
+       return (-1);
+    case 0:
+       amfree(gtok->value);
+       gtok->length = 0;
+       break;
+    default:
+       break;
+    }
+
+    k5printf(("krb5: recv_token: read %d bytes from %s\n", gtok->length,
+       kc->hostname));
+    return (gtok->length);
+}
+
+#ifdef AMANDA_KRB5_ENCRYPT
+static int
+kencrypt(ks, tok, enctok)
+    struct krb5_stream *ks;
+    gss_buffer_desc *tok, *enctok;
+{
+    int conf_state;
+    OM_uint32 maj_stat, min_stat;
+
+    assert(ks->kc->gss_context != GSS_C_NO_CONTEXT);
+    maj_stat = gss_seal(&min_stat, ks->kc->gss_context, 1,
+       GSS_C_QOP_DEFAULT, tok, &conf_state, enctok);
+    if (maj_stat != GSS_S_COMPLETE || conf_state == 0) {
+       security_stream_seterror(&ks->secstr,
+           "krb5 encryption failed to %s: %s",
+           ks->kc->hostname, gss_error(maj_stat, min_stat));
+       return (-1);
+    }
+    return (0);
+}
+
+static int
+kdecrypt(ks, enctok, tok)
+    struct krb5_stream *ks;
+    gss_buffer_desc *enctok, *tok;
+{
+    OM_uint32 maj_stat, min_stat;
+    int conf_state, qop_state;
+
+    k5printf(("krb5: kdecrypt: decrypting %d bytes\n", enctok->length));
+
+    assert(ks->kc->gss_context != GSS_C_NO_CONTEXT);
+    maj_stat = gss_unseal(&min_stat, ks->kc->gss_context, enctok, tok,
+       &conf_state, &qop_state);
+    if (maj_stat != GSS_S_COMPLETE) {
+       security_stream_seterror(&ks->secstr, "krb5 decrypt error from %s: %s",
+           ks->kc->hostname, gss_error(maj_stat, min_stat));
+       return (-1);
+    }
+    return (0);
+}
+#endif
+
+/*
+ * Writes out the entire iovec
+ */
+static int
+net_writev(fd, iov, iovcnt)
+    int fd, iovcnt;
+    struct iovec *iov;
+{
+    int delta, n, total;
+
+    assert(iov != NULL);
+
+    total = 0;
+    while (iovcnt > 0) {
+       /*
+        * Write the iovec
+        */
+       total += n = writev(fd, iov, iovcnt);
+       if (n < 0)
+           return (-1);
+       if (n == 0) {
+           errno = EIO;
+           return (-1);
+       }
+       /*
+        * Iterate through each iov.  Figure out what we still need
+        * to write out.
+        */
+       for (; n > 0; iovcnt--, iov++) {
+           /* 'delta' is the bytes written from this iovec */
+           delta = n < iov->iov_len ? n : iov->iov_len;
+           /* subtract from the total num bytes written */
+           n -= delta;
+           assert(n >= 0);
+           /* subtract from this iovec */
+           iov->iov_len -= delta;
+           (char *)iov->iov_base += delta;
+           /* if this iovec isn't empty, run the writev again */
+           if (iov->iov_len > 0)
+               break;
+       }
+    }
+    return (total);
+}
+
+/*
+ * Like read(), but waits until the entire buffer has been filled.
+ */
+static ssize_t
+net_read(kc, vbuf, origsize, timeout)
+    struct krb5_conn *kc;
+    void *vbuf;
+    size_t origsize;
+    int timeout;
+{
+    char *buf = vbuf, *off;    /* ptr arith */
+    int nread;
+    size_t size = origsize;
+
+    while (size > 0) {
+       if (kc->readbuf.left == 0) {
+           if (net_read_fillbuf(kc, timeout) < 0)
+               return (-1);
+           if (kc->readbuf.size == 0)
+               return (0);
+       }
+       nread = min(kc->readbuf.left, size);
+       off = kc->readbuf.buf + kc->readbuf.size - kc->readbuf.left;
+       memcpy(buf, off, nread);
+
+       buf += nread;
+       size -= nread;
+       kc->readbuf.left -= nread;
+    }
+    return ((ssize_t)origsize);
+}
+
+/*
+ * net_read likes to do a lot of little reads.  Buffer it.
+ */
+static int
+net_read_fillbuf(kc, timeout)
+    struct krb5_conn *kc;
+    int timeout;
+{
+    fd_set readfds;
+    struct timeval tv;
+
+    FD_ZERO(&readfds);
+    FD_SET(kc->fd, &readfds);
+    tv.tv_sec = timeout;
+    tv.tv_usec = 0;
+    switch (select(kc->fd + 1, &readfds, NULL, NULL, &tv)) {
+    case 0:
+       errno = ETIMEDOUT;
+       /* FALLTHROUGH */
+    case -1:
+       return (-1);
+    case 1:
+       assert(FD_ISSET(kc->fd, &readfds));
+       break;
+    default:
+       assert(0);
+       break;
+    }
+    kc->readbuf.left = 0;
+    kc->readbuf.size = read(kc->fd, kc->readbuf.buf,
+       sizeof(kc->readbuf.buf));
+k5printf(("net_read_fillbuf: read %d characters w/ errno %d\n", kc->readbuf.size, errno));
+    if (kc->readbuf.size < 0)
+       return (-1);
+    kc->readbuf.left = kc->readbuf.size;
+    return (0);
+}
+
+/*
+ * hackish, but you can #undef AMANDA_PRINCIPAL here, and you can both
+ * hardcode a principal in your build and use the .k5amandahosts.  This is
+ * available because sites that run pre-releases of amanda 2.5.0 before 
+ * this feature was there do not behave this way...
+ */
+
+/*#undef AMANDA_PRINCIPAL*/
+
+/*
+ * check ~/.k5amandahosts to see if this principal is allowed in.  If it's
+ * hardcoded, then we don't check the realm
+ */
+static char *
+krb5_checkuser(host, name, realm)
+       char *host, *name, *realm;
+{
+#ifdef AMANDA_PRINCIPAL
+    if(strcmp(name, AMANDA_PRINCIPAL) == 0) {
+       return(NULL);
+    } else {
+       return(vstralloc("does not match compiled in default"));
+    }
+#else
+    struct passwd *pwd;
+    char *ptmp;
+    char *result = "generic error";    /* default is to not permit */
+    FILE *fp = NULL;
+    struct stat sbuf;
+    uid_t localuid;
+    char *line = NULL;
+    char *filehost = NULL, *fileuser = NULL, *filerealm = NULL;
+    char n1[NUM_STR_SIZE];
+    char n2[NUM_STR_SIZE];
+
+    assert( host != NULL);
+    assert( name != NULL);
+
+    if((pwd = getpwnam(CLIENT_LOGIN)) == NULL) {
+       result = vstralloc("can not find user ", CLIENT_LOGIN, NULL);
+    }
+    localuid = pwd->pw_uid;
+
+#ifdef USE_AMANDAHOSTS
+    ptmp = stralloc2(pwd->pw_dir, "/.k5amandahosts");
+#else
+    ptmp = stralloc2(pwd->pw_dir, "/.k5login");
+#endif
+
+    if(!ptmp) {
+       result = vstralloc("could not find home directory for ", CLIENT_LOGIN, NULL);
+       goto common_exit;
+   }
+
+   /*
+    * check to see if the ptmp file does nto exist.
+    */
+   if(access(ptmp, R_OK) == -1 && errno == ENOENT) {
+       /*
+        * in this case we check to see if the principal matches
+        * the destination user mimicing the .k5login functionality.
+        */
+        if(strcmp(name, CLIENT_LOGIN) != 0) {
+               result = vstralloc(name, " does not match ",
+                       CLIENT_LOGIN, NULL);
+               goto common_exit;
+       }
+       result = NULL;
+       goto common_exit;
+    }
+
+    k5printf(("opening ptmp: %s\n", (ptmp)?ptmp: "NULL!"));
+    if((fp = fopen(ptmp, "r")) == NULL) {
+       result = vstralloc("can not open ", ptmp, NULL);
+       goto common_exit;
+    }
+    k5printf(("opened ptmp\n"));
+
+    if (fstat(fileno(fp), &sbuf) != 0) {
+       result = vstralloc("cannot fstat ", ptmp, ": ", strerror(errno), NULL);
+       goto common_exit;
+
+    }
+
+    if (sbuf.st_uid != localuid) {
+       snprintf(n1, sizeof(n1), "%ld", (long) sbuf.st_uid);
+       snprintf(n2, sizeof(n2), "%ld", (long) localuid);
+       result = vstralloc(ptmp, ": ",
+           "owned by id ", n1,
+           ", should be ", n2,
+           NULL);
+       goto common_exit;
+    }
+    if ((sbuf.st_mode & 077) != 0) {
+       result = stralloc2(ptmp,
+           ": incorrect permissions; file must be accessible only by its owner");
+       goto common_exit;
+    }       
+
+    while((line = agets(fp)) != NULL) {
+#if defined(SHOW_SECURITY_DETAIL)                               /* { */
+       k5printf(("%s: processing line: <%s>\n", debug_prefix(NULL), line));
+#endif                                                          /* } */
+       /* if there's more than one column, then it's the host */
+       if( (filehost = strtok(line, " \t")) == NULL) {
+           amfree(line);
+           continue;
+       }
+
+       /*
+        * if there's only one entry, then it's a username and we have
+        * no hostname.  (so the principal is allowed from anywhere.
+        */
+       if((fileuser = strtok(NULL, " \t")) == NULL) {
+           fileuser = filehost;
+           filehost = NULL;
+       }
+
+       if(filehost && strcmp(filehost, host) != 0) {
+           amfree(line);
+           continue;
+       } else {
+               k5printf(("found a host match\n"));
+       }
+
+       if( (filerealm = strchr(fileuser, '@')) != NULL) {
+           *filerealm++ = '\0';
+       }
+
+       /*
+        * we have a match.  We're going to be a little bit insecure
+        * and indicate that the principal is correct but the realm is
+        * not if that's the case.  Technically we should say nothing
+        * and let the user figure it out, but it's helpful for debugging.
+        * You likely only get this far if you've turned on cross-realm auth
+        * anyway...
+        */
+       k5printf(("comparing %s %s\n", fileuser, name));
+       if(strcmp(fileuser, name) == 0) {
+               k5printf(("found a match!\n"));
+               if(realm && filerealm && (strcmp(realm, filerealm)!=0)) {
+                       amfree(line);
+                       continue;
+               }
+               result = NULL;
+               goto common_exit;
+       }
+
+       amfree(line);
+    }
+
+    result = vstralloc("no match in ", ptmp, NULL);
+
+common_exit:
+    if(fp)
+       afclose(fp);
+    if(line)
+       amfree(line);
+    return(result);
+#endif /* AMANDA_PRINCIPAL */
+}
+
+#else
+void krb5_security_dummy (void) {}
+#endif /* KRB5_SECURITY */