chiark / gitweb /
General overhaul of tunnelling: allow multiple tunnel drivers in one daemon,
[tripe] / admin.c
diff --git a/admin.c b/admin.c
index 766b30528e9170fb254baba6a5183d64d53e8124..24d86c64ac40dcf7a33e61c84d6ccb2eac48ae6a 100644 (file)
--- a/admin.c
+++ b/admin.c
@@ -1,6 +1,6 @@
 /* -*-c-*-
  *
- * $Id: admin.c,v 1.2 2001/02/03 22:40:29 mdw Exp $
+ * $Id$
  *
  * Admin interface for configuration
  *
  * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
  */
 
-/*----- Revision history --------------------------------------------------* 
- *
- * $Log: admin.c,v $
- * Revision 1.2  2001/02/03 22:40:29  mdw
- * Put timer information into the entropy pool when packets are received
- * and on similar events.  Reseed the generator on the interval timer.
- *
- * Revision 1.1  2001/02/03 20:26:37  mdw
- * Initial checkin.
- *
- */
-
 /*----- Header files ------------------------------------------------------*/
 
 #include "tripe.h"
@@ -62,6 +50,14 @@ const trace_opt tr_opts[] = {
 unsigned tr_flags = 0;
 #endif
 
+static const trace_opt w_opts[] = {
+  { 't',       AF_TRACE,       "trace messages" },
+  { 'n',       AF_NOTE,        "asynchronous notifications" },
+  { 'w',       AF_WARN,        "warnings" },
+  { 'A',       AF_ALLMSGS,     "all of the above" },
+  { 0,         0,              0 }
+};
+
 /*----- Static variables --------------------------------------------------*/
 
 static admin *admins;
@@ -76,12 +72,151 @@ static sig s_term, s_int, s_hup;
 
 #define T_RESOLVE SEC(30)
 
+static void a_destroy(admin */*a*/);
+static void a_lock(admin */*a*/);
+static void a_unlock(admin */*a*/);
+
+/*----- Output functions --------------------------------------------------*/
+
+/* --- @trywrite@ --- *
+ *
+ * Arguments:  @admin *a@ = pointer to an admin block
+ *             @const char *p@ = pointer to buffer to write
+ *             @size_t sz@ = size of data to write
+ *
+ * Returns:    The number of bytes written, or less than zero on error.
+ *
+ * Use:                Attempts to write data to a client.
+ */
+
+static ssize_t trywrite(admin *a, const char *p, size_t sz)
+{
+  ssize_t n, done = 0;
+
+again:
+  if (!sz)
+    return (done);
+  n = write(a->w.fd, p, sz);
+  if (n > 0) {
+    done += n;
+    p += n;
+    sz -= n;
+    goto again;
+  }
+  if (n < 0) {
+    if (errno == EINTR)
+      goto again;
+    if (errno != EAGAIN && errno != EWOULDBLOCK) {
+      a_destroy(a);
+      a_warn("ADMIN client-read-error -- %s", strerror(errno));
+      return (-1);
+    }
+  }
+  return (done);
+}
+
+/* --- @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;
+  obuf *o;
+
+  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;
+  if (!o)
+    sel_addfile(&a->w);
+  else if (o->p_in < o->buf + OBUFSZ)
+    goto noalloc;
+
+  do {
+    o = xmalloc(sizeof(obuf));
+    o->next = 0;
+    o->p_in = o->p_out = o->buf;
+    if (a->o_tail)
+      a->o_tail->next = o;
+    else
+      a->o_head = o;
+    a->o_tail = o;
+
+  noalloc:
+    n = o->buf + OBUFSZ - o->p_in;
+    if (n > sz)
+      n = sz;
+    memcpy(o->p_in, p, n);
+    o->p_in += n;
+    p += n;
+    sz -= n;
+  } while (sz);
+}
+
+/* --- @a_flush@ --- *
+ *
+ * Arguments:  @int fd@ = file descriptor
+ *             @unsigned mode@ = what's happening
+ *             @void *v@ = pointer to my admin block
+ *
+ * Returns:    ---
+ *
+ * Use:                Flushes buffers when a client is ready to read again.
+ */
+
+static void a_flush(int fd, unsigned mode, void *v)
+{
+  admin *a = v;
+  obuf *o, *oo;
+  ssize_t n;
+
+  o = a->o_head;
+  while (o) {
+    if ((n = trywrite(a, o->p_out, o->p_in - o->p_out)) < 0)
+      return;
+    o->p_out += n;
+    if (o->p_in < o->p_out)
+      break;
+    oo = o;
+    o = o->next;
+    xfree(oo);
+  }
+  a->o_head = o;
+  if (!o) {
+    a->o_tail = 0;
+    sel_rmfile(&a->w);
+  }
+}
+
 /*----- Utility functions -------------------------------------------------*/
 
-/* --- @a_write@ --- *
+/* --- @a_write@, @a_vwrite@ --- *
  *
  * Arguments:  @admin *a@ = admin connection to write to
+ *             @const char *tag@ = tag prefix string, or null
  *             @const char *fmt@ = pointer to format string
+ *             @va_list ap@ = arguments in list
  *             @...@ = other arguments
  *
  * Returns:    ---
@@ -89,17 +224,124 @@ static sig s_term, s_int, s_hup;
  * Use:                Sends a message to an admin connection.
  */
 
-static void a_write(admin *a, const char *fmt, ...)
+static void a_vwrite(admin *a, const char *tag, const char *fmt, va_list ap)
 {
-  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);
+  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, ...)
+{
+  va_list ap;
   va_start(ap, fmt);
-  dstr_vputf(&d, fmt, ap);
+  a_vwrite(a, tag, fmt, ap);
   va_end(ap);
-  write(a->fd, d.buf, d.len);
+}
+
+/* --- @a_ok@, @a_info@, @a_fail@ --- *
+ *
+ * Arguments:  @admin *a@ = connection
+ *             @const char *fmt@ = format string
+ *             @...@ = other arguments
+ *
+ * Returns:    ---
+ *
+ * Use:                Convenience functions for @a_write@.
+ */
+
+static void a_ok(admin *a) { a_write(a, "OK", 0); }
+
+static void a_info(admin *a, const char *fmt, ...)
+{
+  va_list ap;
+  va_start(ap, fmt);
+  a_vwrite(a, "INFO", fmt, ap);
+  va_end(ap);
+}
+
+static void a_fail(admin *a, const char *fmt, ...)
+{
+  va_list ap;
+  va_start(ap, fmt);
+  a_vwrite(a, "FAIL", 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 *fmt@ = pointer to format string
+ @             @const char *p@ = pointer to raw string
+ *             @size_t sz@ = size of raw string
+ *             @va_list ap@ = arguments in list
+ *             @...@ = other arguments
+ *
+ * Returns:    ---
+ *
+ * Use:                Write a message to all admin connections matched by the given
+ *             filter.
+ */
+
+static void a_rawalert(unsigned f_and, unsigned f_eq, const char *tag,
+                      const char *p, size_t sz)
+{
+  admin *a, *aa;
+  dstr d = DSTR_INIT;
+  
+  if (!(flags & F_INIT))
+    return;
+  if (tag) {
+    dstr_puts(&d, tag);
+    if (p)
+      dstr_putc(&d, ' ');
+  }
+  if (p)
+    dstr_putm(&d, p, sz);
+  dstr_putc(&d, '\n');
+  p = d.buf;
+  sz = d.len;
+  for (a = admins; a; a = aa) {
+    aa = a->next;
+    if ((a->f & f_and) == f_eq)
+      dosend(a, d.buf, d.len);
+  }
   dstr_destroy(&d);
 }
 
+static void a_valert(unsigned f_and, unsigned f_eq, const char *tag,
+                    const char *fmt, va_list ap)
+{
+  dstr d = DSTR_INIT;
+
+  if (!(flags & F_INIT))
+    return;
+  if (fmt)
+    dstr_vputf(&d, fmt, &ap);
+  a_rawalert(f_and, f_eq, tag, fmt ? d.buf : 0, fmt ? d.len : 0);
+  dstr_destroy(&d);
+}
+
+#if 0 /*unused*/
+static void a_alert(unsigned f_and, unsigned f_eq, const char *tag,
+                   const char *fmt, ...)
+{
+  va_list ap;
+  va_start(ap, fmt);
+  a_valert(f_and, f_eq, tag, fmt, ap);
+  va_end(ap);
+}
+#endif
+
 /* --- @a_warn@ --- *
  *
  * Arguments:  @const char *fmt@ = pointer to format string
@@ -113,22 +355,16 @@ static void a_write(admin *a, const char *fmt, ...)
 void a_warn(const char *fmt, ...)
 {
   va_list ap;
-  admin *a;
-  dstr d = DSTR_INIT;
 
-  if (flags & F_INIT)
-    dstr_puts(&d, "WARN ");
   va_start(ap, fmt);
-  dstr_vputf(&d, fmt, ap);
-  va_end(ap);
-  if (!(flags & F_INIT))
-    moan("%s", d.buf);
+  if (flags & F_INIT)
+    a_valert(0, 0, "WARN", fmt, ap);
   else {
-    dstr_putc(&d, '\n');
-    for (a = admins; a; a = a->next)
-      write(a->fd, d.buf, d.len);
+    fprintf(stderr, "%s: ", QUIS);
+    vfprintf(stderr, fmt, ap);
+    fputc('\n', stderr);
   }
-  dstr_destroy(&d);
+  va_end(ap);
 }
 
 /* --- @a_trace@ --- *
@@ -139,24 +375,36 @@ void a_warn(const char *fmt, ...)
  *
  * Returns:    ---
  *
- * Use:                Custom trace output handler.
+ * Use:                Custom trace output handler.  Sends trace messages to
+ *             interested admin connections.
  */
 
 #ifndef NTRACE
 static void a_trace(const char *p, size_t sz, void *v)
 {
-  dstr d = DSTR_INIT;
-  admin *a;
-
-  dstr_puts(&d, "TRACE ");
-  dstr_putm(&d, p, sz);
-  dstr_putc(&d, '\n');
-  for (a = admins; a; a = a->next)
-    write(a->fd, d.buf, d.len);
-  dstr_destroy(&d);  
+  a_rawalert(AF_TRACE, AF_TRACE, "TRACE", p, sz);
 }
 #endif
 
+/* --- @a_notify@ --- *
+ *
+ * Arguments:  @const char *fmt@ = pointer to format string
+ *             @...@ = other arguments
+ *
+ * Returns:    ---
+ *
+ * Use:                Sends a notification to interested admin connections.
+ */
+
+void a_notify(const char *fmt, ...)
+{
+  va_list ap;
+
+  va_start(ap, fmt);
+  a_valert(AF_NOTE, AF_NOTE, "NOTE", fmt, ap);
+  va_end(ap);
+}
+
 /* --- @a_quit@ --- *
  *
  * Arguments:  ---
@@ -168,6 +416,10 @@ static void a_trace(const char *p, size_t sz, void *v)
 
 void a_quit(void)
 {
+  peer *p;
+
+  while ((p = p_first()) != 0)
+    p_destroy(p);
   close(sock.fd);
   unlink(sockname);
   exit(0);
@@ -192,11 +444,11 @@ static void a_sigdie(int sig, void *v)
     case SIGTERM:      p = "SIGTERM"; break;
     case SIGINT:       p = "SIGINT"; break;
     default:
-      sprintf(buf, "signal %i", sig);
+      sprintf(buf, "%i", sig);
       p = buf;
       break;
   }
-  a_warn("shutting down on %s", p);
+  a_warn("SERVER quit signal %s", p);
   a_quit();
 }
 
@@ -212,7 +464,7 @@ static void a_sigdie(int sig, void *v)
 
 static void a_sighup(int sig, void *v)
 {
-  a_warn("received SIGHUP: ignoring");
+  a_warn("SERVER ignore signal SIGHUP");
 }
 
 /*----- Adding peers ------------------------------------------------------*/
@@ -230,24 +482,27 @@ static void a_sighup(int sig, void *v)
 static void a_resolve(struct hostent *h, void *v)
 {
   admin *a = v;
+
+  a_lock(a);
   T( trace(T_ADMIN, "admin: %u resolved", a->seq); )
   TIMER;
   sel_rmtimer(&a->t);
   if (!h)
-    a_write(a, "ERR couldn't resolve hostname `%s'\n", a->paddr);
+    a_fail(a, "resolve-error %s", a->paddr);
   else if (p_find(a->pname))
-    a_write(a, "ERR peer `%s' already registered\n", 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->peer.sa, a->sasz))
-      a_write(a, "ERR couldn't create peer\n");
+    if (!p_create(a->pname, a->tops, &a->peer.sa, a->sasz))
+      a_fail(a, "peer-create-fail %s", a->pname);
     else
-      a_write(a, "OK\n");
+      a_ok(a);
   }
   xfree(a->pname);
   xfree(a->paddr);
   a->pname = 0;
   selbuf_enable(&a->b);
+  a_unlock(a);
 }
 
 /* --- @a_timer@ --- *
@@ -263,13 +518,16 @@ static void a_resolve(struct hostent *h, void *v)
 static void a_timer(struct timeval *tv, void *v)
 {
   admin *a = v;
+
+  a_lock(a);
   T( trace(T_ADMIN, "admin: %u resolver timeout", a->seq); )
   bres_abort(&a->r);
-  a_write(a, "ERR timeout resolving `%s'\n", a->paddr);
+  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);
 }
 
 /* --- @acmd_add@ --- *
@@ -287,42 +545,77 @@ 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;
 
   /* --- Make sure someone's not got there already --- */
 
   if (p_find(av[0])) {
-    a_write(a, "ERR peer `%s' already registered\n", av[0]);
+    a_fail(a, "peer-exists %s", av[0]);
     return;
   }
 
