chiark / gitweb /
admin: Initialize reference counter for client blocks.
[tripe] / admin.c
diff --git a/admin.c b/admin.c
index 24d86c64ac40dcf7a33e61c84d6ccb2eac48ae6a..80d21af63c3ea817a190fa1a4fbde9b627fa1e3a 100644 (file)
--- a/admin.c
+++ b/admin.c
@@ -38,11 +38,12 @@ const trace_opt tr_opts[] = {
   { 't',       T_TUNNEL,       "tunnel events" },
   { 'r',       T_PEER,         "peer events" },
   { 'a',       T_ADMIN,        "admin interface" },
-  { 'p',       T_PACKET,       "packet contents" },
-  { 'c',       T_CRYPTO,       "crypto details" },
   { 's',       T_KEYSET,       "symmetric keyset management" },
   { 'x',       T_KEYEXCH,      "key exchange" },
   { 'm',       T_KEYMGMT,      "key management" },
+  { 'l',       T_CHAL,         "challenge management" },
+  { 'p',       T_PACKET,       "packet contents" },
+  { 'c',       T_CRYPTO,       "crypto details" },
   { 'A',       T_ALL,          "all of the above" },
   { 0,         0,              0 }
 };
@@ -71,11 +72,14 @@ static sig s_term, s_int, s_hup;
 #define F_INIT 2u
 
 #define T_RESOLVE SEC(30)
+#define T_PING SEC(5)
 
 static void a_destroy(admin */*a*/);
 static void a_lock(admin */*a*/);
 static void a_unlock(admin */*a*/);
 
+#define BOOL(x) ((x) ? "t" : "nil")
+
 /*----- Output functions --------------------------------------------------*/
 
 /* --- @trywrite@ --- *
@@ -108,48 +112,33 @@ again:
       goto again;
     if (errno != EAGAIN && errno != EWOULDBLOCK) {
       a_destroy(a);
-      a_warn("ADMIN client-read-error -- %s", strerror(errno));
+      a_warn("ADMIN", "client-write-error", "?ERRNO", A_END);
       return (-1);
     }
   }
   return (done);
 }
 
-/* --- @dosend@ --- *
+/* --- @doqueue@ -- *
  *
- * Arguemnts:  @admin *a@ = pointer to an admin block
+ * Arguments:  @oqueue *q@ = pointer to output queue
  *             @const char *p@ = pointer to buffer to write
- *             @size_t sz@ = size of data to write
+ *             @size_t sz@ = size of buffer
  *
- * Returns:    ---
+ * Returns:    Nonzero if the queue was previously empty.
  *
- * Use:                Sends data to an admin client.
+ * Use:                Queues data to be written later.
  */
 
-static void dosend(admin *a, const char *p, size_t sz)
+static int doqueue(oqueue *q, const char *p, size_t sz)
 {
-  ssize_t n;
   obuf *o;
+  int rc = 0;
+  size_t n;
 
-  if (a->f & AF_DEAD)
-    return;
-
-  /* --- Try to send the data immediately --- */
-
-  if (!a->o_head) {
-    if ((n = trywrite(a, p, sz)) < 0)
-      return;
-    p += n;
-    sz -= n;
-    if (!sz)
-      return;
-  }
-       
-  /* --- Fill buffers with the data until it's all gone --- */
-
-  o = a->o_tail;
+  o = q->tl;
   if (!o)
-    sel_addfile(&a->w);
+    rc = 1;
   else if (o->p_in < o->buf + OBUFSZ)
     goto noalloc;
 
@@ -157,11 +146,11 @@ static void dosend(admin *a, const char *p, size_t sz)
     o = xmalloc(sizeof(obuf));
     o->next = 0;
     o->p_in = o->p_out = o->buf;
-    if (a->o_tail)
-      a->o_tail->next = o;
+    if (q->tl)
+      q->tl->next = o;
     else
-      a->o_head = o;
-    a->o_tail = o;
+      q->hd = o;
+    q->tl = o;
 
   noalloc:
     n = o->buf + OBUFSZ - o->p_in;
@@ -172,6 +161,37 @@ static void dosend(admin *a, const char *p, size_t sz)
     p += n;
     sz -= n;
   } while (sz);
+
+  return (rc);
+}
+
+/* --- @dosend@ --- *
+ *
+ * Arguemnts:  @admin *a@ = pointer to an admin block
+ *             @const char *p@ = pointer to buffer to write
+ *             @size_t sz@ = size of data to write
+ *
+ * Returns:    ---
+ *
+ * Use:                Sends data to an admin client.
+ */
+
+static void dosend(admin *a, const char *p, size_t sz)
+{
+  ssize_t n;
+
+  if (a->f & AF_DEAD)
+    return;
+  if (!a->out.hd) {
+    if ((n = trywrite(a, p, sz)) < 0)
+      return;
+    p += n;
+    sz -= n;
+    if (!sz)
+      return;
+  }
+  if (doqueue(&a->out, p, sz))
+    sel_addfile(&a->w);
 }
 
 /* --- @a_flush@ --- *
@@ -191,7 +211,7 @@ static void a_flush(int fd, unsigned mode, void *v)
   obuf *o, *oo;
   ssize_t n;
 
-  o = a->o_head;
+  o = a->out.hd;
   while (o) {
     if ((n = trywrite(a, o->p_out, o->p_in - o->p_out)) < 0)
       return;
@@ -202,19 +222,109 @@ static void a_flush(int fd, unsigned mode, void *v)
     o = o->next;
     xfree(oo);
   }
-  a->o_head = o;
+  a->out.hd = o;
   if (!o) {
-    a->o_tail = 0;
+    a->out.tl = 0;
     sel_rmfile(&a->w);
   }
 }
 
 /*----- Utility functions -------------------------------------------------*/
 