+  /* --- Parse options --- */
+
+  i = 1;
+  for (;;) {
+    if (!av[i])
+      goto bad_syntax;
+    if (mystrieq(av[i], "-tunnel")) {
+      i++;
+      if (!av[i])
+       goto bad_syntax;
+      for (j = 0;; j++) {
+       if (!tunnels[j]) {
+         a_fail(a, "unknown-tunnel %s", av[i]);
+         return;
+       }
+       if (mystrieq(av[i], tunnels[j]->name)) {
+         tops = tunnels[j];
+         break;
+       }
+      }
+      i++;
+    } else if (mystrieq(av[i], "--")) {
+      i++;
+      break;
+    } else
+      break;
+  }
+
   /* --- Fill in the easy bits of address --- */
 
   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[2], &p, 0);
+  pt = strtoul(av[i + 1], &p, 0);
   if (*p) {
-    struct servent *s = getservbyname(av[2], "udp");
+    struct servent *s = getservbyname(av[i + 1], "udp");
     if (!s) {
-      a_write(a, "ERR service `%s' not known\n", av[2]);
+      a_fail(a, "unknown-service %s", av[i + 1]);
       return;
     }
     pt = ntohs(s->s_port);
   }
   if (pt == 0 || pt >= 65536) {
-    a_write(a, "ERR bad port number %lu\n", pt);
+    a_fail(a, "invalid-port %lu", pt);
     return;
   }
   a->peer.sin.sin_port = htons(pt);
 
   /* --- If the name is numeric, do it the easy way --- */
   
-  if (inet_aton(av[1], &a->peer.sin.sin_addr)) {
-    if (!p_create(av[0], &a->peer.sa, a->sasz))
-      a_write(a, "ERR couldn't create peer\n");
+  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_write(a, "OK\n");
+      a_ok(a);
     return;
   }
 
@@ -334,7 +627,8 @@ static void acmd_add(admin *a, unsigned ac, char *av[])
    */
 
   a->pname = xstrdup(av[0]);
-  a->paddr = xstrdup(av[1]);
+  a->paddr = xstrdup(av[i]);
+  a->tops = tops;
   selbuf_disable(&a->b);
   gettimeofday(&tv, 0);
   tv.tv_sec += T_RESOLVE;
@@ -342,27 +636,47 @@ static void acmd_add(admin *a, unsigned ac, char *av[])
   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 ...");
+  return;
 }
 
 /*----- Administration commands -------------------------------------------*/
 
 /* --- Miscellaneous commands --- */
 
-#ifndef NTRACE
+/* --- @traceish@ --- *
+ *
+ * Arguments:  @admin *a@ = connection to complain on
+ *             @unsigned ac@ = number of arguments
+ *             @char *av[]@ = vector of arguments
+ *             @const char *what@ = what we're messing with
+ *             @const trace_opt *tt@ = options table
+ *             @unsigned *ff@ = where the flags are
+ *
+ * Returns:    Nonzero if anything changed.
+ *
+ * Use:                Guts of trace-ish commands like `trace' and `watch'.
+ */
 