+/* --- @quotify@ --- *
+ *
+ * Arguments:  @dstr *d@ = where to write the answer
+ *             @const char *p@ = string to quotify
+ *
+ * Returns:    ---
+ *
+ * Use:                Quotes the given string if necessary, according to our
+ *             quoting rules.
+ */
+
+static void quotify(dstr *d, const char *p)
+{
+  if (d->len)
+    dstr_putc(d, ' ');
+  if (*p && !p[strcspn(p, "\"' \t\n\v")])
+    dstr_puts(d, p);
+  else {
+    dstr_putc(d, '\"');
+    while (*p) {
+      if (*p == '\\' || *p == '\"')
+       dstr_putc(d, '\\');
+      dstr_putc(d, *p++);
+    }
+    dstr_putc(d, '\"');
+  }
+}
+
+/* --- @a_vformat@ --- *
+ *
+ * Arguments:  @dstr *d@ = where to leave the formatted message
+ *             @const char *fmt@ = pointer to format string
+ *             @va_list ap@ = arguments in list
+ *
+ * Returns:    ---
+ *
+ * Use:                Main message token formatting driver.
+ */
+
+static void a_vformat(dstr *d, const char *fmt, va_list ap)
+{
+  dstr dd = DSTR_INIT;
+
+  while (fmt) {
+    if (*fmt == '*') {
+      dstr_putc(d, ' ');
+      dstr_vputf(d, fmt + 1, &ap);
+    } else if (*fmt == '?') {
+      if (strcmp(fmt, "?ADDR") == 0) {
+       const addr *a = va_arg(ap, const addr *);
+       switch (a->sa.sa_family) {
+         case AF_INET:
+           quotify(d, "INET");
+           quotify(d, inet_ntoa(a->sin.sin_addr));
+           dstr_putf(d, " %u", (unsigned)ntohs(a->sin.sin_port));
+           break;
+         default:
+           abort();
+       }
+      } else if (strcmp(fmt, "?B64") == 0) {
+       const octet *p = va_arg(ap, const octet *);
+       size_t n = va_arg(ap, size_t);
+       base64_ctx b64;
+       dstr_putc(d, ' ');
+       base64_init(&b64);
+       b64.indent = "";
+       b64.maxline = 0;
+       base64_encode(&b64, p, n, d);
+       base64_encode(&b64, 0, 0, d);
+       while (d->len && d->buf[d->len - 1] == '=') d->len--;
+      } else if (strcmp(fmt, "?PEER") == 0)
+       quotify(d, p_name(va_arg(ap, peer *)));
+      else if (strcmp(fmt, "?ERRNO") == 0) {
+       dstr_putf(d, " E%d", errno);
+       quotify(d, strerror(errno));
+      } else
+       abort();
+    } else {
+      if (*fmt == '!') fmt++;
+      DRESET(&dd);
+      dstr_vputf(&dd, fmt, &ap);
+      quotify(d, dd.buf);
+    }
+    fmt = va_arg(ap, const char *);
+  }
+
+  dstr_destroy(&dd);
+}
+
 /* --- @a_write@, @a_vwrite@ --- *
  *
  * Arguments:  @admin *a@ = admin connection to write to
- *             @const char *tag@ = tag prefix string, or null
+ *             @const char *status@ = status code to report
+ *             @const char *tag@ = tag string, or null
  *             @const char *fmt@ = pointer to format string
  *             @va_list ap@ = arguments in list
  *             @...@ = other arguments
@@ -224,26 +334,25 @@ static void a_flush(int fd, unsigned mode, void *v)
  * Use:                Sends a message to an admin connection.
  */
 
-static void a_vwrite(admin *a, const char *tag, const char *fmt, va_list ap)
+static void a_vwrite(admin *a, const char *status, const char *tag,
+                    const char *fmt, va_list ap)
 {
   dstr d = DSTR_INIT;
-  if (tag) {
-    dstr_puts(&d, tag);
-    if (fmt)
-      dstr_putc(&d, ' ');
-  }
-  if (fmt)
-    dstr_vputf(&d, fmt, &ap);
+  if (tag) dstr_puts(&d, "BG");
+  dstr_puts(&d, status);
+  if (tag) quotify(&d, tag);
+  a_vformat(&d, fmt, ap);
   dstr_putc(&d, '\n');
   dosend(a, d.buf, d.len);
   dstr_destroy(&d);
 }
 
-static void a_write(admin *a, const char *tag, const char *fmt, ...)
+static void a_write(admin *a, const char *status, const char *tag,
+                   const char *fmt, ...)
 {
   va_list ap;
   va_start(ap, fmt);
-  a_vwrite(a, tag, fmt, ap);
+  a_vwrite(a, status, tag, fmt, ap);
   va_end(ap);
 }
 
@@ -258,13 +367,13 @@ static void a_write(admin *a, const char *tag, const char *fmt, ...)
  * Use:                Convenience functions for @a_write@.
  */
 
-static void a_ok(admin *a) { a_write(a, "OK", 0); }
+static void a_ok(admin *a) { a_write(a, "OK", 0, A_END); }
 
 static void a_info(admin *a, const char *fmt, ...)
 {
   va_list ap;
   va_start(ap, fmt);
-  a_vwrite(a, "INFO", fmt, ap);
+  a_vwrite(a, "INFO", 0, fmt, ap);
   va_end(ap);
 }
 
@@ -272,16 +381,16 @@ static void a_fail(admin *a, const char *fmt, ...)
 {
   va_list ap;
   va_start(ap, fmt);
-  a_vwrite(a, "FAIL", fmt, ap);
+  a_vwrite(a, "FAIL", 0, fmt, ap);
   va_end(ap);
 }
 
 /* --- @a_alert@, @a_valert@, @a_rawalert@ --- *
  *
  * Arguments:  @unsigned f_and, f_eq@ = filter for connections
- *             @const char *tag@ = tag prefix string
+ *             @const char *status@ = status string
  *             @const char *fmt@ = pointer to format string
@             @const char *p@ = pointer to raw string
*             @const char *p@ = pointer to raw string
  *             @size_t sz@ = size of raw string
  *             @va_list ap@ = arguments in list
  *             @...@ = other arguments
@@ -292,7 +401,7 @@ static void a_fail(admin *a, const char *fmt, ...)
  *             filter.
  */
 