-static void acmd_trace(admin *a, unsigned ac, char *av[])
+static int traceish(admin *a, unsigned ac, char *av[],
+                   const char *what, const trace_opt *tt, unsigned *ff)
 {
+  int ch = 0;
+
   if (!ac || strcmp(av[0], "?") == 0) {
     const trace_opt *t;
-    a_write(a, "INFO Trace options:\n");
-    for (t = tr_opts; t->ch; t++) {
-      a_write(a, "INFO %c %c  %s\n",
-             t->ch, (tr_flags & t->f) == t->f ? '*' : ' ', t->help);
+    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);
     }
   } else {
     unsigned sense = 1;
-    unsigned f = tr_flags;
-    const trace_opt *tt;
+    unsigned f = *ff;
+    const trace_opt *t;
     char *p = av[0];
 
     while (*p) {
@@ -370,48 +684,99 @@ static void acmd_trace(admin *a, unsigned ac, char *av[])
        case '+': sense = 1; break;
        case '-': sense = 0; break;
        default:
-         for (tt = tr_opts; tt->ch; tt++) {
-           if (tt->ch == *p) {
-             if (sense) f |= tt->f;
-             else f &= ~tt->f;
+         for (t = tt; t->ch; t++) {
+           if (t->ch == *p) {
+             if (sense) f |= t->f;
+             else f &= ~t->f;
              goto tropt_ok;
            }
          }
-         a_write(a, "ERR unknown trace option `%c'\n", *p);
-         return;
+         a_fail(a, "bad-%s-option %c", what, *p);
+         return (0);
         tropt_ok:;
          break;
       }
       p++;
     }
-    tr_flags = f;
-    trace_level(tr_flags);
+    *ff = f;
+    ch = 1;
   }
-  a_write(a, "OK\n");
+  a_ok(a);
+  return (ch);
+}
+
+#ifndef NTRACE
+
+static void acmd_trace(admin *a, unsigned ac, char *av[])
+{
+  if (traceish(a, ac, av, "trace", tr_opts, &tr_flags))
+    trace_level(tr_flags);
 }
 
 #endif
 
-static void acmd_port(admin *a, unsigned ac, char *av[])
+static void acmd_watch(admin *a, unsigned ac, char *av[])
 {
-  a_write(a, "INFO %u\nOK\n", p_port());
+  traceish(a, ac, av, "watch", w_opts, &a->f);
 }
 
-static void a_destroy(admin */*a*/);
+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[])
+{
+  dstr d = DSTR_INIT;
+  unsigned i;
+
+  dstr_puts(&d, "USER");
+  for (i = 0; i < ac; i++)
+    quotify(&d, av[i]);
+  dstr_putz(&d);
+  a_rawalert(f_and, f_eq, tag, d.buf, d.len);
+  dstr_destroy(&d);
+  a_ok(a);
+}
+
+static void acmd_notify(admin *a, unsigned ac, char *av[])
+  { alertcmd(a, AF_NOTE, AF_NOTE, "NOTE", ac, av); }
+static void acmd_warn(admin *a, unsigned ac, char *av[])
+  { alertcmd(a, AF_WARN, AF_WARN, "WARN", ac, av); }
+
+static void acmd_port(admin *a, unsigned ac, char *av[])
+{
+  a_info(a, "%u", p_port());
+  a_ok(a);
+}
 
 static void acmd_daemon(admin *a, unsigned ac, char *av[])
 {
   if (flags & F_DAEMON)
-    a_write(a, "ERR already running as a daemon\n");
+    a_fail(a, "already-daemon");
   else {
-    if (a_stdin) {
-      a_write(a_stdin, "WARN becoming a daemon\n");
+    a_notify("DAEMON");
+    if (a_stdin)
       a_destroy(a_stdin);
-    }
     if (u_daemon())
-      a_write(a, "ERR error becoming a daemon: %s", strerror(errno));
-    else
+      a_fail(a, "daemon-error -- %s", strerror(errno));
+    else {
       flags |= F_DAEMON;
+      a_ok(a);
+    }
   }
 }
 