-static void a_rawalert(unsigned f_and, unsigned f_eq, const char *tag,
+static void a_rawalert(unsigned f_and, unsigned f_eq, const char *status,
                       const char *p, size_t sz)
 {
   admin *a, *aa;
@@ -300,13 +409,11 @@ static void a_rawalert(unsigned f_and, unsigned f_eq, const char *tag,
   
   if (!(flags & F_INIT))
     return;
-  if (tag) {
-    dstr_puts(&d, tag);
-    if (p)
-      dstr_putc(&d, ' ');
-  }
-  if (p)
+  dstr_puts(&d, status);
+  if (p) {
+    dstr_putc(&d, ' ');
     dstr_putm(&d, p, sz);
+  }
   dstr_putc(&d, '\n');
   p = d.buf;
   sz = d.len;
@@ -325,8 +432,7 @@ static void a_valert(unsigned f_and, unsigned f_eq, const char *tag,
 
   if (!(flags & F_INIT))
     return;
-  if (fmt)
-    dstr_vputf(&d, fmt, &ap);
+  a_vformat(&d, fmt, ap);
   a_rawalert(f_and, f_eq, tag, fmt ? d.buf : 0, fmt ? d.len : 0);
   dstr_destroy(&d);
 }
@@ -360,9 +466,12 @@ void a_warn(const char *fmt, ...)
   if (flags & F_INIT)
     a_valert(0, 0, "WARN", fmt, ap);
   else {
+    dstr d = DSTR_INIT;
     fprintf(stderr, "%s: ", QUIS);
-    vfprintf(stderr, fmt, ap);
-    fputc('\n', stderr);
+    a_vformat(&d, fmt, ap);
+    dstr_putc(&d, '\n');
+    dstr_write(&d, stderr);
+    dstr_destroy(&d);
   }
   va_end(ap);
 }
@@ -381,9 +490,7 @@ void a_warn(const char *fmt, ...)
 
 #ifndef NTRACE
 static void a_trace(const char *p, size_t sz, void *v)
-{
-  a_rawalert(AF_TRACE, AF_TRACE, "TRACE", p, sz);
-}
+  { a_rawalert(AF_TRACE, AF_TRACE, "TRACE", p, sz); }
 #endif
 
 /* --- @a_notify@ --- *
@@ -418,10 +525,10 @@ void a_quit(void)
 {
   peer *p;
 
-  while ((p = p_first()) != 0)
-    p_destroy(p);
   close(sock.fd);
   unlink(sockname);
+  while ((p = p_first()) != 0)
+    p_destroy(p);
   exit(0);
 }
 
@@ -448,7 +555,7 @@ static void a_sigdie(int sig, void *v)
       p = buf;
       break;
   }
-  a_warn("SERVER quit signal %s", p);
+  a_warn("SERVER", "quit", "signal", "%s", p, A_END);
   a_quit();
 }
 
@@ -464,70 +571,329 @@ static void a_sigdie(int sig, void *v)
 
 static void a_sighup(int sig, void *v)
 {
-  a_warn("SERVER ignore signal SIGHUP");
+  a_warn("SERVER", "ignore", "signal", "SIGHUP", A_END);
 }
 
-/*----- Adding peers ------------------------------------------------------*/
-/* --- @a_resolve@ --- *
+/* --- @a_parsetime@ --- *
+ *
+ * Arguments;  @const char *p@ = time string to parse
+ *
+ * Returns:    Time in seconds, or @< 0@ on error.
+ */
+
+static long a_parsetime(const char *p)
+{
+  char *q;
+  long t = strtol(p, &q, 0);
+
+  switch (*q) {
+    case 'd': t *= 24;
+    case 'h': t *= 60;
+    case 'm': t *= 60;
+    case 's': if (q[1] != 0)
+    default:    t = -1;
+    case 0:   break;
+  }
+  return (t);    
+}
+
+/* --- @a_findpeer@ --- *
+ *
+ * Arguments:  @admin *a@ = admin connection
+ *             @const char *pn@ = peer name
+ *
+ * Returns:    The peer, or null if not there.
+ *
+ * Use:                Finds a peer, reporting an error if it failed.
+ */
+
+static peer *a_findpeer(admin *a, const char *pn)
+{
+  peer *p;
+
+  if ((p = p_find(pn)) == 0)
+    a_fail(a, "unknown-peer", "%s", pn, A_END);
+  return (p);
+}
+
+/*----- Backgrounded operations -------------------------------------------*/
+
+#define BGTAG(bg)                                                      \
+  (((admin_bgop *)(bg))->tag ? ((admin_bgop *)(bg))->tag : "<foreground>")
+
+/* --- @a_bgrelease@ --- *
+ *
+ * Arguments:  @admin_bgop *bg@ = backgrounded operation
+ *
+ * Returns:    ---
+ *
+ * Use:                Removes a backgrounded operation from the queue, since
+ *             (presumably) it's done.
+ */
+
+static void a_bgrelease(admin_bgop *bg)
+{
+  admin *a = bg->a;
+
+  T( trace(T_ADMIN, "admin: release bgop %s", BGTAG(bg)); )
+  if (bg->tag) xfree(bg->tag);
+  else selbuf_enable(&a->b);
+  if (bg->next) bg->next->prev = bg->prev;
+  if (bg->prev) bg->prev->next = bg->next;
+  else a->bg = bg->next;
+  xfree(bg);
+  if (a->f & AF_CLOSE) a_destroy(a);
+  a_unlock(a);
+}
+
+/* --- @a_bgok@, @a_bginfo@, @a_bgfail@ --- *
+ *
+ * Arguments:  @admin_bgop *bg@ = backgrounded operation
+ *             @const char *fmt@ = format string
+ *             @...@ = other arguments
+ *
+ * Returns:    ---
+ *
+ * Use:                Convenience functions for @a_write@.
+ */
+
+static void a_bgok(admin_bgop *bg)
+  { a_write(bg->a, "OK", bg->tag, A_END); }
+
+static void a_bginfo(admin_bgop *bg, const char *fmt, ...)
+{
+  va_list ap;
+  va_start(ap, fmt);
+  a_vwrite(bg->a, "INFO", bg->tag, fmt, ap);
+  va_end(ap);
+}
+
+static void a_bgfail(admin_bgop *bg, const char *fmt, ...)
+{
+  va_list ap;
+  va_start(ap, fmt);
+  a_vwrite(bg->a, "FAIL", bg->tag, fmt, ap);
+  va_end(ap);
+}
+
+/* --- @a_bgadd@ --- *
+ *
+ * Arguments:  @admin *a@ = administration connection
+ *             @admin_bgop *bg@ = pointer to background operation
+ *             @const char *tag@ = background tag, or null for foreground
+ *             @void (*cancel)(admin_bgop *)@ = cancel function
+ *
+ * Returns:    ---
+ *
+ * Use:                Links a background job into the list.
+ */
+
+static void a_bgadd(admin *a, admin_bgop *bg, const char *tag,
+                   void (*cancel)(admin_bgop *))
+{
+  if (tag)
+    bg->tag = xstrdup(tag);
+  else {
+    bg->tag = 0;
+    selbuf_disable(&a->b);
+  }
+  bg->a = a;
+  bg->cancel = cancel;
+  bg->next = a->bg;
+  bg->prev = 0;
+  if (a->bg) a->bg->prev = bg;
+  a->bg = bg;
+  a_lock(a);
+  T( trace(T_ADMIN, "admin: add bgop %s", BGTAG(bg)); )
+  if (tag) a_write(a, "DETACH", tag, A_END);
+}
+
+/*----- Name resolution operations ----------------------------------------*/
+
+/* --- @a_resolved@ --- *
  *
  * Arguments:  @struct hostent *h@ = pointer to resolved hostname
- *             @void *v@ = pointer to admin block
+ *             @void *v@ = pointer to resolver operation
  *
  * Returns:    ---
  *
  * Use:                Handles a completed name resolution.
  */
 
-static void a_resolve(struct hostent *h, void *v)
+static void a_resolved(struct hostent *h, void *v)
 {
-  admin *a = v;
+  admin_resop *r = v;
 
-  a_lock(a);
-  T( trace(T_ADMIN, "admin: %u resolved", a->seq); )
+  T( trace(T_ADMIN, "admin: resop %s resolved", BGTAG(r)); )
   TIMER;
-  sel_rmtimer(&a->t);
-  if (!h)
-    a_fail(a, "resolve-error %s", a->paddr);
-  else if (p_find(a->pname))
-    a_fail(a, "peer-exists %s", a->pname);
-  else {
-    memcpy(&a->peer.sin.sin_addr, h->h_addr, sizeof(struct in_addr));
-    if (!p_create(a->pname, a->tops, &a->peer.sa, a->sasz))
-      a_fail(a, "peer-create-fail %s", a->pname);
-    else
-      a_ok(a);
+  if (!h) {
+    a_bgfail(&r->bg, "resolve-error", "%s", r->addr, A_END);
+    r->func(r, ARES_FAIL);
+  } else {
+    memcpy(&r->sa.sin.sin_addr, h->h_addr, sizeof(struct in_addr));
+    r->func(r, ARES_OK);
   }
-  xfree(a->pname);
-  xfree(a->paddr);
-  a->pname = 0;
-  selbuf_enable(&a->b);
-  a_unlock(a);
-}
+  sel_rmtimer(&r->t);
+  xfree(r->addr);
+  a_bgrelease(&r->bg);
+} 
 
-/* --- @a_timer@ --- *
+/* --- @a_restimer@ --- *
  *
  * Arguments:  @struct timeval *tv@ = timer
- *             @void *v@ = pointer to admin block
+ *             @void *v@ = pointer to resolver operation
  *
  * Returns:    ---
  *
  * Use:                Times out a resolver.
  */
 
-static void a_timer(struct timeval *tv, void *v)
+static void a_restimer(struct timeval *tv, void *v)
 {
-  admin *a = v;
+  admin_resop *r = v;
+
+  T( trace(T_ADMIN, "admin: resop %s timeout", BGTAG(r)); )
+  a_bgfail(&r->bg, "resolver-timeout", "%s", r->addr, A_END);
+  r->func(r, ARES_FAIL);
+  bres_abort(&r->r);
+  xfree(r->addr);
+  a_bgrelease(&r->bg);
+}
 
-  a_lock(a);
-  T( trace(T_ADMIN, "admin: %u resolver timeout", a->seq); )
-  bres_abort(&a->r);
-  a_fail(a, "resolver-timeout %s\n", a->paddr);
-  xfree(a->pname);
-  xfree(a->paddr);
-  a->pname = 0;
-  selbuf_enable(&a->b);
-  a_unlock(a);
+/* --- @a_rescancel@ --- *
+ *
+ * Arguments:  @admin_bgop *bg@ = background operation
+ *
+ * Returns:    ---
+ *
+ * Use:                Cancels an add operation.
+ */
+
+static void a_rescancel(admin_bgop *bg)
+{
+  admin_resop *r = (admin_resop *)bg;
+
+  T( trace(T_ADMIN, "admin: cancel resop %s", BGTAG(r)); )
+  r->func(r, ARES_FAIL);
+  sel_rmtimer(&r->t);
+  xfree(r->addr);
+  bres_abort(&r->r);
+}
+
+/* --- @a_resolve@ --- *
+ *
+ * Arguments:  @admin *a@ = administration connection
+ *             @admin_resop *r@ = resolver operation to run
+ *             @const char *tag@ = background operation tag
+ *             @void (*func)(struct admin_resop *, int@ = handler function
+ *             @unsigned ac@ = number of remaining arguments
+ *             @char *av[]@ = pointer to remaining arguments
+ *
+ * Returns:    ---
+ *
+ * Use:                Cranks up a resolver job.
+ */
+
+static void a_resolve(admin *a, admin_resop *r, const char *tag,
+                     void (*func)(struct admin_resop *, int),
+                     unsigned ac, char *av[])
+{
+  struct timeval tv;
+  unsigned long pt;
+  char *p;
+  int i = 0;
+
+  /* --- Fill in the easy bits of address --- */
+
+  r->addr = 0;
+  r->func = func;
+  if (mystrieq(av[i], "inet")) i++;
+  if (ac - i != 2) {
+    a_fail(a, "bad-addr-syntax", "[inet] ADDRESS PORT", A_END);
+    goto fail;
+  }
+  r->sa.sin.sin_family = AF_INET;
+  r->sasz = sizeof(r->sa.sin);
+  r->addr = xstrdup(av[i]);
+  pt = strtoul(av[i + 1], &p, 0);
+  if (*p) {
+    struct servent *s = getservbyname(av[i + 1], "udp");
+    if (!s) {
+      a_fail(a, "unknown-service", "%s", av[i + 1], A_END);
+      goto fail;
+    }
+    pt = ntohs(s->s_port);
+  }
+  if (pt == 0 || pt >= 65536) {
+    a_fail(a, "invalid-port", "%lu", pt, A_END);
+    goto fail;
+  }
+  r->sa.sin.sin_port = htons(pt);
+
+  /* --- Report backgrounding --- *
+   *
+   * Do this for consistency of interface, even if we're going to get the
+   * answer straight away.
+   */
+
+  a_bgadd(a, &r->bg, tag, a_rescancel);
+  T( trace(T_ADMIN, "admin: %u, resop %s, hostname `%s'",
+          a->seq, BGTAG(r), r->addr); )
+
+  /* --- If the name is numeric, do it the easy way --- */
+  
+  if (inet_aton(av[i], &r->sa.sin.sin_addr)) {
+    T( trace(T_ADMIN, "admin: resop %s done the easy way", BGTAG(r)); )
+    func(r, ARES_OK);
+    xfree(r->addr);
+    a_bgrelease(&r->bg);
+    return;
+  }
+
+  /* --- Store everything for later and crank up the resolver --- */
+
+  gettimeofday(&tv, 0);
+  tv.tv_sec += T_RESOLVE;
+  sel_addtimer(&sel, &r->t, &tv, a_restimer, r);
+  bres_byname(&r->r, r->addr, a_resolved, r);
+  return;
+
+fail:
+  func(r, ARES_FAIL);
+  if (r->addr) xfree(r->addr);
+  xfree(r);
+}
+
+/*----- Adding peers ------------------------------------------------------*/
+
+/* --- @a_doadd@ --- *
+ *
+ * Arguments:  @admin_resop *r@ = resolver operation
+ *             @int rc@ = how it worked
+ *
+ * Returns:    ---
+ *
+ * Use:                Handles a completed resolution.
+ */
+
+static void a_doadd(admin_resop *r, int rc)
+{
+  admin_addop *add = (admin_addop *)r;
+
+  T( trace(T_ADMIN, "admin: done add op %s", BGTAG(add)); )
+
+  if (rc == ARES_OK) {
+    add->peer.sasz = add->r.sasz;
+    add->peer.sa = add->r.sa;
+    if (p_find(add->peer.name))
+      a_bgfail(&add->r.bg, "peer-exists", "%s", add->peer.name, A_END);
+    else if (!p_create(&add->peer))
+      a_bgfail(&add->r.bg, "peer-create-fail", "%s", add->peer.name, A_END);
+    else
+      a_bgok(&add->r.bg);
+  }
+
+  xfree(add->peer.name);
 }
 
 /* --- @acmd_add@ --- *
@@ -543,17 +909,22 @@ static void a_timer(struct timeval *tv, void *v)
 
 static void acmd_add(admin *a, unsigned ac, char *av[])
 {
-  unsigned long pt;
-  struct timeval tv;
   unsigned i, j;
-  const tunnel_ops *tops = tun_default;
-  char *p;
+  const char *tag = 0;
+  admin_addop *add = 0;
+
+  /* --- Set stuff up --- */
+
+  add = xmalloc(sizeof(*add));
+  add->peer.name = xstrdup(av[0]);
+  add->peer.t_ka = 0;
+  add->peer.tops = tun_default;
 
   /* --- Make sure someone's not got there already --- */
 
   if (p_find(av[0])) {
-    a_fail(a, "peer-exists %s", av[0]);
-    return;
+    a_fail(a, "peer-exists", "%s", av[0], A_END);
+    goto fail;
   }
 
   /* --- Parse options --- */
@@ -562,87 +933,174 @@ static void acmd_add(admin *a, unsigned ac, char *av[])
   for (;;) {
     if (!av[i])
       goto bad_syntax;
-    if (mystrieq(av[i], "-tunnel")) {
-      i++;
-      if (!av[i])
-       goto bad_syntax;
+    if (mystrieq(av[i], "-background")) {
+      if (!av[++i]) goto bad_syntax;
+      tag = av[i];
+    } else if (mystrieq(av[i], "-tunnel")) {
+      if (!av[++i]) goto bad_syntax;
       for (j = 0;; j++) {
        if (!tunnels[j]) {
-         a_fail(a, "unknown-tunnel %s", av[i]);
-         return;
+         a_fail(a, "unknown-tunnel", "%s", av[i], A_END);
+         goto fail;
        }
        if (mystrieq(av[i], tunnels[j]->name)) {
-         tops = tunnels[j];
+         add->peer.tops = tunnels[j];
          break;
        }
       }
-      i++;
+    } else if (mystrieq(av[i], "-keepalive")) {
+      long t;
+      if (!av[++i]) goto bad_syntax;
+      if ((t = a_parsetime(av[i])) < 0) {
+       a_fail(a, "bad-time-spec", "%s", av[i], A_END);
+       goto fail;
+      }
+      add->peer.t_ka = t;
     } else if (mystrieq(av[i], "--")) {
       i++;
       break;
     } else
       break;
+    i++;
   }
 
-  /* --- Fill in the easy bits of address --- */
+  /* --- Crank up the resolver --- */
 
-  BURN(a->peer);
-  if (mystrieq(av[i], "inet")) i++;
-  if (ac - i != 2) {
-    a_fail(a, "bad-syntax -- add PEER [-tunnel TUN] [inet] ADDRESS PORT");
-    return;
-  }
-  a->peer.sin.sin_family = AF_INET;
-  a->sasz = sizeof(a->peer.sin);
-  pt = strtoul(av[i + 1], &p, 0);
-  if (*p) {
-    struct servent *s = getservbyname(av[i + 1], "udp");
-    if (!s) {
-      a_fail(a, "unknown-service %s", av[i + 1]);
-      return;
-    }
-    pt = ntohs(s->s_port);
+  a_resolve(a, &add->r, tag, a_doadd, ac - i, av + i);
+  return;
+
+  /* --- Clearing up --- */
+
+bad_syntax:
+  a_fail(a, "bad-syntax", "add", "PEER [OPTIONS] ADDR ...", A_END);
+fail:
+  xfree(add->peer.name);
+  xfree(add);
+  return;
+}
+
+/*----- Ping --------------------------------------------------------------*/
+
+/* --- @a_pingcancel@ --- *
+ *
+ * Arguments:  @admin_bgop *bg@ = background operation block
+ *
+ * Returns:    ---
+ *
+ * Use:                Cancels a running ping.
+ */
+
+static void a_pingcancel(admin_bgop *bg)
+{
+  admin_pingop *pg = (admin_pingop *)bg;
+  T( trace(T_ADMIN, "admin: cancel ping op %s", BGTAG(pg)); )
+  p_pingdone(&pg->ping, PING_NONOTIFY);
+}
+
+/* --- @a_pong@ --- *
+ *
+ * Arguments:  @int rc@ = return code
+ *             @void *v@ = ping operation block
+ *
+ * Returns:    ---
+ *
+ * Use:                Collects what happened to a ping message.
+ */
+
+static void a_pong(int rc, void *v)
+{
+  admin_pingop *pg = v;
+  struct timeval tv;
+  double millis;
+
+  switch (rc) {
+    case PING_OK:
+      gettimeofday(&tv, 0);
+      tv_sub(&tv, &tv, &pg->pingtime);
+      millis = (double)tv.tv_sec * 1000 + (double)tv.tv_usec/1000;
+      a_bginfo(&pg->bg, "ping-ok", "%.1f", millis, A_END);
+      a_bgok(&pg->bg);
+      break;
+    case PING_TIMEOUT:
+      a_bginfo(&pg->bg, "ping-timeout", A_END);
+      a_bgok(&pg->bg);
+      break;
+    case PING_PEERDIED:
+      a_bginfo(&pg->bg, "ping-peer-died", A_END);
+      a_bgok(&pg->bg);
+      break;
+    default:
+      abort();
   }
-  if (pt == 0 || pt >= 65536) {
-    a_fail(a, "invalid-port %lu", pt);
-    return;
+  T( trace(T_ADMIN, "admin: ponged ping op %s", BGTAG(pg)); )
+  a_bgrelease(&pg->bg);
+}
+
+/* --- @acmd_ping@, @acmd_eping@ --- *
+ *
+ * Arguments:  @admin *a@ = connection which requested the ping
+ *             @unsigned ac@ = argument count
+ *             @char *av[]@ = pointer to the argument list
+ *
+ * Returns:    ---
+ *
+ * Use:                Pings a peer.
+ */
+
+static void a_ping(admin *a, unsigned ac, char *av[],
+                  const char *cmd, unsigned msg)
+{
+  long t = T_PING;
+  int i;
+  peer *p;
+  admin_pingop *pg = 0;
+  const char *tag = 0;
+
+  i = 0;
+  for (;;) {
+    if (!av[i])
+      goto bad_syntax;
+    if (mystrieq(av[i], "-background")) {
+      if (!av[++i]) goto bad_syntax;
+      tag = av[i];
+    } else if (mystrieq(av[i], "-timeout")) {
+      if (!av[++i]) goto bad_syntax;
+      if ((t = a_parsetime(av[i])) < 0) {
+       a_fail(a, "bad-time-spec", "%s", av[i], A_END);
+       return;
+      }
+    } else if (mystrieq(av[i], "--")) {
+      i++;
+      break;
+    } else
+      break;
+    i++;
   }
-  a->peer.sin.sin_port = htons(pt);
 
-  /* --- If the name is numeric, do it the easy way --- */
-  
-  if (inet_aton(av[i], &a->peer.sin.sin_addr)) {
-    if (!p_create(av[0], tops, &a->peer.sa, a->sasz))
-      a_fail(a, "peer-create-fail %s", a->pname);
-    else
-      a_ok(a);
+  if (!av[i]) goto bad_syntax;
+  if ((p = a_findpeer(a, av[i])) == 0)
     return;
+  pg = xmalloc(sizeof(*pg));
+  gettimeofday(&pg->pingtime, 0);
+  a_bgadd(a, &pg->bg, tag, a_pingcancel);
+  T( trace(T_ADMIN, "admin: ping op %s: %s to %s",
+          BGTAG(pg), cmd, p_name(p)); )
+  if (p_pingsend(p, &pg->ping, msg, t, a_pong, pg)) {
+    a_bgfail(&pg->bg, "ping-send-failed", A_END);
+    a_bgrelease(&pg->bg);
   }
-
-  /* --- Store everything for later and crank up the resolver --- *
-   *
-   * We disable the line buffer until the resolver completes (or times out).
-   * This prevents other commands on the same connection (though the rest of
-   * the system continues regardless), but makes life simpler for the client.
-   */
-
-  a->pname = xstrdup(av[0]);
-  a->paddr = xstrdup(av[i]);
-  a->tops = tops;
-  selbuf_disable(&a->b);
-  gettimeofday(&tv, 0);
-  tv.tv_sec += T_RESOLVE;
-  sel_addtimer(&sel, &a->t, &tv, a_timer, a);
-  bres_byname(&a->r, a->paddr, a_resolve, a);
-  T( trace(T_ADMIN, "admin: %u resolving hostname `%s'",
-          a->seq, a->paddr); )
   return;
-
+    
 bad_syntax:
-  a_fail(a, "bad-syntax -- add PEER [-tunnel TUN] ADDR ...");
+  a_fail(a, "bad-syntax", "%s", cmd, "[OPTIONS] PEER", cmd, A_END);
   return;
 }
 
+static void acmd_ping(admin *a, unsigned ac, char *av[])
+  { a_ping(a, ac, av, "ping", MISC_PING); }
+static void acmd_eping(admin *a, unsigned ac, char *av[])
+  { a_ping(a, ac, av, "eping", MISC_EPING); }
+
 /*----- Administration commands -------------------------------------------*/
 
 /* --- Miscellaneous commands --- */
@@ -668,10 +1126,9 @@ static int traceish(admin *a, unsigned ac, char *av[],
 
   if (!ac || strcmp(av[0], "?") == 0) {
     const trace_opt *t;
-    a_info(a, "Current %s status:", what);
     for (t = tt; t->ch; t++) {
-      a_info(a, "%c %c  %s",
-            t->ch, (*ff & t->f) == t->f ? '*' : ' ', t->help);
+      a_info(a, "*%c%c %s",
+            t->ch, (*ff & t->f) == t->f ? '+' : ' ', t->help, A_END);
     }
   } else {
     unsigned sense = 1;
@@ -691,7 +1148,7 @@ static int traceish(admin *a, unsigned ac, char *av[],
              goto tropt_ok;
            }
          }
-         a_fail(a, "bad-%s-option %c", what, *p);
+         a_fail(a, "bad-%s-option", what, "%c", *p, A_END);
          return (0);
         tropt_ok:;
          break;
@@ -720,23 +1177,6 @@ static void acmd_watch(admin *a, unsigned ac, char *av[])
   traceish(a, ac, av, "watch", w_opts, &a->f);
 }
 
-static void quotify(dstr *d, const char *p)
-{
-  if (d->len)
-    dstr_putc(d, ' ');
-  if (*p && !p[strcspn(p, "\"' \t\n\v")])
-    dstr_puts(d, p);
-  else {
-    dstr_putc(d, '\"');
-    while (*p) {
-      if (*p == '\\' || *p == '\"')
-       dstr_putc(d, '\\');
-      dstr_putc(d, *p++);
-    }
-    dstr_putc(d, '\"');
-  }
-}
-
 static void alertcmd(admin *a, unsigned f_and, unsigned f_eq,
                     const char *tag, unsigned ac, char *av[])
 {
@@ -759,20 +1199,20 @@ static void acmd_warn(admin *a, unsigned ac, char *av[])
 
 static void acmd_port(admin *a, unsigned ac, char *av[])
 {
-  a_info(a, "%u", p_port());
+  a_info(a, "%u", p_port(), A_END);
   a_ok(a);
 }
 
 static void acmd_daemon(admin *a, unsigned ac, char *av[])
 {
   if (flags & F_DAEMON)
-    a_fail(a, "already-daemon");
+    a_fail(a, "already-daemon", A_END);
   else {
-    a_notify("DAEMON");
+    a_notify("DAEMON", A_END);
     if (a_stdin)
       a_destroy(a_stdin);
     if (u_daemon())
-      a_fail(a, "daemon-error -- %s", strerror(errno));
+      a_fail(a, "daemon-error", "?ERRNO", A_END);
     else {
       flags |= F_DAEMON;
       a_ok(a);
@@ -784,7 +1224,7 @@ static void acmd_list(admin *a, unsigned ac, char *av[])
 {
   peer *p;
   for (p = p_first(); p; p = p_next(p))
-    a_info(a, "%s", p_name(p));
+    a_info(a, "%s", p_name(p), A_END);
   a_ok(a);
 }
 
@@ -792,10 +1232,51 @@ static void acmd_ifname(admin *a, unsigned ac, char *av[])
 {
   peer *p;
 
-  if ((p = p_find(av[0])) == 0)
-    a_fail(a, "unknown-peer %s", av[0]);
-  else {
-    a_info(a, "%s", p_ifname(p));
+  if ((p = a_findpeer(a, av[0])) != 0) {
+    a_info(a, "%s", p_ifname(p), A_END);
+    a_ok(a);
+  }
+}
+
+static void acmd_getchal(admin *a, unsigned ac, char *av[])
+{
+  buf b;
+
+  buf_init(&b, buf_i, PKBUFSZ);
+  c_new(&b);
+  a_info(a, "?B64", BBASE(&b), (size_t)BLEN(&b), A_END);
+  a_ok(a);
+}
+
+static void acmd_checkchal(admin *a, unsigned ac, char *av[])
+{
+  base64_ctx b64;
+  buf b;
+  dstr d = DSTR_INIT;
+
+  base64_init(&b64);
+  base64_decode(&b64, av[0], strlen(av[0]), &d);
+  base64_decode(&b64, 0, 0, &d);
+  buf_init(&b, d.buf, d.len);
+  if (c_check(&b) || BBAD(&b) || BLEFT(&b))
+    a_fail(a, "invalid-challenge", A_END);
+  else
+    a_ok(a);
+  dstr_destroy(&d);
+}
+
+static void acmd_greet(admin *a, unsigned ac, char *av[])
+{
+  peer *p;
+  base64_ctx b64;
+  dstr d = DSTR_INIT;
+
+  if ((p = a_findpeer(a, av[0])) != 0) {
+    base64_init(&b64);
+    base64_decode(&b64, av[1], strlen(av[1]), &d);
+    base64_decode(&b64, 0, 0, &d);
+    p_greet(p, d.buf, d.len);
+    dstr_destroy(&d);
     a_ok(a);
   }
 }
@@ -805,67 +1286,94 @@ static void acmd_addr(admin *a, unsigned ac, char *av[])
   peer *p;
   const addr *ad;
 
-  if ((p = p_find(av[0])) == 0)
-    a_fail(a, "unknown-peer %s", av[0]);
-  else {
+  if ((p = a_findpeer(a, av[0])) != 0) {
     ad = p_addr(p);
     assert(ad->sa.sa_family == AF_INET);
-    a_info(a, "INET %s %u",
-           inet_ntoa(ad->sin.sin_addr),
-           (unsigned)ntohs(ad->sin.sin_port));
+    a_info(a, "?ADDR", ad, A_END);
     a_ok(a);
   }
 }
 
-static void acmd_stats(admin *a, unsigned ac, char *av[])
+static void acmd_peerinfo(admin *a, unsigned ac, char *av[])
 {
   peer *p;
-  stats *st;
+  const peerspec *ps;
 
-  if ((p = p_find(av[0])) == 0)
-    a_fail(a, "unknown-peer %s", av[0]);
-  else {
-    st = p_stats(p);
-    a_info(a, "start-time=%s", timestr(st->t_start));
-    a_info(a, "last-packet-time=%s", timestr(st->t_last));
-    a_info(a, "last-keyexch-time=%s", timestr(st->t_kx));
-    a_info(a, "packets-in=%lu bytes-in=%lu", st->n_in, st->sz_in);
-    a_info(a, "packets-out=%lu bytes-out=%lu",
-           st->n_out, st->sz_out);
-    a_info(a, "keyexch-packets-in=%lu keyexch-bytes-in=%lu",
-           st->n_kxin, st->sz_kxin);
-    a_info(a, "keyexch-packets-out=%lu keyexch-bytes-out=%lu",
-           st->n_kxout, st->sz_kxout);
-    a_info(a, "ip-packets-in=%lu ip-bytes-in=%lu",
-           st->n_ipin, st->sz_ipin);
-    a_info(a, "ip-packets-out=%lu ip-bytes-out=%lu",
-           st->n_ipout, st->sz_ipout);
-    a_info(a, "rejected-packets=%lu", st->n_reject);
+  if ((p = a_findpeer(a, av[0])) != 0) {
+    ps = p_spec(p);
+    a_info(a, "tunnel=%s", ps->tops->name, A_END);
+    a_info(a, "keepalive=%lu", ps->t_ka, A_END);
     a_ok(a);
   }
 }
 
+static void acmd_servinfo(admin *a, unsigned ac, char *av[])
+{
+  a_info(a, "implementation=edgeware-tripe", A_END);
+  a_info(a, "version=%s", VERSION, A_END);
+  a_info(a, "daemon=%s", BOOL(flags & F_DAEMON), A_END);
+  a_ok(a);
+}
+
+static void acmd_stats(admin *a, unsigned ac, char *av[])
+{
+  peer *p;
+  stats *st;
+
+  if ((p = a_findpeer(a, av[0])) == 0)
+    return;
+
+  st = p_stats(p);
+  a_info(a, "start-time=%s", timestr(st->t_start), A_END);
+  a_info(a, "last-packet-time=%s", timestr(st->t_last), A_END);
+  a_info(a, "last-keyexch-time=%s", timestr(st->t_kx), A_END);
+  a_info(a, "packets-in=%lu bytes-in=%lu", st->n_in, st->sz_in, A_END);
+  a_info(a, "packets-out=%lu bytes-out=%lu",
+        st->n_out, st->sz_out, A_END);
+  a_info(a, "keyexch-packets-in=%lu keyexch-bytes-in=%lu",
+        st->n_kxin, st->sz_kxin, A_END);
+  a_info(a, "keyexch-packets-out=%lu keyexch-bytes-out=%lu",
+        st->n_kxout, st->sz_kxout, A_END);
+  a_info(a, "ip-packets-in=%lu ip-bytes-in=%lu",
+        st->n_ipin, st->sz_ipin, A_END);
+  a_info(a, "ip-packets-out=%lu ip-bytes-out=%lu",
+        st->n_ipout, st->sz_ipout, A_END);
+  a_info(a, "rejected-packets=%lu", st->n_reject, A_END);
+  a_ok(a);
+}
+
 static void acmd_kill(admin *a, unsigned ac, char *av[])
 {
   peer *p;
-  if ((p = p_find(av[0])) == 0)
-    a_fail(a, "unknown-peer %s", av[0]);
-  else {
+  if ((p = a_findpeer(a, av[0])) != 0) {
     p_destroy(p);
     a_ok(a);
   }
 }
 
+static void acmd_forcekx(admin *a, unsigned ac, char *av[])
+{
+  peer *p;
+  if ((p = a_findpeer(a, av[0])) != 0) {
+    kx_start(&p->kx, 1);
+    a_ok(a);
+  }
+}
+
+static void acmd_reload(admin *a, unsigned ac, char *av[])
+  { p_keyreload(); a_ok(a); }
+
 static void acmd_quit(admin *a, unsigned ac, char *av[])
 {
-  a_warn("SERVER quit admin-request");
+  a_warn("SERVER", "quit", "admin-request", A_END);
   a_ok(a);
+  a_unlock(a);
   a_quit();
 }
 
 static void acmd_version(admin *a, unsigned ac, char *av[])
 {
-  a_info(a, "%s %s", PACKAGE, VERSION);
+  a_info(a, "%s", PACKAGE, "%s", VERSION, A_END);
   a_ok(a);
 }
 
@@ -873,7 +1381,7 @@ static void acmd_tunnels(admin *a, unsigned ac, char *av[])
 {
   int i;
   for (i = 0; tunnels[i]; i++)
-    a_info(a, "%s", tunnels[i]->name);
+    a_info(a, "%s", tunnels[i]->name, A_END);
   a_ok(a);
 }
 
@@ -889,33 +1397,45 @@ typedef struct acmd {
 static void acmd_help(admin */*a*/, unsigned /*ac*/, char */*av*/[]);
 
 static const acmd acmdtab[] = {
-  { "help",    "help",                 0,      0,      acmd_help },
-  { "version", "version",              0,      0,      acmd_version },
+  { "add",     "PEER [OPTIONS] ADDR ...", 2,   0xffff, acmd_add },
+  { "addr",    "PEER",                 1,      1,      acmd_addr },
+  { "checkchal", "CHAL",               1,      1,      acmd_checkchal },
+  { "daemon",  0,                      0,      0,      acmd_daemon },
+  { "eping",   "[OPTIONS] PEER",       1,      0xffff, acmd_eping },
+  { "forcekx", "PEER",                 1,      1,      acmd_forcekx },
+  { "getchal", 0,                      0,      0,      acmd_getchal },
+  { "greet",   "PEER CHAL",            2,      2,      acmd_greet },
+  { "help",    0,                      0,      0,      acmd_help },
+  { "ifname",  "PEER",                 1,      1,      acmd_ifname },
+  { "kill",    "PEER",                 1,      1,      acmd_kill },
+  { "list",    0,                      0,      0,      acmd_list },
+  { "notify",  "MESSAGE ...",          1,      0xffff, acmd_notify },
+  { "peerinfo",        "PEER",                 1,      1,      acmd_peerinfo },
+  { "ping",    "[OPTIONS] PEER",       1,      0xffff, acmd_ping },
+  { "port",    0,                      0,      0,      acmd_port },
+  { "quit",    0,                      0,      0,      acmd_quit },
+  { "reload",  0,                      0,      0,      acmd_reload },
+  { "servinfo",        0,                      0,      0,      acmd_servinfo },
+  { "stats",   "PEER",                 1,      1,      acmd_stats },
 #ifndef NTRACE
-  { "trace",   "trace [OPTIONS]",      0,      1,      acmd_trace },
+  { "trace",   "[OPTIONS]",            0,      1,      acmd_trace },
 #endif
-  { "watch",   "watch [OPTIONS]",      0,      1,      acmd_watch },
-  { "notify",  "notify MESSAGE ...",   1,      0xffff, acmd_notify },
-  { "warn",    "warn MESSAGE ...",     1,      0xffff, acmd_warn },
-  { "port",    "port",                 0,      0,      acmd_port },
-  { "daemon",  "daemon",               0,      0,      acmd_daemon },
-  { "list",    "list",                 0,      0,      acmd_list },
-  { "ifname",  "ifname PEER",          1,      1,      acmd_ifname },
-  { "addr",    "addr PEER",            1,      1,      acmd_addr },
-  { "stats",   "stats PEER",           1,      1,      acmd_stats },
-  { "kill",    "kill PEER",            1,      1,      acmd_kill },
-  { "add",     "add PEER [-tunnel TUN] ADDR ...",
-                                       2,      0xffff, acmd_add },
-  { "tunnels", "tunnels",              0,      0,      acmd_tunnels },
-  { "quit",    "quit",                 0,      0,      acmd_quit },
+  { "tunnels", 0,                      0,      0,      acmd_tunnels },
+  { "version", 0,                      0,      0,      acmd_version },
+  { "warn",    "MESSAGE ...",          1,      0xffff, acmd_warn },
+  { "watch",   "[OPTIONS]",            0,      1,      acmd_watch },
   { 0,         0,                      0,      0,      0 }
 };
 
 static void acmd_help(admin *a, unsigned ac, char *av[])
 {
   const acmd *c;
-  for (c = acmdtab; c->name; c++)
-    a_info(a, "%s", c->help);
+  for (c = acmdtab; c->name; c++) {
+    if (c->help)
+      a_info(a, "%s", c->name, "*%s", c->help, A_END);
+    else 
+      a_info(a, "%s", c->name, A_END);
+  }
   a_ok(a);
 }
 
@@ -931,38 +1451,32 @@ static void acmd_help(admin *a, unsigned ac, char *av[])
  *             immediately.
  */
 
-static void a_lock(admin *a) { assert(!(a->f & AF_LOCK)); a->f |= AF_LOCK; }
+static void a_lock(admin *a) { a->ref++; }
 
-/* --- @a_unlock@ --- *
+/* --- @a_dodestroy@ --- *
  *
  * Arguments:  @admin *a@ = pointer to an admin block
  *
  * Returns:    ---
  *
- * Use:                Unlocks an admin block, allowing its destruction.  This is
- *             also the second half of @a_destroy@.
+ * Use:                Actually does the legwork of destroying an admin block.
  */
 
-static void a_unlock(admin *a)
+static void a_dodestroy(admin *a)
 {
-  assert(a->f & AF_LOCK);
-  if (!(a->f & AF_DEAD)) {
-    a->f &= ~AF_LOCK;
-    return;
-  }
+  admin_bgop *bg, *bbg;
 
   T( trace(T_ADMIN, "admin: completing destruction of connection %u",
           a->seq); )
 
   selbuf_destroy(&a->b);
-  if (a->pname) {
-    xfree(a->pname);
-    xfree(a->paddr);
-    bres_abort(&a->r);
-    sel_rmtimer(&a->t);
+  for (bg = a->bg; bg; bg = bbg) {
+    bbg = bg->next;
+    bg->cancel(bg);
+    if (bg->tag) xfree(bg->tag);
+    xfree(bg);
   }
-  if (a->b.reader.fd != a->w.fd)
-    close(a->b.reader.fd);
+  if (a->b.reader.fd != a->w.fd) close(a->b.reader.fd);
   close(a->w.fd);
 
   if (a_stdin == a)
@@ -976,6 +1490,23 @@ static void a_unlock(admin *a)
   DESTROY(a);
 }
 
+/* --- @a_unlock@ --- *
+ *
+ * Arguments:  @admin *a@ = pointer to an admin block
+ *
+ * Returns:    ---
+ *
+ * Use:                Unlocks an admin block, allowing its destruction.  This is
+ *             also the second half of @a_destroy@.
+ */
+
+static void a_unlock(admin *a)
+{
+  assert(a->ref);
+  if (!--a->ref && (a->f & AF_DEAD))
+    a_dodestroy(a);
+}
+
 /* --- @a_destroy@ --- *
  *
  * Arguments:  @admin *a@ = pointer to an admin block
@@ -986,6 +1517,17 @@ static void a_unlock(admin *a)
  *             care.
  */
 
+static void freequeue(oqueue *q)
+{
+  obuf *o, *oo;
+
+  for (o = q->hd; o; o = oo) {
+    oo = o->next;
+    xfree(o);
+  }
+  q->hd = q->tl = 0;
+}
+
 static void a_destroy(admin *a)
 {
   /* --- Don't multiply destroy admin blocks --- */
@@ -1000,24 +1542,16 @@ static void a_destroy(admin *a)
 
   /* --- Free the output buffers --- */
 
-  if (a->o_head) {
-    obuf *o, *oo;
+  if (a->out.hd)
     sel_rmfile(&a->w);
-    for (o = a->o_head; o; o = oo) {
-      oo = o->next;
-      xfree(o);
-    }
-    a->o_head = 0;
-  }
+  freequeue(&a->out);
 
   /* --- If the block is locked, that's all we can manage --- */
 
-  if (a->f & AF_LOCK) {
-    T( trace(T_ADMIN, "admin: deferring destruction..."); )
-    return;
-  }
-  a->f |= AF_LOCK;
-  a_unlock(a);
+  if (!a->ref)
+    a_dodestroy(a);
+  T( else 
+     trace(T_ADMIN, "admin: deferring destruction..."); )
 }
 
 /* --- @a_line@ --- *
@@ -1042,7 +1576,12 @@ static void a_line(char *p, size_t len, void *vp)
   if (a->f & AF_DEAD)
     return;
   if (!p) {
-    a_destroy(a);
+    if (!a->bg)
+      a_destroy(a);
+    else {
+      a->f |= AF_CLOSE;
+      selbuf_disable(&a->b);
+    }
     return;
   }
   ac = str_qsplit(p, av, 16, 0, STRF_QUOTE);
@@ -1051,9 +1590,12 @@ static void a_line(char *p, size_t len, void *vp)
   for (c = acmdtab; c->name; c++) {
     if (mystrieq(av[0], c->name)) {
       ac--;
-      if (c->argmin > ac || ac > c->argmax)
-       a_fail(a, "bad-syntax -- %s", c->help);
-      else {
+      if (c->argmin > ac || ac > c->argmax) {
+       if (!c->help)
+         a_fail(a, "bad-syntax", "%s", c->name, "", A_END);
+       else 
+         a_fail(a, "bad-syntax", "%s", c->name, "%s", c->help, A_END);
+      } else {
        a_lock(a);
        c->func(a, ac, av + 1);
        a_unlock(a);
@@ -1061,7 +1603,7 @@ static void a_line(char *p, size_t len, void *vp)
       return;
     }
   }
-  a_fail(a, "unknown-command %s", av[0]);
+  a_fail(a, "unknown-command", "%s", av[0], A_END);
 }
 
 /* --- @a_create@ --- *
@@ -1081,21 +1623,19 @@ void a_create(int fd_in, int fd_out, unsigned f)
   T( static unsigned seq = 0;
      a->seq = seq++; )
   T( trace(T_ADMIN, "admin: accepted connection %u", a->seq); )
-  a->pname = 0;
+  a->bg = 0;
+  a->ref = 0;
   a->f = f;
-  if (fd_in == STDIN_FILENO)
-    a_stdin = a;
+  if (fd_in == STDIN_FILENO) a_stdin = a;
   fdflags(fd_in, O_NONBLOCK, O_NONBLOCK, FD_CLOEXEC, FD_CLOEXEC);
   if (fd_out != fd_in)
     fdflags(fd_out, O_NONBLOCK, O_NONBLOCK, FD_CLOEXEC, FD_CLOEXEC);
   selbuf_init(&a->b, &sel, fd_in, a_line, a);
   sel_initfile(&sel, &a->w, fd_out, SEL_WRITE, a_flush, a);
-  a->o_head = 0;
-  a->o_tail = 0;
+  a->out.hd = a->out.tl = 0;
   a->next = admins;
   a->prev = 0;
-  if (admins)
-    admins->prev = a;
+  if (admins) admins->prev = a;
   admins = a;
 }
 
@@ -1119,7 +1659,7 @@ static void a_accept(int fd, unsigned mode, void *v)
   if ((nfd = accept(fd, (struct sockaddr *)&sun, &sz)) < 0) {
     if (errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK &&
        errno != ECONNABORTED && errno != EPROTO)
-      a_warn("ADMIN accept-error -- %s", strerror(errno));
+      a_warn("ADMIN", "accept-error", "?ERRNO", A_END);
     return;
   }
   a_create(nfd, nfd, 0);