@@ -419,8 +784,8 @@ static void acmd_list(admin *a, unsigned ac, char *av[])
 {
   peer *p;
   for (p = p_first(); p; p = p_next(p))
-    a_write(a, "INFO %s\n", p_name(p));
-  a_write(a, "OK\n");
+    a_info(a, "%s", p_name(p));
+  a_ok(a);
 }
 
 static void acmd_ifname(admin *a, unsigned ac, char *av[])
@@ -428,9 +793,11 @@ static void acmd_ifname(admin *a, unsigned ac, char *av[])
   peer *p;
 
   if ((p = p_find(av[0])) == 0)
-    a_write(a, "ERR peer `%s' not found\n", av[0]);
-  else
-    a_write(a, "INFO %s\nOK\n", p_ifname(p));
+    a_fail(a, "unknown-peer %s", av[0]);
+  else {
+    a_info(a, "%s", p_ifname(p));
+    a_ok(a);
+  }
 }
 
 static void acmd_addr(admin *a, unsigned ac, char *av[])
@@ -439,13 +806,42 @@ static void acmd_addr(admin *a, unsigned ac, char *av[])
   const addr *ad;
 
   if ((p = p_find(av[0])) == 0)
-    a_write(a, "ERR peer `%s' not found\n", av[0]);
+    a_fail(a, "unknown-peer %s", av[0]);
   else {
     ad = p_addr(p);
     assert(ad->sa.sa_family == AF_INET);
-    a_write(a, "INFO %s %u\nOK\n",
+    a_info(a, "INET %s %u",
            inet_ntoa(ad->sin.sin_addr),
            (unsigned)ntohs(ad->sin.sin_port));
+    a_ok(a);
+  }
+}
+
+static void acmd_stats(admin *a, unsigned ac, char *av[])
+{
+  peer *p;
+  stats *st;
+
+  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);
+    a_ok(a);
   }
 }
 
@@ -453,19 +849,34 @@ static void acmd_kill(admin *a, unsigned ac, char *av[])
 {
   peer *p;
   if ((p = p_find(av[0])) == 0)
-    a_write(a, "ERR peer `%s' not found\n", av[0]);
+    a_fail(a, "unknown-peer %s", av[0]);
   else {
     p_destroy(p);
-    a_write(a, "OK\n");
+    a_ok(a);
   }
 }
 
 static void acmd_quit(admin *a, unsigned ac, char *av[])
 {
-  a_warn("closing down on admin request");
+  a_warn("SERVER quit admin-request");
+  a_ok(a);
   a_quit();
 }
 
+static void acmd_version(admin *a, unsigned ac, char *av[])
+{
+  a_info(a, "%s %s", PACKAGE, VERSION);
+  a_ok(a);
+}
+
+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_ok(a);
+}
+
 /* --- The command table and help --- */
 
 typedef struct acmd {
@@ -478,18 +889,25 @@ typedef struct acmd {
 static void acmd_help(admin */*a*/, unsigned /*ac*/, char */*av*/[]);
 
 static const acmd acmdtab[] = {
-  { "help",    "HELP",                 0,      0,      acmd_help },
+  { "help",    "help",                 0,      0,      acmd_help },
+  { "version", "version",              0,      0,      acmd_version },
 #ifndef NTRACE
-  { "trace",   "TRACE [options]",      0,      1,      acmd_trace },
+  { "trace",   "trace [OPTIONS]",      0,      1,      acmd_trace },
 #endif
-  { "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 },
-  { "kill",    "KILL peer",            1,      1,      acmd_kill },
-  { "add",     "ADD peer addr port",   3,      3,      acmd_add },
-  { "quit",    "QUIT",                 0,      0,      acmd_quit },
+  { "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 },
   { 0,         0,                      0,      0,      0 }
 };
 
@@ -497,48 +915,115 @@ static void acmd_help(admin *a, unsigned ac, char *av[])
 {
   const acmd *c;
   for (c = acmdtab; c->name; c++)
-    a_write(a, "INFO %s\n", c->help);
-  a_write(a, "OK\n");
+    a_info(a, "%s", c->help);
+  a_ok(a);
 }
 
 /*----- Connection handling -----------------------------------------------*/
 
-/* --- @a_destroy@ --- *
+/* --- @a_lock@ --- *
  *
  * Arguments:  @admin *a@ = pointer to an admin block
  *
  * Returns:    ---
  *
- * Use:                Destroys an admin block.
+ * Use:                Locks an admin block so that it won't be destroyed
+ *             immediately.
  */
 
-static void a_destroy(admin *a)
+static void a_lock(admin *a) { assert(!(a->f & AF_LOCK)); a->f |= AF_LOCK; }
+
+/* --- @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)
 {
-  T( trace(T_ADMIN, "admin: destroying connection %u", a->seq); )
+  assert(a->f & AF_LOCK);
+  if (!(a->f & AF_DEAD)) {
+    a->f &= ~AF_LOCK;
+    return;
+  }
+
+  T( trace(T_ADMIN, "admin: completing destruction of connection %u",
+          a->seq); )
+
   selbuf_destroy(&a->b);
-  if (a->b.reader.fd != a->fd)
-    close(a->b.reader.fd);
-  close(a->fd);
   if (a->pname) {
     xfree(a->pname);
     xfree(a->paddr);
     bres_abort(&a->r);
     sel_rmtimer(&a->t);
   }
+  if (a->b.reader.fd != a->w.fd)
+    close(a->b.reader.fd);
+  close(a->w.fd);
+
+  if (a_stdin == a)
+    a_stdin = 0;
   if (a->next)
     a->next->prev = a->prev;
   if (a->prev)
     a->prev->next = a->next;
   else
     admins = a->next;
-  if (a_stdin == a)
-    a_stdin = 0;
   DESTROY(a);
 }
 
+/* --- @a_destroy@ --- *
+ *
+ * Arguments:  @admin *a@ = pointer to an admin block
+ *
+ * Returns:    ---
+ *
+ * Use:                Destroys an admin block.  This requires a certain amount of
+ *             care.
+ */
+
+static void a_destroy(admin *a)
+{
+  /* --- Don't multiply destroy admin blocks --- */
+
+  if (a->f & AF_DEAD)
+    return;
+
+  /* --- Make sure nobody expects it to work --- */
+
+  a->f |= AF_DEAD;
+  T( trace(T_ADMIN, "admin: destroying connection %u", a->seq); )
+
+  /* --- Free the output buffers --- */
+
+  if (a->o_head) {
+    obuf *o, *oo;
+    sel_rmfile(&a->w);
+    for (o = a->o_head; o; o = oo) {
+      oo = o->next;
+      xfree(o);
+    }
+    a->o_head = 0;
+  }
+
+  /* --- 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);
+}
+
 /* --- @a_line@ --- *
  *
  * Arguments:  @char *p@ = pointer to the line read
+ *             @size_t len@ = length of the line
  *             @void *vp@ = pointer to my admin block
  *
  * Returns:    ---
@@ -546,58 +1031,67 @@ static void a_destroy(admin *a)
  * Use:                Handles a line of input.
  */
 
-static void a_line(char *p, void *vp)
+static void a_line(char *p, size_t len, void *vp)
 {
   admin *a = vp;
   const acmd *c;
-  char *av[4];
+  char *av[16];
   size_t ac;
 
   TIMER;
+  if (a->f & AF_DEAD)
+    return;
   if (!p) {
     a_destroy(a);
     return;
   }
-  ac = str_qsplit(p, av, 4, 0, STRF_QUOTE);
+  ac = str_qsplit(p, av, 16, 0, STRF_QUOTE);
   if (!ac)
     return;
-  for (p = av[0]; *p; p++) *p = tolower((unsigned char)*p);
   for (c = acmdtab; c->name; c++) {
-    if (strcmp(av[0], c->name) == 0) {
+    if (mystrieq(av[0], c->name)) {
       ac--;
       if (c->argmin > ac || ac > c->argmax)
-       a_write(a, "ERR syntax: %s\n", c->help);
-      else
+       a_fail(a, "bad-syntax -- %s", c->help);
+      else {
+       a_lock(a);
        c->func(a, ac, av + 1);
+       a_unlock(a);
+      }
       return;
     }
   }
-  a_write(a, "ERR unknown command `%s'\n", av[0]);
+  a_fail(a, "unknown-command %s", av[0]);
 }
 
 /* --- @a_create@ --- *
  *
  * Arguments:  @int fd_in, fd_out@ = file descriptors to use
+ *             @unsigned f@ = initial flags to set
  *
  * Returns:    ---
  *
  * Use:                Creates a new admin connection.
  */
 
-void a_create(int fd_in, int fd_out)
+void a_create(int fd_in, int fd_out, unsigned f)
 {
   admin *a = CREATE(admin);
-  T( static unsigned seq = 0; )
-  a->seq = seq++;
+
+  T( static unsigned seq = 0;
+     a->seq = seq++; )
   T( trace(T_ADMIN, "admin: accepted connection %u", a->seq); )
   a->pname = 0;
+  a->f = f;
   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);
-  a->fd = fd_out;
   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->next = admins;
   a->prev = 0;
   if (admins)
@@ -623,10 +1117,12 @@ static void a_accept(int fd, unsigned mode, void *v)
   size_t sz = sizeof(sun);
 
   if ((nfd = accept(fd, (struct sockaddr *)&sun, &sz)) < 0) {
-    a_warn("accept admin connection failed: %s", strerror(errno));
+    if (errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK &&
+       errno != ECONNABORTED && errno != EPROTO)
+      a_warn("ADMIN accept-error -- %s", strerror(errno));
     return;
   }
-  a_create(nfd, nfd);
+  a_create(nfd, nfd, 0);
 }
 
 /* --- @a_daemon@ --- *
@@ -698,7 +1194,7 @@ again:
     }
     if (!S_ISSOCK(st.st_mode))
       die(EXIT_FAILURE, "object `%s' isn't a socket", sun.sun_path);
-    T( trace(T_ADMIN, "stale socket found; removing it"); )
+    T( trace(T_ADMIN, "admin: stale socket found; removing it"); )
     unlink(sun.sun_path);
     close(fd);
     goto again;
@@ -722,6 +1218,7 @@ again:
 
   sig_add(&s_term, SIGTERM, a_sigdie, 0);
   sig_add(&s_hup, SIGHUP, a_sighup, 0);
+  signal(SIGPIPE, SIG_IGN);
   sigaction(SIGINT, 0, &sa);
   if (sa.sa_handler != SIG_IGN)
     sig_add(&s_int, SIGINT, a_sigdie, 0);