More support scripts and other cool stuff.
authormdw <mdw>
Tue, 4 Oct 2005 18:25:28 +0000 (18:25 +0000)
committermdw <mdw>
Tue, 4 Oct 2005 18:25:28 +0000 (18:25 +0000)
19 files changed:
Makefile.am
admin.c
configure.in
debian/control
debian/rules
doc/Makefile.am
doc/tripe-admin.5
doc/tripe-keys.8 [new file with mode: 0644]
doc/tripe-keys.conf.5 [new file with mode: 0644]
doc/tripemon.1 [new file with mode: 0644]
peer.c
tripe-keys.in [new file with mode: 0644]
tripe-keys.master [new file with mode: 0644]
tripe.h
tripemon.in [new file with mode: 0644]
tun-bsd.c
tun-linux.c
tun-slip.c
tun-unet.c

index f5f5a25..8fbec70 100644 (file)
@@ -37,6 +37,10 @@ tun = @tun@
 sbin_PROGRAMS = tripe
 bin_PROGRAMS = tripectl tripe-mitm pkstream
 noinst_SCRIPTS = tripe-init
+bin_SCRIPTS = @pyscripts@ @pygtkscripts@
+PYTHONSCRIPTS = tripe-keys
+PYGTKSCRIPTS = tripemon
+EXTRA_SCRIPTS = ${PYTHONSCRIPTS} ${PYGTKSCRIPTS}
 tripe_SOURCES = \
        tripe.c tripe.h tripe-protocol.h \
        admin.c peer.c \
@@ -59,6 +63,7 @@ install-hook:
 
 EXTRA_DIST = tripe.conf \
        debian/rules debian/control debian/changelog debian/copyright \
-       debian/tripe.postinst debian/tripe.prerm debian/tripe.postrm
+       debian/tripe.postinst debian/tripe.prerm debian/tripe.postrm \
+       tripe-keys.in
 
 ##----- That's all, folks ---------------------------------------------------
diff --git a/admin.c b/admin.c
index 92ea0a6..987de7a 100644 (file)
--- a/admin.c
+++ b/admin.c
@@ -77,6 +77,8 @@ 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@ --- *
@@ -512,6 +514,9 @@ static long a_parsetime(const char *p)
 
 /*----- Backgrounded operations -------------------------------------------*/
 
+#define BGTAG(bg)                                                      \
+  (((admin_bgop *)(bg))->tag ? ((admin_bgop *)(bg))->tag : "<foreground>")
+
 /* --- @a_bgrelease@ --- *
  *
  * Arguments:  @admin_bgop *bg@ = backgrounded operation
@@ -526,19 +531,15 @@ static void a_bgrelease(admin_bgop *bg)
 {
   admin *a = bg->a;
 
-  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;
+  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);
+  if (a->f & AF_CLOSE) a_destroy(a);
+  a_unlock(a);
 }
 
 /* --- @a_bgok@, @a_bginfo@, @a_bgfail@ --- *
@@ -596,7 +597,10 @@ static void a_bgadd(admin *a, admin_bgop *bg, const char *tag,
   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, 0);
 }
 
@@ -613,6 +617,7 @@ static void a_bgadd(admin *a, admin_bgop *bg, const char *tag,
 
 static void a_addfree(admin_addop *add)
 {
+  T( trace(T_ADMIN, "admin: free add op %s", BGTAG(add)); )
   if (add->peer.name) xfree(add->peer.name);
   if (add->paddr) xfree(add->paddr);
 }
@@ -630,6 +635,7 @@ static void a_addcancel(admin_bgop *bg)
 {
   admin_addop *add = (admin_addop *)bg;
 
+  T( trace(T_ADMIN, "admin: cancel add op %s", BGTAG(add)); )
   sel_rmtimer(&add->t);
   bres_abort(&add->r);
   a_addfree(add);
@@ -668,8 +674,7 @@ static void a_addresolve(struct hostent *h, void *v)
 {
   admin_addop *add = v;
 
-  a_lock(add->bg.a);
-  T( trace(T_ADMIN, "admin: %u resolved", add->bg.a->seq); )
+  T( trace(T_ADMIN, "admin: add op %s resolved", BGTAG(add)); )
   TIMER;
   if (!h)
     a_bgfail(&add->bg, "resolve-error %s", add->paddr);
@@ -680,7 +685,6 @@ static void a_addresolve(struct hostent *h, void *v)
   sel_rmtimer(&add->t);
   a_addfree(add);
   a_bgrelease(&add->bg);
-  a_unlock(add->bg.a);
 }
 
 /* --- @a_addtimer@ --- *
@@ -697,13 +701,11 @@ static void a_addtimer(struct timeval *tv, void *v)
 {
   admin_addop *add = v;
 
-  a_lock(add->bg.a);
-  T( trace(T_ADMIN, "admin: %u resolver timeout", add->bg.a->seq); )
+  T( trace(T_ADMIN, "admin: add op %s timeout", BGTAG(add)); )
   a_bgfail(&add->bg, "resolver-timeout %s\n", add->paddr);
   bres_abort(&add->r);
   a_addfree(add);
   a_bgrelease(&add->bg);
-  a_unlock(add->bg.a);
 }
 
 /* --- @acmd_add@ --- *
@@ -810,10 +812,13 @@ static void acmd_add(admin *a, unsigned ac, char *av[])
    */
 
   a_bgadd(a, &add->bg, tag, a_addcancel);
+  T( trace(T_ADMIN, "admin: %u, add op %s resolving hostname `%s'",
+          a->seq, BGTAG(add), add->paddr); )
 
   /* --- If the name is numeric, do it the easy way --- */
   
   if (inet_aton(av[i], &add->peer.sa.sin.sin_addr)) {
+    T( trace(T_ADMIN, "admin: add op %s done the easy way", BGTAG(add)); )
     a_doadd(add);
     a_addfree(add);
     a_bgrelease(&add->bg);
@@ -826,8 +831,6 @@ static void acmd_add(admin *a, unsigned ac, char *av[])
   tv.tv_sec += T_RESOLVE;
   sel_addtimer(&sel, &add->t, &tv, a_addtimer, add);
   bres_byname(&add->r, add->paddr, a_addresolve, add);
-  T( trace(T_ADMIN, "admin: %u resolving hostname `%s'",
-          a->seq, add->paddr); )
   return;
 
 bad_syntax:
@@ -854,6 +857,7 @@ fail:
 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);
 }
 
@@ -873,7 +877,6 @@ static void a_pong(int rc, void *v)
   struct timeval tv;
   double millis;
 
-  a_lock(pg->bg.a);
   switch (rc) {
     case PING_OK:
       gettimeofday(&tv, 0);
@@ -893,8 +896,8 @@ static void a_pong(int rc, void *v)
     default:
       abort();
   }
+  T( trace(T_ADMIN, "admin: ponged ping op %s", BGTAG(pg)); )
   a_bgrelease(&pg->bg);
-  a_unlock(pg->bg.a);
 }
 
 /* --- @acmd_ping@, @acmd_eping@ --- *
@@ -946,6 +949,8 @@ static void a_ping(admin *a, unsigned ac, char *av[],
   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_bgrelease(&pg->bg);
@@ -987,10 +992,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);
     }
   } else {
     unsigned sense = 1;
@@ -1136,32 +1140,57 @@ static void acmd_addr(admin *a, unsigned ac, char *av[])
   }
 }
 
+static void acmd_peerinfo(admin *a, unsigned ac, char *av[])
+{
+  peer *p;
+  const peerspec *ps;
+
+  if ((p = p_find(av[0])) == 0) {
+    a_fail(a, "unknown-peer %s", av[0]);
+    return;
+  }
+
+  ps = p_spec(p);
+  a_info(a, "tunnel=%s", ps->tops->name);
+  a_info(a, "keepalive=%lu", ps->t_ka);
+  a_ok(a);
+}
+
+static void acmd_servinfo(admin *a, unsigned ac, char *av[])
+{
+  a_info(a, "implementation=edgeware-tripe");
+  a_info(a, "version=%s", VERSION);
+  a_info(a, "daemon=%s", BOOL(flags & F_DAEMON));
+  a_ok(a);
+}
+
 static void acmd_stats(admin *a, unsigned ac, char *av[])
 {
   peer *p;
   stats *st;
 
-  if ((p = p_find(av[0])) == 0)
+  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);
+    return;
   }
+
+  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);
 }
 
 static void acmd_kill(admin *a, unsigned ac, char *av[])
@@ -1234,10 +1263,12 @@ static const acmd acmdtab[] = {
   { "kill",    "kill PEER",            1,      1,      acmd_kill },
   { "list",    "list",                 0,      0,      acmd_list },
   { "notify",  "notify MESSAGE ...",   1,      0xffff, acmd_notify },
+  { "peerinfo",        "peerinfo PEER",        1,      1,      acmd_peerinfo },
   { "ping",    "ping [OPTIONS] PEER",  1,      0xffff, acmd_ping },
   { "port",    "port",                 0,      0,      acmd_port },
   { "quit",    "quit",                 0,      0,      acmd_quit },
   { "reload",  "reload",               0,      0,      acmd_reload },
+  { "servinfo",        "servinfo",             0,      0,      acmd_servinfo },
   { "stats",   "stats PEER",           1,      1,      acmd_stats },
 #ifndef NTRACE
   { "trace",   "trace [OPTIONS]",      0,      1,      acmd_trace },
@@ -1269,32 +1300,20 @@ 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)
 {
   admin_bgop *bg, *bbg;
-  
-  assert(a->f & AF_LOCK);
-
-  /* --- If we're not dead, that's fine --- */
-
-  if (!(a->f & AF_DEAD)) {
-    a->f &= ~AF_LOCK;
-    return;
-  }
-
-  /* --- If we are, then destroy the rest of the block --- */
 
   T( trace(T_ADMIN, "admin: completing destruction of connection %u",
           a->seq); )
@@ -1320,6 +1339,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
@@ -1361,12 +1397,10 @@ static void a_destroy(admin *a)
 
   /* --- 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@ --- *
index 70ceb79..690c193 100644 (file)
@@ -33,6 +33,30 @@ AC_CANONICAL_HOST
 AC_PROG_MAKE_SET
 AC_PROG_CC
 AM_PROG_LIBTOOL
+python=no
+mdw_PROG_PYTHON([2.3], 
+  [python=yes
+   pyscripts='${PYTHONSCRIPTS}' 
+   pymans='${PYTHONMANS}'])
+AC_SUBST([pyscripts]) AC_SUBST([pymans])
+
+if test $python = yes; then
+  mdw_CHECK_PYTHON([2.3])
+  AC_CACHE_CHECK([for pygtk], [mdw_cv_pygtk], [
+    mdw_cv_pygtk=no
+    python -c >&5 2>&5 '
+import pygtk
+pygtk.require("2.0")
+import gtk 
+' && mdw_cv_pygtk=yes
+  ])
+  if test $mdw_cv_pygtk = yes; then
+    pygtkscripts='${PYGTKSCRIPTS}'
+    pygtkmans='${PYGTKMANS}'
+  fi
+fi
+AC_SUBST([pygtkscripts]) AC_SUBST([pygtkmans])
+
 AC_CHECK_HEADERS([stdarg.h])
 mdw_GCC_FLAGS([-Wall])
 mdw_OPT_TRACE
@@ -226,6 +250,8 @@ mdw_DEFINE_PATHS([
   AC_SUBST(initconfig)
 ])
 AC_SUBST(DIRS)
-AC_OUTPUT(Makefile doc/Makefile ethereal/Makefile tripe-init)
+AC_OUTPUT( \
+       Makefile doc/Makefile ethereal/Makefile \
+       tripe-init tripe-keys tripemon)
 
 dnl ----- That's all, folks -------------------------------------------------
index 4386a9a..ef4865c 100644 (file)
@@ -37,3 +37,22 @@ Description: Trivial IP Encryption: a simple virtual private network
  and authenticity of packets it sends and receives.
  .
  This package contains the protocol analysis plug-in for Ethereal.
+
+Package: tripemon
+Architecture: all
+Depends: python (>= 2.3), python-gtk2 (>= 2.6), tripe
+Description: Trivial IP Encryption: a simple virtual private network
+ TrIPE is a simple VPN protocol.  It uses cryptography to ensure secrecy
+ and authenticity of packets it sends and receives.
+ .
+ This package contains a graphical monitor program for managing and
+ keeping an eye on a TrIPE server.
+
+Package: tripe-keys
+Architecture: all
+Depends: python (>= 2.3), tripe, catacomb-bin, python-catacomb
+Description: Trivial IP Encryption: a simple virtual private network
+ TrIPE is a simple VPN protocol.  It uses cryptography to ensure secrecy
+ and authenticity of packets it sends and receives.
+ .
+ This package contains a tool for centrally managing TrIPE keys.
index 6a52d03..7b94252 100755 (executable)
@@ -30,6 +30,24 @@ install: build
        cp deb-build/tripe-init debian/tripe/etc/init.d/tripe
        chmod 755 debian/tripe/etc/init.d/tripe
        cp tripe.conf debian/tripe/etc/default/tripe
+       mkdir -p debian/tripe-keys/usr/bin
+       mv debian/tripe/usr/bin/tripe-keys debian/tripe-keys/usr/bin
+       mkdir -p \
+         debian/tripe-keys/usr/share/man/man5 \
+         debian/tripe-keys/usr/share/man/man8
+       mv debian/tripe/usr/share/man/man5/tripe-keys.conf.5 \
+         debian/tripe-keys/usr/share/man/man5
+       mv debian/tripe/usr/share/man/man8/tripe-keys.8 \
+         debian/tripe-keys/usr/share/man/man8
+       mkdir -p debian/tripe-keys/usr/share/doc/tripe-keys/examples
+       cp tripe-keys.master \
+         debian/tripe-keys/usr/share/doc/tripe-keys/examples
+       mkdir -p debian/tripemon/usr/bin
+       mv debian/tripe/usr/bin/tripemon debian/tripemon/usr/bin
+       mkdir -p \
+         debian/tripemon/usr/share/man/man1
+       mv debian/tripe/usr/share/man/man1/tripemon.1 \
+         debian/tripemon/usr/share/man/man1
        mkdir -p debian/pkstream/usr/bin
        mv debian/tripe/usr/bin/pkstream debian/pkstream/usr/bin
        mkdir -p debian/pkstream/usr/share/man/man1
@@ -40,7 +58,17 @@ install: build
        rm -f debian/tripe-ethereal/usr/lib/ethereal/plugins/*/*.a
        rmdir debian/tripe/usr/lib
 
-binary-indep:
+binary-indep: install
+       dh_testdir -i
+       dh_testroot -i
+       dh_compress -i
+       dh_installdocs -i
+       dh_installlogrotate -i
+       dh_gencontrol -i
+       dh_fixperms -i
+       dh_installdeb -i
+       dh_md5sums -i
+       dh_builddeb -i
 
 binary-arch: install
        dpkg --status ethereal | \
index b537576..ee4efdd 100644 (file)
@@ -1,6 +1,6 @@
 ## -*-makefile-*-
 ##
-## $Id: Makefile.am,v 1.5 2004/04/08 01:36:17 mdw Exp $
+## $Id$
 ##
 ## Makefile for TrIPE documentation
 ##
 
 AUTOMAKE_OPTIONS = foreign
 
-man_MANS = tripe.8 tripectl.1 tripe-admin.5 pkstream.1 tripe-mitm.8
-EXTRA_DIST = $(man_MANS)
+man_MANS = \
+       tripe.8 tripectl.1 tripe-admin.5 pkstream.1 tripe-mitm.8 \
+       @pymans@ @pygtkmans@
+PYTHONMANS = tripe-keys.8 tripe-keys.conf.5
+PYGTKMANS = tripemon.1
+EXTRA_DIST = $(man_MANS) $(PYTHONMANS) $(PYGTKMANS)
 
 ##----- That's all, folks ---------------------------------------------------
index b4d51b5..b6bcdfc 100644 (file)
@@ -1,4 +1,10 @@
 .\" -*-nroff-*-
+.\"
+.ie t \{\
+.  if \n(.g \{\
+.    fam P
+.  \}
+.\}
 .TH tripe-admin 5 "18 February 2001" "Straylight/Edgeware" "TrIPE: Trivial IP Encryption"
 .SH NAME
 tripe-admin \- administrator commands for TrIPE
@@ -134,7 +140,11 @@ indicates that a background command succeeded or failed, respectively.
 .PP
 A background command will never issue an
 .B OK
-response: it will always detach and then issue a
+or
+.B BGINFO
+response: it will always detach and then issue any
+.B BGINFO
+lines followed by
 .B BGOK
 response.
 .SS "Network addresses"
@@ -159,6 +169,43 @@ If, on input, no recognised address family token is found, the following
 words are assumed to represent an
 .B INET
 address.
+.SS "Key-value output"
+Some commands (e.g.,
+.B STATS
+and
+.BR SERVINFO )
+produce output in the form of
+.IB key = value
+pairs, one per word.  Neither the
+.I key
+nor the
+.I value
+contain spaces.
+.SS "Trace lists"
+Commands which enable or disable kinds of output (e.g.,
+.B TRACE
+and
+.BR WATCH )
+work in similar ways.  They take a single optional argument, which
+consists of a string of letters selecting message types, optionally
+interspersed with
+.RB ` + '
+to enable, or
+.RB ` \- '
+to disable, the subsequently listed types.
+.PP
+If the argument is omitted, the available message types are displayed,
+one to an
+.B INFO
+line, in a fixed-column format.  Column zero contains the key letter for
+selecting that message type; column one contains either a space or a
+.RB ` + ' 
+sign, if the message type is disabled or enabled respectively; and a
+textual description of the message type begins at column 3 and continues
+to the end of the line.
+.PP
+Lowercase key letters control individual message types.  Uppercase key
+letters control collections of message types.
 .SH "COMMAND REFERENCE"
 The commands provided are:
 .TP
@@ -255,6 +302,19 @@ Issues a
 .B USER
 notification to all interested administration clients.
 .TP
+.BI "PEERINFO " peer
+Returns information about a peer, in key-value form.  The following keys
+are returned.
+.RS
+.TP
+.B tunnel
+The tunnel driver used for this peer.
+.TP
+.B keepalive
+The keepalive interval, in seconds, or zero if no keepalives are to be
+sent.
+.RE
+.TP
 .BI "PING \fR[" options "\fR] " peer
 Send a transport-level ping to the peer.  The ping and its response are
 not encrypted or authenticated.  This command, possibly in conjunction
@@ -313,6 +373,29 @@ for example after adding a new peer key.
 .B "QUIT"
 Instructs the server to exit immediately.  A warning is sent.
 .TP
+.B "SERVINFO"
+Returns information about the server, in the form of key-value pairs.
+The following keys are used.
+.RS
+.TP
+.B implementation
+A keyword naming the implementation of the
+.BR tripe (8)
+server.  The current implementation is called
+.BR edgeware-tripe .
+.TP
+.B version
+The server's version number, as reported by
+.BR VERSION .
+.TP
+.B daemon
+Either
+.B t
+or
+.BR nil ,
+if the server has or hasn't (respectively) become a daemon.
+.RE
+.TP
 .BI "STATS " peer
 Emits a number of
 .B INFO
@@ -321,20 +404,9 @@ lines, each containing one or more statistics in the form
 The statistics-gathering is experimental and subject to change.
 .TP
 .BR "TRACE " [\fIoptions\fP]
-A trace argument consists of a string of letters (listed below)
-selecting trace outputs, optionally interspersed with
-.RB ` + '
-to enable, or
-.RB ` \- '
-to disable, the subsequently listed outputs; the initial behaviour is to
-enable listed outputs.  For example, the string
-.B ra\-st+x
-enables tracing of peer management, admin-connection handling and
-key-exchange processing, and disables tracing of symmetric keyset
-management and the system-specific tunnel driver.  If no argument is
-given, a table is returned showing the available tracing option letters
-and their meanings.  Programs should not attempt to parse this table:
-its format is not guaranteed to remain the same.
+Selects trace outputs: see
+.B "Trace lists" 
+above.  Message types provided are:
 .RS
 .PP
 Currently, the following tracing options are supported:
@@ -396,17 +468,24 @@ or
 All of the above.
 .RE
 .TP
+.B "TUNNELS"
+For each available tunnel driver, an
+.B INFO
+line is printed giving its name.
+.TP
+.B "VERSION"
+Causes the server to emit an
+.B INFO
+line stating its software version, as two words: the server name, and
+its version string.  The server name
+.B tripe
+is reserved to the Straylight/Edgeware implementation.
+.TP
 .BR "WATCH " [\fIoptions\fP]
 Enables or disables asynchronous messages
 .IR "for the current connection only" .
-This command has no effect on other connections.  A watch argument
-consists of a string of letters (listed below) selecting message types,
-optionally interspersed with
-.RB ` + '
-to enable, or
-.RB ` \- '
-to disable, the subsequently listed types, similar to
-.B trace
+See
+.B "Trace lists" 
 above.  The default watch state for the connection the server opens
 automatically on stdin/stdout is to show warnings and trace messages;
 other connections show no asynchronous messages.  (This is done in order
@@ -414,7 +493,7 @@ to guarantee that a program reading the server's stdout does not miss
 any warnings.)
 .RS
 .PP
-Currently, the following watch options are supported:
+Message types provided are:
 .TP
 .B t
 .B TRACE
@@ -432,14 +511,6 @@ messages.
 All of the above.
 .RE
 .TP
-.B "VERSION"
-Causes the server to emit an
-.B INFO
-line stating its software version, as two words: the server name, and
-its version string.  The server name
-.B tripe
-is reserved to the Straylight/Edgeware implementation.
-.TP
 .BI "WARN " tokens\fR...
 Issues a 
 .B USER
diff --git a/doc/tripe-keys.8 b/doc/tripe-keys.8
new file mode 100644 (file)
index 0000000..9055c87
--- /dev/null
@@ -0,0 +1,212 @@
+.\" -*-nroff-*-
+.\".
+.de hP
+.IP
+\h'-\w'\fB\\$1\ \fP'u'\fB\\$1\ \fP\c
+..
+.de VS
+.sp 1
+.RS
+.nf
+.ft B
+..
+.de VE
+.ft R
+.fi
+.RE
+.sp 1
+..
+.ie t \{\
+.  ds o \(bu
+.  ds ss \s8\u
+.  ds se \d\s0
+.  if \n(.g \{\
+.    fam P
+.  \}
+.\}
+.el \{\
+.  ds o o
+.  ds ss ^
+.  ds se
+.\}
+.TH tripe-keys 8 "14 September 2005" "Straylight/Edgeware" "TrIPE: Trivial IP Encryption"
+.SH "NAME"
+tripe-keys \- simple centralized key management for tripe
+.SH "SYNOPSIS"
+.B tripe-keys
+.I operation
+.IP "Operations supported:"
+.B "help"
+.br
+.BI "generate " tag
+.br
+.B "update"
+.br
+.B "rebuild"
+.br
+.B "setup"
+.br
+.B "upload"
+.SH "DESCRIPTION"
+The
+.B tripe-keys
+script implements a very simple, centralized key management system for
+.BR tripe (8).
+It assumes that there is a central authority who knows all the public
+keys for a private network.
+.SS "Overview"
+The
+.B tripe-keys
+program maintains a
+.I repository
+of public keys.  It provides a way for a master authority to publish the
+repository and for clients to obtain authentic copies of it.
+.PP
+The repository is very simple: it consists of a directory
+.B repos
+full of public-key files, each named
+.BI peer- tag .pub \fR.
+.PP
+The repository setup process creates a master signing key, stored in the
+.B master
+keyring, and a key describing the parameters to be used for generating
+key-exchange keys, stored in
+.BR repos/param .
+.PP
+The master authority has a configuration file
+.BR tripe-keys.master ,
+usually created by copying the template provided and editing it.  
+.PP
+The published repository consists of a tarball of the
+.B repos
+directory, containing the key-generation parameters and all the peers'
+public keys, and a client configuration file
+.BR tripe-keys.conf .
+The tarball is signed by the master authority's signing key.
+.PP
+The client configuration file is essentially a copy of
+.B tripe-keys.master
+with some extra bits filled in: in particular, it contains the
+fingerprint of the master signing key, so that the client can be sure
+it's checking the right key.
+.PP
+A peer starts by downloading a copy of
+.B tripe-keys.conf
+and then making sure it's authentic.  (This is one of the tricky bits.
+The other is getting public keys back to the master authority.)  This is
+enough for the peer to fetch a copy of the repository, verify the
+signature, and assemble a public keyring for the other peers in the
+network.
+.PP
+In fact, it's not
+.I quite
+that simple.  The system allows new signing keys to replace old ones, so
+in fact the publication process signs the repository archive using a
+collection of keys.  Each signing key is given a sequence number.  The
+client configuration file contains the sequence number of the master
+signing key whose fingerprint it knows.  During an update, the right
+signature is fetched and checked; if there's a new master key, then the
+.B tripe-keys.conf
+in the new repository archive will have its sequence number and
+fingerprint: the update process will replace its configuration file with
+the new version, and the peer will use the new key from then on.
+.SS "Options"
+The
+.B tripe-keys
+program accepts some standard command-line options:
+.TP
+.B "\-h, \-\-help"
+Print general help about
+.B tripe-keys
+to standard output and exit successfully.
+.TP
+.B "\-v, \-\-version"
+Print the version number of
+.B tripe-keys
+to standard output and exit successfully.
+.TP
+.B "\-u, \-\-usage"
+Print brief usage about
+.B tripe-keys
+to standard output and exit successfully.
+.SS "Subcommands"
+.TP
+.BI help\fR[ command \fR]
+With no arguments, shows help, as for the
+.B \-\-help
+option.  With an argument, shows help about that
+.IR command .
+.TP
+.B "setup"
+Constructs a new repository and makes a signing key (as for 
+.BR newmaster )
+and key-exchange parameters.  Fails if
+.B repos
+already exists.
+.TP
+.B "upload"
+Build a repository archive, sign it with the active signing keys, and
+make a
+.B tripe-keys.conf
+file.  Copy the results to the places named by
+.IR repos-file ,
+.IR sig-file ,
+and
+.I conf-file
+respectively.  (This command is currently misnamed.  It only copies
+stuff about the local filesystem.  Some day it'll really upload stuff.)
+.TP
+.BI "generate " tag
+Generate a peer key for the peer named
+.IR tag .
+The private key ends up in
+.BR keyring ;
+the public key is written to
+.BI peer- tag .pub
+in the
+.I current
+directory.
+.TP
+.B update
+Fetches a new copy of the repository archive and its signature.  It
+unpacks the archive in a temporary directory, and checks the enclosed
+master public key against the fingerprint in the configuration file.  It
+then verifies the signature on the archive using this public key.  If
+all is well, it replaces the current
+.B repos
+directory with the version in the new archive, and if necessary it
+replaces the current configuration file with the new one in the
+archive.  It then does a 
+.B rebuild
+to construct a new
+.B keyring.pub
+file.
+.TP
+.B newmaster
+Generates a new master signing key.  The old master key is not deleted.
+.TP
+.B rebuild
+Rebuilds the public keyring
+.B keyring.pub
+from the public keys in the
+.B repos
+directory.
+.TP
+.B clean
+Deletes everything which
+.B tripe-keys
+might have written to a directory.  In particular, it deletes
+.BR repos ,
+.BR tmp ,
+.BR master ,
+.BR keyring ,
+.BR keying.pub ,
+and their associated
+.B .old
+files.
+.SH "SEE ALSO"
+.BR key (1),
+.BR tripe\-keys.conf (5),
+.BR tripe (8).
+.SH "AUTHOR"
+Mark Wooding, <mdw@distorted.org.uk>
diff --git a/doc/tripe-keys.conf.5 b/doc/tripe-keys.conf.5
new file mode 100644 (file)
index 0000000..dccb855
--- /dev/null
@@ -0,0 +1,267 @@
+.\" -*-nroff-*-
+.\".
+.de hP
+.IP
+\h'-\w'\fB\\$1\ \fP'u'\fB\\$1\ \fP\c
+..
+.de VS
+.sp 1
+.RS
+.nf
+.ft B
+..
+.de VE
+.ft R
+.fi
+.RE
+.sp 1
+..
+.ie t \{\
+.  ds o \(bu
+.  ds ss \s8\u
+.  ds se \d\s0
+.  if \n(.g \{\
+.    fam P
+.  \}
+.\}
+.el \{\
+.  ds o o
+.  ds ss ^
+.  ds se
+.\}
+.TH tripe-keys.conf 5 "14 September 2005" "Straylight/Edgeware" "TrIPE: Trivial IP Encryption"
+.SH "NAME"
+tripe-keys.conf \- configuration file format for tripe-keys
+.SH "DESCRIPTION"
+The
+.B tripe-keys.master
+or
+.B tripe-keys.conf
+file is a simple line-based configuration file read by
+.BR tripe-keys (1).
+Lines may be empty (consist only of whitespace), be comments (first
+non-whitespace character is
+.RB ` # ')
+or have the form
+.IP
+.I name
+.RB [ = ]
+.I value
+.PP
+A
+.I name
+consists of alphanumeric characters and hyphens.  Values may contain
+substitutions, of the form
+.BI ${ name } \fR,
+which are replaced by the value assigned to
+.IR name .
+Many 
+.IR name s
+have significance to the
+.B tripe-keys
+program: these are described below.  Many have sensible defaults.
+.SS "The tripe-keys.master file"
+The client configuration file is built by applying substitutions to the
+.B tripe-keys.master
+file.  The following tokens are substituted:
+.TP
+.B @MASTER-SEQUENCE@
+The sequence number of the most recently-added signing key.
+.TP
+.B @HK-MASTER@
+The fingerprint of the signing key identified by
+.BR @MASTER-SEQUENCE@ .
+.SS "Master repository parameters"
+.TP
+.I base-url
+The base URL of the key repository (usually with a trailing
+.RB ` / ').
+Typically, this will be something like
+.RB http://www.distorted.org.uk/vpn/ .
+No default.
+.TP
+.I repos-base
+The basename for the repository archive.  Default is
+.BR tripe-keys.tar.gz .
+.TP
+.I sig-base
+The basename template for repository signatures.  Default is
+.BR tripe-keys.sig-<SEQ> .
+The
+.RB ` <SEQ> '
+portion, if any, is replaced by the sequence number of the key which
+made the signature.
+.TP
+.I repos-url
+The URL for the key repository tarball.  Default is the concatenation of
+.I base-url
+and
+.IR repos-base .
+.TP
+.I sig-url
+The URL template for key repository signatures.  Default is the
+concatenation of
+.I sig-url
+and
+.IR sig-base .
+.TP
+.I master-sequence
+The sequence number of the master authority's current signing key.  No
+default.  Usually set up automatically.
+.TP
+.I hk-master
+The fingerprint of the current master signing key.  No default.  Usually
+set up automatically.
+.SS "Crypto parameters"
+.TP
+.I kx
+Key-exchange algorithm to use.  Either
+.B dh 
+(integer Diffie-Hellman)
+or
+.B ec
+(elliptic curves).  The default is
+.BR dh .
+.TP
+.I kx-param
+Options to pass to
+.B "key add"
+when generating the parameters key.  Default depends on
+.I kx
+as follows.
+.TS
+center;
+| ci | ci |
+| lb | lb |.
+_
+kx     kx-param
+_
+dh     \-LS \-b2048 \-B256
+ec     \-Cnist-p256
+_
+.TE
+.TP
+.I kx-expire
+Expiry time for generated keys.  Default is
+.BR "now + 1 day" .
+.TP
+.I hash
+Hashing algorithm to use.  Default is
+.BR sha256 .
+.TP
+.I mac
+Message authentication algorithm to use.  Default is
+.IB hash -hmac/ halfhashlen \fR,
+where
+.I halfhashlen
+is half of
+.IR hash 's
+output length.
+.TP
+.I mgf
+Mask-generation algorithm to use.  Default is
+.IB hash -mgf \fR.
+This is probably a good choice.
+.TP
+.I cipher
+Symmetric encryption scheme to use.  Default is
+.BR blowfish-cbc .
+.TP
+.I sig
+Signature scheme to use.  Must be one of those recognized by
+.BR catsign (1).
+Default is
+.B dsa
+if
+.I kx
+is
+.BR dh ,
+or
+.B ecdsa
+if
+.I kx
+is
+.BR ec .
+.TP
+.I sig-genalg
+Key-generation algorithm for signing key.  Default depends on 
+.I sig
+as follows.
+.TS
+center;
+| ci | ci |
+| lb | lb |.
+_
+sig    sig-genalg
+_
+kcdsa  dh
+dsa    dsa
+rsapcs1        rsa
+rsapss rsa
+ecdsa  ec
+eckcdsa        ec
+_
+.TE
+.TP
+.I sig-param
+Signature-key generation parameters.  Default depends on
+.I sig-genalg
+as follows.
+.TS
+center;
+| ci | ci |
+| lb | lb |.
+_
+sig-genalg     sig-param
+_
+dh     \-LS \-b2048 \-B256
+dsa    \-b2048 \-B256
+rsa    \-b2048
+ec     \-Cnist-p256
+_
+.TE
+.TP
+.I sig-hash
+Hash function to use for making signatures.  Default is
+.IR hash .
+.TP
+.I sig-fresh
+Oldest time we should consider a signed archive to be fresh.  Default is
+.BR always ,
+meaning that all signatures are fresh.
+.TP
+.I sig-expire
+Expiry time for master signing key.  Default is
+.BR forever .
+.TP
+.I fingerprint-hash
+Hash function to use for key fingerprinting.  Default is
+.IR hash .
+.SS "Master maintenance parameters"
+.TP
+.I base-dir
+Local base directory for the repository files.  This probably ought to
+end in a
+.RB ` / '
+character.  No default.
+.TP
+.I repos-file
+Filename for local repository tarball.  Default is the concatenation of
+.I base-dir
+and
+.IB repos-base .
+.TP
+.I sig-file
+Tempalte for repository signatures.  Default is the concatenation of
+.I base-dir
+and
+.IR sig-base .
+.TP
+.I conf-file
+Filename for local repository configuration file.  Default is
+.IB basedir /tripe-keys.conf \fR.
+.SH "SEE ALSO"
+.BR tripe (8),
+.BR tripe\-keys (8).
+.SH "AUTHOR"
+Mark Wooding, <mdw@distorted.org.uk>
diff --git a/doc/tripemon.1 b/doc/tripemon.1
new file mode 100644 (file)
index 0000000..5518493
--- /dev/null
@@ -0,0 +1,88 @@
+.\" -*-nroff-*-
+.\".
+.de hP
+.IP
+\h'-\w'\fB\\$1\ \fP'u'\fB\\$1\ \fP\c
+..
+.de VS
+.sp 1
+.RS
+.nf
+.ft B
+..
+.de VE
+.ft R
+.fi
+.RE
+.sp 1
+..
+.ie t \{\
+.  ds o \(bu
+.  ds ss \s8\u
+.  ds se \d\s0
+.  if \n(.g \{\
+.    fam P
+.  \}
+.\}
+.el \{\
+.  ds o o
+.  ds ss ^
+.  ds se
+.\}
+.TH tripemon 1 "4 October 2005" "Straylight/Edgeware" "TrIPE: Trivial IP Encryption"
+.SH "NAME"
+tripemon \- graphical monitor for tripe
+.SH "SYNOPSIS"
+.B tripemon
+.RB [ \-d
+.IR dir ]
+.RB [ \-a
+.IR socket ]
+.SH "DESCRIPTION"
+The
+.B tripemon
+program is a fairly simple graphical monitor program for TrIPE.  It's
+not ever-so user-friendly in some ways, but it works fairly well.  A
+perusal of 
+.BR tripe-admin (5)
+would help you understand it better.  That said, describing the program
+in detail is probably less interesting than letting you explore it for
+yourself.
+.PP
+The command-line options available are:
+.TP
+.B "\-h, \-\-help"
+Writes a brief description of the command-line options available to
+standard output and exits with status 0.
+.TP
+.B "\-v, \-\-version"
+Writes tripe's version number to standard output and exits with status
+0.
+.TP
+.B "\-u, \-\-usage"
+Writes a brief usage summary to standard output and exits with status 0.
+.TP
+.BI "\-d, \-\-directory=" dir
+Make
+.I dir
+the current directory, before doing anything else.  Note that all the
+other filenames (e.g., the log output file) are relative to this
+directory.  The default directory, if this option is not specified, is
+taken from the environment variable
+.BR TRIPEDIR ;
+if that's not defined either, a default default of
+.BR /var/lib/tripe
+is used.
+.TP
+.BI "\-a, \-\-admin=" socket
+If connecting to a running server, connect to the socket named
+.IR socket ;
+if running a new server, instruct it to listen for admin
+connections on
+.IR socket .
+.SH "SEE ALSO"
+.BR tripectl (1),
+.BR tripe\-admin (5),
+.BR tripe (8).
+.SH "AUTHOR"
+Mark Wooding, <mdw@distorted.org.uk>
diff --git a/peer.c b/peer.c
index 0e2664b..5e4c4f9 100644 (file)
--- a/peer.c
+++ b/peer.c
@@ -53,6 +53,17 @@ const tunnel_ops *tunnels[] = {
 
 /*----- Main code ---------------------------------------------------------*/
 
+static void checktimers(void)
+{
+  sel_timer *t, **tt;
+
+  tt = &sel.timers;
+  while (*tt) {
+    assert((*tt)->prev == tt);
+    tt = &(*tt)->next;
+  }
+}
+
 /* --- @p_pingtype@ --- *
  *
  * Arguments:  @unsigned msg@ = message type
@@ -340,12 +351,13 @@ static void p_pingwrite(ping *p, buf *b)
 
 void p_pingdone(ping *p, int rc)
 {
-  if (!p->p) return;
   if (p->prev) p->prev->next = p->next;
   else p->p->pings = p->next;
   if (p->next) p->next->prev = p->prev;
   if (rc != PING_TIMEOUT) sel_rmtimer(&p->t);
-  p->p = 0;
+  T( trace(T_PEER, "peer: ping 0x%08lx done (rc = %d)",
+          (unsigned long)p->id, rc); )
+checktimers();
   if (rc >= 0) p->func(rc, p->arg);
 }
 
@@ -418,10 +430,12 @@ int p_pingsend(peer *p, ping *pg, unsigned type,
   pg->p = p;
   pg->func = func;
   pg->arg = arg;
+  if (p->pings) p->pings->prev = pg;
   p->pings = pg;
   gettimeofday(&tv, 0);
   tv.tv_sec += timeout;
   sel_addtimer(&sel, &pg->t, &tv, p_pingtimeout, pg);
+checktimers();
   T( trace(T_PEER, "peer: send %s 0x%08lx to %s",
           p_pingtype(type), (unsigned long)pg->id, p->spec.name); )
   return (0);
@@ -679,6 +693,15 @@ tidy_0:
 
 const char *p_name(peer *p) { return (p->spec.name); }
 
+/* --- @p_spec@ --- *
+ *
+ * Arguments:  @peer *p@ = pointer to a peer block
+ *
+ * Returns:    Pointer to the peer's specification
+ */
+
+const peerspec *p_spec(peer *p) { return (&p->spec); }
+
 /* --- @p_find@ --- *
  *
  * Arguments:  @const char *name@ = name to look up
diff --git a/tripe-keys.in b/tripe-keys.in
new file mode 100644 (file)
index 0000000..099f27e
--- /dev/null
@@ -0,0 +1,347 @@
+#! @PYTHON@
+# -*-python-*-
+
+### External dependencies
+
+import catacomb as C
+import os as OS
+import sys as SYS
+import sre as RX
+import getopt as O
+from cStringIO import StringIO
+from errno import *
+from stat import *
+
+### Useful regular expressions
+
+r_comment = RX.compile(r'^\s*(#|$)')
+r_keyval = RX.compile(r'^\s*([-\w]+)(?:\s+(?!=)|\s*=\s*)(|\S|\S.*\S)\s*$')
+r_dollarsubst = RX.compile(r'\$\{([-\w]+)\}')
+r_atsubst = RX.compile(r'@([-\w]+)@')
+r_nonalpha = RX.compile(r'\W')
+
+### Utility functions
+
+class SubprocessError (Exception): pass
+class VerifyError (Exception): pass
+
+quis = OS.path.basename(SYS.argv[0])
+PACKAGE = "@PACKAGE@"
+VERSION = "@VERSION@"
+
+def moan(msg):
+  SYS.stderr.write('%s: %s\n' % (quis, msg))
+
+def die(msg, rc = 1):
+  moan(msg)
+  SYS.exit(rc)
+
+def subst(s, rx, map):
+  out = StringIO()
+  i = 0
+  for m in rx.finditer(s):
+    out.write(s[i:m.start()] + map[m.group(1)])
+    i = m.end()
+  out.write(s[i:])
+  return out.getvalue()
+
+def rmtree(path):
+  try:
+    st = OS.stat(path)
+  except OSError, err:
+    if err.errno == ENOENT:
+      return
+    raise
+  if not S_ISDIR(st.st_mode):
+    OS.unlink(path)
+  else:
+    cwd = OS.getcwd()
+    try:
+      OS.chdir(path)
+      for i in OS.listdir('.'):
+        rmtree(i)
+    finally:
+      OS.chdir(cwd)
+    OS.rmdir(path)
+
+def zap(file):
+  try:
+    OS.unlink(file)
+  except OSError, err:
+    if err.errno == ENOENT: return
+    raise
+
+def run(args):
+  args = map(conf_subst, args.split())
+  nargs = []
+  for a in args:
+    if len(a) > 0 and a[0] != '!':
+      nargs += [a]
+    else:
+      nargs += a[1:].split()
+  args = nargs
+  print '+ %s' % ' '.join(args)
+  rc = OS.spawnvp(OS.P_WAIT, args[0], args)
+  if rc != 0:
+    raise SubprocessError, rc
+
+def hexhyphens(bytes):
+  out = StringIO()
+  for i in xrange(0, len(bytes)):
+    if i > 0 and i % 4 == 0: out.write('-')
+    out.write('%02x' % ord(bytes[i]))
+  return out.getvalue()
+
+def fingerprint(kf, ktag):
+  h = C.gchashes[conf['fingerprint-hash']]()
+  k = C.KeyFile(kf)[ktag].fingerprint(h, '-secret')
+  return h.done()
+
+### Read configuration
+
+class ConfigFileError (Exception): pass
+conf = {}
+
+def conf_subst(s): return subst(s, r_dollarsubst, conf)
+
+## Read the file
+def conf_read(f):
+  lno = 0
+  for line in file(f):
+    lno += 1
+    if r_comment.match(line): continue
+    if line[-1] == '\n': line = line[:-1]
+    match = r_keyval.match(line)
+    if not match:
+      raise ConfigFileError, "%s:%d: bad line `%s'" % (f, lno, line)
+    k, v = match.groups()
+    conf[k] = conf_subst(v)
+
+## Sift the wreckage
+def conf_defaults():
+  for k, v in [('sig-url', '${base-url}tripe-keys.sig'),
+               ('repos-url', '${base-url}tripe-keys.tar.gz'),
+               ('sig-file', '${base-dir}tripe-keys.sig'),
+               ('repos-file', '${base-dir}tripe-keys.tar.gz'),
+               ('conf-file', '${base-dir}tripe-keys.conf'),
+               ('kx', 'dh'),
+               ('kx-param', lambda: {'dh': '-LS -b2048 -B256',
+                                     'ec': '-Cnist-p256'}[conf['kx']]),
+               ('kx-expire', 'now + 1 year'),
+               ('cipher', 'blowfish-cbc'),
+               ('hash', 'sha256'),
+               ('mgf', '${hash}-mgf'),
+               ('mac', lambda: '%s-hmac/%d' %
+                         (conf['hash'],
+                          C.gchashes[conf['hash']].hashsz * 4)),
+               ('sig', lambda: {'dh': 'dsa', 'ec': 'ecdsa'}[conf['kx']]),
+               ('sig-fresh', 'always'),
+               ('sig-genalg', lambda: {'kcdsa': 'dh',
+                                       'dsa': 'dsa',
+                                       'rsapkcs1': 'rsa',
+                                       'rsapss': 'rsa',
+                                       'ecdsa': 'ec',
+                                       'eckcdsa': 'ec'}[conf['sig']]),
+               ('sig-param', lambda: {'dh': '-LS -b2048 -B256',
+                                      'dsa': '-b2048 -B256',
+                                      'ec': '-Cnist-p256',
+                                      'rsa': '-b2048'}[conf['sig-genalg']]),
+               ('sig-hash', '${hash}'),
+               ('sig-expire', 'forever'),
+               ('fingerprint-hash', '${hash}')]:
+    try:
+      if k in conf: continue
+      if type(v) == str:
+        conf[k] = conf_subst(v)
+      else:
+        conf[k] = v()
+    except KeyError, exc:
+      if len(exc.args) == 0: raise
+      conf[k] = '<missing-var %s>' % exc.args[0]
+
+### Commands
+
+def version(fp = SYS.stdout):
+  fp.write('%s, %s version %s\n' % (quis, PACKAGE, VERSION))
+
+def usage(fp):
+  fp.write('Usage: %s SUBCOMMAND [ARGS...]\n' % quis)
+
+def cmd_help(args):
+  if len(args) == 0:
+    version(SYS.stdout)
+    print
+    usage(SYS.stdout)
+    print """
+Key management utility for TrIPE.
+
+Options supported:
+
+-h, --help             Show this help message.
+-v, --version          Show the version number.
+-u, --usage            Show pointlessly short usage string.
+
+Subcommands available:
+"""
+    args = commands.keys()
+    args.sort()
+  for c in args:
+    func, min, max, help = commands[c]
+    print '%s %s' % (c, help)
+
+def cmd_setup(args):
+  OS.mkdir('repos')
+
+  ## Generate the master key
+  run('''key -kmaster add
+    -a${sig-genalg} !${sig-param}
+    -e${sig-expire} -l -ttripe-keys-master ccsig
+    sig=${sig} hash=${sig-hash}''')
+  run('key -kmaster extract -f-secret repos/master.pub tripe-keys-master')
+
+  ## Generate the parameters key
+  run('''key -krepos/param add
+    -a${kx}-param !${kx-param}
+    -eforever -tparam tripe-${kx}-param
+    cipher=${cipher} hash=${hash} mac=${mac} mgf=${mgf}''')
+
+  ## Get fingerprints
+  print 'Setup OK: master key = %s' % \
+        hexhyphens(fingerprint('repos/master.pub', 'tripe-keys-master'))
+
+def cmd_upload(args):
+
+  ## Sanitize the repository directory
+  umask = OS.umask(0); OS.umask(umask)
+  mode = 0666 & ~umask
+  for f in OS.listdir('repos'):
+    ff = OS.path.join('repos', f)
+    if f.endswith('.old'):
+      OS.unlink(ff)
+      continue
+    OS.chmod(OS.path.join('repos', f), mode)
+
+  ## Build the configuration file
+  v = {'HK-MASTER': hexhyphens(fingerprint('repos/master.pub',
+                                           'tripe-keys-master'))}
+  fin = file('tripe-keys.master')
+  fout = file(conf_subst('${conf-file}.new'), 'w')
+  for line in fin:
+    fout.write(subst(line, r_atsubst, v))
+  fin.close(); fout.close()
+
+  ## Make and sign the repository archive
+  run('tar chozf ${repos-file}.new repos')
+  run('''catsign -kmaster sign -abdC -ktripe-keys-master
+    -o${sig-file}.new ${repos-file}.new''')
+
+  ## Commit the changes
+  for i in ['conf-file', 'repos-file', 'sig-file']:
+    base = conf[i]
+    new = '%s.new' % base
+    OS.rename(new, base)
+
+def cmd_update(args):
+  cwd = OS.getcwd()
+  rmtree('tmp')
+  try:
+
+    ## Fetch a new distribution
+    OS.mkdir('tmp')
+    OS.chdir('tmp')
+    run('wget -q -O tripe-keys.tar.gz ${repos-url}')
+    run('wget -q -O tripe-keys.sig ${sig-url}')
+    run('tar xfz tripe-keys.tar.gz')
+
+    ## Verify the signature
+    want = C.bytes(r_nonalpha.sub('', conf['hk-master']))
+    got = fingerprint('repos/master.pub', 'tripe-keys-master')
+    if want != got: raise VerifyError
+    run('''catsign -krepos/master.pub verify -avC -ktripe-keys-master
+      -t${sig-fresh} tripe-keys.sig tripe-keys.tar.gz''')
+
+    ## OK: update our copy
+    OS.chdir(cwd)
+    if OS.path.exists('repos'): OS.rename('repos', 'repos.old')
+    OS.rename('tmp/repos', 'repos')
+    rmtree('repos.old')
+
+  finally:
+    OS.chdir(cwd)
+    rmtree('tmp')
+  cmd_rebuild(args)
+
+def cmd_rebuild(args):
+  zap('keyring.pub')
+  for i in OS.listdir('repos'):
+    if i.startswith('peer-') and i.endswith('.pub'):
+      run('key -kkeyring.pub merge %s' % OS.path.join('repos', i))
+
+def cmd_generate(args):
+  tag, = args
+  keyring_pub = 'peer-%s.pub' % tag
+  zap('keyring'); zap(keyring_pub)
+  run('key -kkeyring merge repos/param')
+  run('key -kkeyring add -a${kx} -pparam -e${kx-expire} -t%s tripe-${kx}' %
+      (tag,))
+  run('key -kkeyring extract -f-secret %s %s' % (keyring_pub, tag))
+  print 'Generated %s key = %s' % \
+        (tag,
+         hexhyphens(fingerprint('repos/master.pub', 'tripe-keys-master')))
+  
+
+def cmd_clean(args):
+  rmtree('repos')
+  rmtree('tmp')
+  for i in 'master', 'keyring.pub':
+    zap(i)
+    zap('%s.old' % i)
+
+### Main driver
+
+class UsageError (Exception): pass
+  
+commands = {'help': (cmd_help, 0, 1, ''),
+            'setup': (cmd_setup, 0, 0, ''),
+            'upload': (cmd_upload, 0, 0, ''),
+            'update': (cmd_update, 0, 0, ''),
+            'clean': (cmd_clean, 0, 0, ''),
+            'generate': (cmd_generate, 1, 1, 'TAG'),
+            'rebuild': (cmd_rebuild, 0, 0, '')}
+
+def init():
+  for f in ['tripe-keys.master', 'tripe-keys.conf']:
+    if OS.path.exists(f):
+      conf_read(f)
+      break
+  conf_defaults()
+def main(argv):
+  try:
+    opts, args = O.getopt(argv[1:], 'hvu',
+                          ['help', 'version', 'usage'])
+  except O.GetoptError, exc:
+    moan(exc)
+    usage(SYS.stderr)
+    SYS.exit(1)
+  for o, v in opts:
+    if o in ('-h', '--help'):
+      cmd_help([])
+      SYS.exit(0)
+    elif o in ('-v', '--version'):
+      version(SYS.stdout)
+      SYS.exit(0)
+    elif o in ('-u', '--usage'):
+      usage(SYS.stdout)
+      SYS.exit(0)
+  if len(argv) < 2:
+    cmd_help([])
+  else:
+    c = argv[1]
+    func, min, max, help = commands[c]
+    args = argv[2:]
+    if len(args) < min or (max > 0 and len(args) > max):
+      raise UsageError, (c, help)
+    func(args)
+
+init()
+main(SYS.argv)
diff --git a/tripe-keys.master b/tripe-keys.master
new file mode 100644 (file)
index 0000000..5e6e9d1
--- /dev/null
@@ -0,0 +1,46 @@
+# tripe-keys configuration file
+#
+# see tripe-keys.conf(5) for full details
+
+### File locations (required)
+
+# The base URL for the repository files.  Include the trailing slash if
+# necessary.
+# base-url = http://some.server.somewhere/blah/
+
+# The local directory name for the repository files.  Again, include the
+# trailing slash if necessary.
+# base-dir = /some/directory/blah/
+
+### Crypto parameters
+
+# The key-exchange type.  May be `dh' or `ec'.
+# kx = dh
+
+# Key-generation parameters for key exchange group.
+# kx-param = -LS -b2048 -B256
+
+# Expiry time for peer key-exchange keys.
+# kx-expire = now + 1 day
+
+# Symmetric encryption scheme to use.
+# cipher = blowfish-cbc
+
+# Hash function to use.  (We derive the MGF and MAC from this.)
+# hash = sha256
+
+# Signature scheme to use for signing/verifying repository archives.
+# sig = dsa
+
+# How recently an archive must have been signed to be valid.
+# sig-fresh = always
+
+# When the signing key expires.  We're not good at rolling these over.
+# sig-expire = forever
+
+### Master key hash
+
+# Since the master public key is contained within the repository, we must
+# check its integrity: therefore we record its fingerprint here.  This is
+# filled in automatically by `tripe-keys upload'.  Leave it as it is.
+hk-master = @HK-MASTER@
diff --git a/tripe.h b/tripe.h
index a358fb9..47719fe 100644 (file)
--- a/tripe.h
+++ b/tripe.h
@@ -372,6 +372,7 @@ typedef struct admin_pingop {
 typedef struct admin {
   struct admin *next, *prev;           /* Links to next and previous */
   unsigned f;                          /* Various useful flags */
+  unsigned ref;                                /* Reference counter */
 #ifndef NTRACE
   unsigned seq;                                /* Sequence number for tracing */
 #endif
@@ -383,13 +384,12 @@ typedef struct admin {
 } admin;
 
 #define AF_DEAD 1u                     /* Destroy this admin block */
-#define AF_LOCK 2u                     /* Don't destroy it yet */
+#define AF_CLOSE 2u                    /* Client closed connection */
 #define AF_NOTE 4u                     /* Catch notifications */
 #define AF_WARN 8u                     /* Catch warning messages */
 #ifndef NTRACE
   #define AF_TRACE 16u                 /* Catch tracing */
 #endif
-#define AF_CLOSE 32u                   /* Client closed connection */
 
 #ifndef NTRACE
 #  define AF_ALLMSGS (AF_NOTE | AF_TRACE | AF_WARN)
@@ -918,10 +918,21 @@ extern peer *p_create(peerspec */*spec*/);
  * Arguments:  @peer *p@ = pointer to a peer block
  *
  * Returns:    A pointer to the peer's name.
+ *
+ * Use:                Equivalent to @p_spec(p)->name@.
  */
 
 extern const char *p_name(peer */*p*/);
 
+/* --- @p_spec@ --- *
+ *
+ * Arguments:  @peer *p@ = pointer to a peer block
+ *
+ * Returns:    Pointer to the peer's specification
+ */
+
+extern const peerspec *p_spec(peer */*p*/);
+
 /* --- @p_find@ --- *
  *
  * Arguments:  @const char *name@ = name to look up
diff --git a/tripemon.in b/tripemon.in
new file mode 100644 (file)
index 0000000..7ab6888
--- /dev/null
@@ -0,0 +1,1296 @@
+#! @PYTHON@
+# -*-python-*-
+
+#----- Dependencies ---------------------------------------------------------
+
+import socket as S
+from sys import argv, exit, stdin, stdout, stderr
+import os as OS
+from os import environ
+import sets as SET
+import getopt as O
+import time as T
+import sre as RX
+from cStringIO import StringIO
+
+import pygtk
+pygtk.require('2.0')
+import gtk as G
+import gobject as GO
+import gtk.gdk as GDK
+
+#----- Configuration --------------------------------------------------------
+
+tripedir = "@configdir@"
+socketdir = "@socketdir@"
+PACKAGE = "@PACKAGE@"
+VERSION = "@VERSION@"
+
+debug = False
+
+#----- Utility functions ----------------------------------------------------
+
+## Program name, shorn of extraneous stuff.
+quis = OS.path.basename(argv[0])
+
+def moan(msg):
+  """Report a message to standard error."""
+  stderr.write('%s: %s\n' % (quis, msg))
+
+def die(msg, rc = 1):
+  """Report a message to standard error and exit."""
+  moan(msg)
+  exit(rc)
+
+rx_space = RX.compile(r'\s+')
+rx_ordinary = RX.compile(r'[^\\\'\"\s]+')
+rx_weird = RX.compile(r'([\\\'])')
+rx_time = RX.compile(r'^(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)$')
+rx_num = RX.compile(r'^[-+]?\d+$')
+
+c_red = GDK.color_parse('red')
+
+def getword(s):
+  """Pull a word from the front of S, handling quoting according to the
+  tripe-admin(5) rules.  Returns the word and the rest of S, or (None, None)
+  if there are no more words left."""
+  i = 0
+  m = rx_space.match(s, i)
+  if m: i = m.end()
+  r = ''
+  q = None
+  if i >= len(s):
+    return None, None
+  while i < len(s) and (q or not s[i].isspace()):
+    m = rx_ordinary.match(s, i)
+    if m:
+      r += m.group()
+      i = m.end()
+    elif s[i] == '\\':
+      r += s[i + 1]
+      i += 2
+    elif s[i] == q:
+      q = None
+      i += 1
+    elif not q and s[i] == '`' or s[i] == "'":
+      q = "'"
+      i += 1
+    elif not q and s[i] == '"':
+      q = '"'
+      i += 1
+    else:
+      r += s[i]
+      i += 1
+  if q:
+    raise SyntaxError, 'missing close quote'
+  m = rx_space.match(s, i)
+  if m: i = m.end()
+  return r, s[i:]
+
+def quotify(s):
+  """Quote S according to the tripe-admin(5) rules."""
+  m = rx_ordinary.match(s)
+  if m and m.end() == len(s):
+    return s
+  else:
+    return "'" + rx_weird.sub(r'\\\1', s) + "'"
+
+#----- Random bits of infrastructure ----------------------------------------
+
+class struct (object):
+  """Simple object which stores attributes and has a sensible construction
+  syntax."""
+  def __init__(me, **kw):
+    me.__dict__.update(kw)
+
+class peerinfo (struct): pass
+class pingstate (struct): pass
+
+def invoker(func):
+  """Return a function which throws away its arguments and calls FUNC.  (If
+  for loops worked by binding rather than assignment then we wouldn't need
+  this kludge."""
+  return lambda *hunoz, **hukairz: func()
+
+class HookList (object):
+  """I maintain a list of functions, and provide the ability to call them
+  when something interesting happens.  The functions are called in the order
+  they were added to the list, with all the arguments.  If a function returns
+  a non-None result, no further functions are called."""
+  def __init__(me):
+    me.list = []
+  def add(me, func, obj):
+    me.list.append((obj, func))
+  def prune(me, obj):
+    new = []
+    for o, f in me.list:
+      if o is not obj:
+        new.append((o, f))
+    me.list = new
+  def run(me, *args, **kw):
+    for o, hook in me.list:
+      rc = hook(*args, **kw)
+      if rc is not None: return rc
+    return None
+
+class HookClient (object):
+  def __init__(me):
+    me.hooks = SET.Set()
+  def hook(me, hk, func):
+    hk.add(func, me)
+    me.hooks.add(hk)
+  def unhook(me, hk):
+    hk.prune(me)
+    me.hooks.discard(hk)
+  def unhookall(me):
+    for hk in me.hooks:
+      hk.prune(me)
+    me.hooks.clear()
+  ##def __del__(me):
+  ##  print '%s dying' % me
+
+#----- Connections and commands ---------------------------------------------
+
+class ConnException (Exception):
+  """Some sort of problem occurred while communicating with the tripe
+  server."""
+  pass
+
+class Error (ConnException):
+  """A command caused the server to issue a FAIL message."""
+  pass
+
+class ConnectionFailed (ConnException):
+  """The connection failed while communicating with the server."""
+
+jobid_seq = 0
+def jobid():
+  """Return a job tag.  Used for background commands."""
+  global jobid_seq
+  jobid_seq += 1
+  return 'bg-%d' % jobid_seq
+
+class BackgroundCommand (HookClient):
+  def __init__(me, conn, cmd):
+    HookClient.__init__(me)
+    me.conn = conn
+    me.tag = None
+    me.cmd = cmd
+    me.donehook = HookList()
+    me.losthook = HookList()
+    me.info = []
+    me.submit()
+    me.hook(me.conn.disconnecthook, me.lost)
+  def submit(me):
+    me.conn.bgcommand(me.cmd, me)
+  def lost(me):
+    me.losthook.run()
+    me.unhookall()
+  def fail(me, msg):
+    me.conn.error("Unexpected error from server command `%s': %s" %
+                  (me.cmd % msg))
+    me.unhookall()
+  def ok(me):
+    me.donehook.run(me.info)
+    me.unhookall()
+
+class SimpleBackgroundCommand (BackgroundCommand):
+  def submit(me):
+    try:
+      BackgroundCommand.submit(me)
+    except ConnectionFailed, err:
+      me.conn.error('Unexpected error communicating with server: %s' % msg)
+      raise
+
+class Connection (HookClient):
+
+  """I represent a connection to the TrIPE server.  I provide facilities for
+  sending commands and receiving replies.  The connection is notional: the
+  underlying socket connection can come and go under our feet.
+
+  Useful attributes:
+  connectedp: whether the connection is active
+  connecthook: called when we have connected
+  disconnecthook: called if we have disconnected
+  notehook: called with asynchronous notifications
+  errorhook: called if there was a command error"""
+
+  def __init__(me, sockname):
+    """Make a new connection to the server listening to SOCKNAME.  In fact,
+    we're initially disconnected, to allow the caller to get his life in
+    order before opening the floodgates."""
+    HookClient.__init__(me)
+    me.sockname = sockname
+    me.sock = None
+    me.connectedp = False
+    me.connecthook = HookList()
+    me.disconnecthook = HookList()
+    me.errorhook = HookList()
+    me.inbuf = ''
+    me.info = []
+    me.waitingp = False
+    me.bgcmd = None
+    me.bgmap = {}
+  def connect(me):
+    "Connect to the server.  Runs connecthook if it works."""
+    if me.sock: return
+    sock = S.socket(S.AF_UNIX, S.SOCK_STREAM)
+    try:
+      sock.connect(me.sockname)
+    except S.error, err:
+      me.error('error opening connection: %s' % err[1])
+      me.disconnecthook.run()
+      return
+    sock.setblocking(0)
+    me.socketwatch = GO.io_add_watch(sock, GO.IO_IN, me.ready)
+    me.sock = sock
+    me.connectedp = True
+    me.connecthook.run()
+  def disconnect(me):
+    "Disconnects from the server.  Runs disconnecthook."
+    if not me.sock: return
+    GO.source_remove(me.socketwatch)
+    me.sock.close()
+    me.sock = None
+    me.connectedp = False
+    me.disconnecthook.run()
+  def error(me, msg):
+    """Reports an error on the connection."""
+    me.errorhook.run(msg)
+
+  def bgcommand(me, cmd, bg):
+    """Sends a background command and feeds it properly."""
+    try:
+      me.bgcmd = bg
+      err = me.docommand(cmd)
+      if err:
+        bg.fail(err)
+    finally:
+      me.bgcmd = None
+  def command(me, cmd):
+    """Sends a command to the server.  Returns a list of INFO responses.  Do
+    not use this for backgrounded commands: create a BackgroundCommand
+    instead.  Raises apprpopriate exceptions on error, but doesn't send
+    report them to the errorhook."""
+    err = me.docommand(cmd)
+    if err:
+      raise Error, err
+    return me.info
+  def docommand(me, cmd):
+    if not me.sock:
+      raise ConnException, 'not connected'
+    if debug: print ">>> %s" % cmd
+    me.sock.sendall(cmd + '\n')
+    me.waitingp = True
+    me.info = []
+    try:
+      me.sock.setblocking(1)
+      while True:
+        rc, err = me.collect()
+        if rc: break
+    finally:
+      me.waitingp = False
+      me.sock.setblocking(0)
+      if len(me.inbuf) > 0:
+        GO.idle_add(lambda: me.flushbuf() and False)
+    return err
+  def simplecmd(me, cmd):
+    """Like command(), but reports errors via the errorhook as well as
+    raising exceptions."""
+    try:
+      i = me.command(cmd)
+    except Error, msg:
+      me.error("Unexpected error from server command `%s': %s" % (cmd, msg))
+      raise
+    except ConnectionFailed, msg:
+      me.error("Unexpected error communicating with server: %s" % msg);
+      raise
+    return i
+  def ready(me, sock, condition):
+    try:
+      me.collect()
+    except ConnException, msg:
+      me.error("Error watching server connection: %s" % msg)
+      if me.sock:
+        me.disconnect()
+        me.connect()
+    return True
+  def collect(me):
+    data = me.sock.recv(16384)
+    if data == '':
+      me.disconnect()
+      raise ConnectionFailed, 'server disconnected'
+    me.inbuf += data
+    return me.flushbuf()
+  def flushbuf(me):
+    while True:
+      nl = me.inbuf.find('\n')
+      if nl < 0: break
+      line = me.inbuf[:nl]
+      if debug: print "<<< %s" % line
+      me.inbuf = me.inbuf[nl + 1:]
+      tag, line = getword(line)
+      rc, err = me.parseline(tag, line)
+      if rc: return rc, err
+    return False, None
+  def parseline(me, code, line):
+    if code == 'BGDETACH':
+      if not me.bgcmd:
+        raise ConnectionFailed, 'unexpected detach'
+      me.bgcmd.tag = line
+      me.bgmap[line] = me.bgcmd
+      me.waitingp = False
+      me.bgcmd = None
+      return True, None
+    elif code == 'BGINFO':
+      tag, line = getword(line)
+      me.bgmap[tag].info.append(line)
+      return False, None
+    elif code == 'BGFAIL':
+      tag, line = getword(line)
+      me.bgmap[tag].fail(line)
+      del me.bgmap[tag]
+      return False, None
+    elif code == 'BGOK':
+      tag, line = getword(line)
+      me.bgmap[tag].ok()
+      del me.bgmap[tag]
+      return False, None
+    elif code == 'INFO':
+      if not me.waitingp or me.bgcmd:
+        raise ConnectionFailed, 'unexpected INFO response'
+      me.info.append(line)
+      return False, None
+    elif code == 'OK':
+      if not me.waitingp or me.bgcmd:
+        raise ConnectionFailed, 'unexpected OK response'
+      return True, None
+    elif code == 'FAIL':
+      if not me.waitingp:
+        raise ConnectionFailed, 'unexpected FAIL response'
+      return True, line
+    else:
+      raise ConnectionFailed, 'unknown response code `%s' % code
+
+class Monitor (Connection):
+  """I monitor a TrIPE server, noticing when it changes state and keeping
+  track of its peers.  I also provide facilities for sending the server
+  commands and collecting the answers.
+
+  Useful attributes:
+  addpeerhook: called with a new Peer when the server adds one
+  delpeerhook: called with a Peer when the server kills one
+  tracehook: called with a trace message
+  warnhook: called with a warning message
+  peers: mapping from names to Peer objects"""
+  def __init__(me, sockname):
+    """Initializes the monitor."""
+    Connection.__init__(me, sockname)
+    me.addpeerhook = HookList()
+    me.delpeerhook = HookList()
+    me.tracehook = HookList()
+    me.warnhook = HookList()
+    me.notehook = HookList()
+    me.hook(me.connecthook, me.connected)
+    me.delay = []
+    me.peers = {}
+  def addpeer(me, peer):
+    if peer not in me.peers:
+      p = Peer(me, peer)
+      me.peers[peer] = p
+      me.addpeerhook.run(p)
+  def delpeer(me, peer):
+    if peer in me.peers:
+      p = me.peers[peer]
+      me.delpeerhook.run(p)
+      p.dead()
+      del me.peers[peer]
+  def updatelist(me, peers):
+    newmap = {}
+    for p in peers:
+      newmap[p] = True
+      if p not in me.peers:
+        me.addpeer(p)
+    oldpeers = me.peers.copy()
+    for p in oldpeers:
+      if p not in newmap:
+        me.delpeer(p)
+  def connected(me):
+    try:
+      me.simplecmd('WATCH -A+wnt')
+      me.updatelist([s.strip() for s in me.simplecmd('LIST')])
+    except ConnException:
+      me.disconnect()
+      return
+  def parseline(me, code, line):
+    ## Delay async messages until the current command is done.  Otherwise the
+    ## handler for the async message might send another command before this
+    ## one's complete, and the whole edifice turns to jelly.
+    ##
+    ## No, this isn't the server's fault.  If we rely on the server to delay
+    ## notifications then there's a race between when we send a command and
+    ## when the server gets it.
+    if me.waitingp and code in ('TRACE', 'WARN', 'NOTE'):
+      if len(me.delay) == 0: GO.idle_add(me.flushdelay)
+      me.delay.append((code, line))
+    elif code == 'TRACE':
+      me.tracehook.run(line)
+    elif code == 'WARN':
+      me.warnhook.run(line)
+    elif code == 'NOTE':
+      note, line = getword(line)
+      me.notehook.run(note, line)
+      if note == 'ADD':
+        me.addpeer(getword(line)[0])
+      elif note == 'KILL':
+        me.delpeer(line)
+      else:
+        ## Well, I asked for it.
+        pass
+    else:
+      return Connection.parseline(me, code, line)
+    return False, None
+  def flushdelay(me):
+    delay = me.delay
+    me.delay = []
+    for tag, line in delay:
+      me.parseline(tag, line)
+    return False
+
+def parseinfo(info):
+  """Parse key=value output into a dictionary."""
+  d = {}
+  for i in info:
+    for w in i.split(' '):
+      q = w.index('=')
+      d[w[:q]] = w[q + 1:]
+  return d
+
+class Peer (object):
+  """I represent a TrIPE peer.  Useful attributes are:
+
+  name: peer's name
+  addr: human-friendly representation of the peer's address
+  ifname: interface associated with the peer
+  alivep: true if the peer hasn't been killed
+  deadhook: called with no arguments when the peer is killed"""
+  def __init__(me, monitor, name):
+    me.mon = monitor
+    me.name = name
+    addr = me.mon.simplecmd('ADDR %s' % name)[0].split(' ')
+    if addr[0] == 'INET':
+      ipaddr, port = addr[1:]
+      try:
+        name = S.gethostbyaddr(ipaddr)[0]
+        me.addr = 'INET %s:%s [%s]' % (name, port, ipaddr)
+      except S.herror:
+        me.addr = 'INET %s:%s' % (ipaddr, port)
+    else:
+      me.addr = ' '.join(addr)
+    me.ifname = me.mon.simplecmd('IFNAME %s' % me.name)[0]
+    me.__dict__.update(parseinfo(me.mon.simplecmd('PEERINFO %s' % me.name)))
+    me.deadhook = HookList()
+    me.alivep = True
+  def dead(me):
+    me.alivep = False
+    me.deadhook.run()
+
+#----- Window management cruft ----------------------------------------------
+
+class MyWindowMixin (G.Window, HookClient):
+  """Mixin for windows which call a closehook when they're destroyed."""
+  def mywininit(me):
+    me.closehook = HookList()
+    HookClient.__init__(me)
+    me.connect('destroy', invoker(me.close))
+  def close(me):
+    me.closehook.run()
+    me.destroy()
+    me.unhookall()
+class MyWindow (MyWindowMixin):
+  """A window which calls a closehook when it's destroyed."""
+  def __init__(me, kind = G.WINDOW_TOPLEVEL):
+    G.Window.__init__(me, kind)
+    me.mywininit()
+class MyDialog (G.Dialog, MyWindowMixin, HookClient):
+  """A dialogue box with a closehook and sensible button binding."""
+  def __init__(me, title = None, flags = 0, buttons = []):
+    """The buttons are a list of (STOCKID, THUNK) pairs: call the appropriate
+    THUNK when the button is pressed.  The others are just like GTK's Dialog
+    class."""
+    i = 0
+    br = []
+    me.rmap = []
+    for b, f in buttons:
+      br.append(b)
+      br.append(i)
+      me.rmap.append(f)
+      i += 1
+    G.Dialog.__init__(me, title, None, flags, tuple(br))
+    HookClient.__init__(me)
+    me.mywininit()
+    me.set_default_response(i - 1)
+    me.connect('response', me.respond)
+  def respond(me, hunoz, rid, *hukairz):
+    if rid >= 0: me.rmap[rid]()
+
+class WindowSlot (HookClient):
+  """A place to store a window.  If the window is destroyed, remember this;
+  when we come to open the window, raise it if it already exists; otherwise
+  make a new one."""
+  def __init__(me, createfunc):
+    """Constructor: CREATEFUNC must return a new Window which supports the
+    closehook protocol."""
+    HookClient.__init__(me)
+    me.createfunc = createfunc
+    me.window = None
+  def open(me):
+    """Opens the window, creating it if necessary."""
+    if me.window:
+      me.window.window.raise_()
+    else:
+      me.window = me.createfunc()
+      me.hook(me.window.closehook, me.closed)
+  def closed(me):
+    me.unhook(me.window.closehook)
+    me.window = None
+
+class ValidationError (Exception):
+  """Raised by ValidatingEntry.get_text() if the text isn't valid."""
+  pass
+class ValidatingEntry (G.Entry):
+  """Like an Entry, but makes the text go red if the contents are invalid.
+  If get_text is called, and the text is invalid, ValidationError is
+  raised."""
+  def __init__(me, valid, text = '', size = -1, *arg, **kw):
+    """Make an Entry.  VALID is a regular expression or a predicate on
+    strings.  TEXT is the default text to insert.  SIZE is the size of the
+    box to set, in characters (ish).  Other arguments are passed to Entry."""
+    G.Entry.__init__(me, *arg, **kw)
+    me.connect("changed", me.check)
+    if callable(valid):
+      me.validate = valid
+    else:
+      me.validate = RX.compile(valid).match
+    me.ensure_style()
+    me.c_ok = me.get_style().text[G.STATE_NORMAL]
+    me.c_bad = c_red
+    if size != -1: me.set_width_chars(size)
+    me.set_activates_default(True)
+    me.set_text(text)
+    me.check()
+  def check(me, *hunoz):
+    if me.validate(G.Entry.get_text(me)):
+      me.validp = True
+      me.modify_text(G.STATE_NORMAL, me.c_ok)
+    else:
+      me.validp = False
+      me.modify_text(G.STATE_NORMAL, me.c_bad)
+  def get_text(me):
+    if not me.validp:
+      raise ValidationError
+    return G.Entry.get_text(me)
+
+def numericvalidate(min = None, max = None):
+  """Validation function for numbers.  Entry must consist of an optional sign
+  followed by digits, and the resulting integer must be within the given
+  bounds."""
+  return lambda x: (rx_num.match(x) and
+                    (min is None or long(x) >= min) and
+                    (max is None or long(x) <= max))
+
+#----- Various minor dialog boxen -------------------------------------------
+
+GPL = """This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software Foundation,
+Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA."""
+
+class AboutBox (G.AboutDialog, MyWindowMixin):
+  """The program `About' box."""
+  def __init__(me):
+    G.AboutDialog.__init__(me)
+    me.mywininit()
+    me.set_name('TrIPEmon')
+    me.set_version(VERSION)
+    me.set_license(GPL)
+    me.set_authors(['Mark Wooding'])
+    me.connect('unmap', invoker(me.close))
+    me.show()
+aboutbox = WindowSlot(AboutBox)
+
+def moanbox(msg):
+  """Report an error message in a window."""
+  d = G.Dialog('Error from %s' % quis,
+               flags = G.DIALOG_MODAL,
+               buttons = ((G.STOCK_OK, G.RESPONSE_NONE)))
+  label = G.Label(msg)
+  label.set_padding(20, 20)
+  d.vbox.pack_start(label)
+  label.show()
+  d.run()
+  d.destroy()
+
+#----- Logging windows ------------------------------------------------------
+
+class LogModel (G.ListStore):
+  """A simple list of log messages."""
+  def __init__(me, columns):
+    """Call with a list of column names.  All must be strings.  We add a time
+    column to the left."""
+    me.cols = ('Time',) + columns
+    G.ListStore.__init__(me, *((GO.TYPE_STRING,) * len(me.cols)))
+  def add(me, *entries):
+    """Adds a new log message, with a timestamp."""
+    now = T.strftime('%Y-%m-%d %H:%M:%S')
+    me.append((now,) + entries)
+
+class TraceLogModel (LogModel):
+  """Log model for trace messages."""
+  def __init__(me):
+    LogModel.__init__(me, ('Message',))
+  def notify(me, line):
+    """Call with a new trace message."""
+    me.add(line)
+
+class WarningLogModel (LogModel):
+  """Log model for warnings.  We split the category out into a separate
+  column."""
+  def __init__(me):
+    LogModel.__init__(me, ('Category', 'Message'))
+  def notify(me, line):
+    """Call with a new warning message."""
+    me.add(*getword(line))
+
+class LogViewer (MyWindow):
+  """Log viewer window.  Nothing very exciting."""
+  def __init__(me, model):
+    MyWindow.__init__(me)
+    me.model = model
+    scr = G.ScrolledWindow()
+    scr.set_policy(G.POLICY_AUTOMATIC, G.POLICY_AUTOMATIC)
+    me.list = G.TreeView(me.model)
+    me.closehook = HookList()
+    i = 0
+    for c in me.model.cols:
+      me.list.append_column(G.TreeViewColumn(c,
+                                             G.CellRendererText(),
+                                             text = i))
+      i += 1
+    me.set_default_size(440, 256)
+    scr.add(me.list)
+    me.add(scr)
+    me.show_all()
+
+def makeactiongroup(name, acts):
+  """Creates an ActionGroup called NAME.  ACTS is a list of tuples
+  containing:
+  ACT: an action name
+  LABEL: the label string for the action
+  ACCEL: accelerator string, or None
+  FUNC: thunk to call when the action is invoked"""
+  actgroup = G.ActionGroup(name)
+  for act, label, accel, func in acts:
+    a = G.Action(act, label, None, None)
+    if func: a.connect('activate', invoker(func))
+    actgroup.add_action_with_accel(a, accel)
+  return actgroup
+
+class TraceOptions (MyDialog):
+  """Tracing options window."""
+  def __init__(me, monitor):
+    MyDialog.__init__(me, title = 'Tracing options',
+                      buttons = [(G.STOCK_CLOSE, me.destroy),
+                                 (G.STOCK_OK, me.ok)])
+    me.mon = monitor
+    me.opts = []
+    for o in me.mon.simplecmd('TRACE'):
+      char = o[0]
+      onp = o[1]
+      text = o[3].upper() + o[4:]
+      if char.isupper(): continue
+      ticky = G.CheckButton(text)
+      ticky.set_active(onp != ' ')
+      me.vbox.pack_start(ticky)
+      me.opts.append((char, ticky))
+    me.show_all()
+  def ok(me):
+    on = []
+    off = []
+    for char, ticky in me.opts:
+      if ticky.get_active():
+        on.append(char)
+      else:
+        off.append(char)
+    setting = ''.join(on) + '-' + ''.join(off)
+    me.mon.simplecmd('TRACE %s' % setting)
+    me.destroy()
+
+def unimplemented(*hunoz):
+  """Indicator of laziness."""
+  moanbox("I've not written that bit yet.")
+
+class GridPacker (G.Table):
+  """Like a Table, but with more state: makes filling in the widgets
+  easier."""
+  def __init__(me):
+    G.Table.__init__(me)
+    me.row = 0
+    me.col = 0
+    me.rows = 1
+    me.cols = 1
+    me.set_border_width(4)
+    me.set_col_spacings(4)
+    me.set_row_spacings(4)
+  def pack(me, w, width = 1, newlinep = False,
+           xopt = G.EXPAND | G.FILL | G.SHRINK, yopt = 0,
+           xpad = 0, ypad = 0):
+    """Packs a new widget.  W is the widget to add.  XOPY, YOPT, XPAD and
+    YPAD are as for Table.  WIDTH is how many cells to take up horizontally.
+    NEWLINEP is whether to start a new line for this widget.  Returns W."""
+    if newlinep:
+      me.row += 1
+      me.col = 0
+    bot = me.row + 1
+    right = me.col + width
+    if bot > me.rows or right > me.cols:
+      if bot > me.rows: me.rows = bot
+      if right > me.cols: me.cols = right
+      me.resize(me.rows, me.cols)
+    me.attach(w, me.col, me.col + width, me.row, me.row + 1,
+              xopt, yopt, xpad, ypad)
+    me.col += width
+    return w
+  def labelled(me, lab, w, newlinep = False, **kw):
+    """Packs a labelled widget.  Other arguments are as for pack.  Returns
+    W."""
+    label = G.Label(lab)
+    label.set_alignment(1.0, 0)
+    me.pack(label, newlinep = newlinep, xopt = G.FILL)
+    me.pack(w, **kw)
+    return w
+  def info(me, label, text = None, len = 18, **kw):
+    e = G.Entry()
+    if text is not None: e.set_text(text)
+    e.set_width_chars(len)
+    e.set_editable(False)
+    me.labelled(label, e, **kw)
+    return e
+
+def xlate_time(t):
+  """Translate a time in tripe's stats format to something a human might
+  actually want to read."""
+  if t == 'NEVER': return '(never)'
+  Y, M, D, h, m, s = map(int, rx_time.match(t).group(1, 2, 3, 4, 5, 6))
+  return '%04d:%02d:%02d %02d:%02d:%02d' % (Y, M, D, h, m, s)
+def xlate_bytes(b):
+  """Translate a number of bytes into something a human might want to read."""
+  suff = 'B'
+  b = int(b)
+  for s in 'KMG':
+    if b < 4096: break
+    b /= 1024
+    suff = s
+  return '%d %s' % (b, suff)
+
+## How to translate peer stats.  Maps the stat name to a translation
+## function.
+statsxlate = \
+  [('start-time', xlate_time),
+   ('last-packet-time', xlate_time),
+   ('last-keyexch-time', xlate_time),
+   ('bytes-in', xlate_bytes),
+   ('bytes-out', xlate_bytes),
+   ('keyexch-bytes-in', xlate_bytes),
+   ('keyexch-bytes-out', xlate_bytes),
+   ('ip-bytes-in', xlate_bytes),
+   ('ip-bytes-out', xlate_bytes)]
+
+## How to lay out the stats dialog.  Format is (LABEL, FORMAT): LABEL is
+## the label to give the entry box; FORMAT is the format string to write into
+## the entry.
+statslayout = \
+  [('Start time', '%(start-time)s'),
+   ('Last key-exchange', '%(last-keyexch-time)s'),
+   ('Last packet', '%(last-packet-time)s'),
+   ('Packets in/out',
+    '%(packets-in)s (%(bytes-in)s) / %(packets-out)s (%(bytes-out)s)'),
+   ('Key-exchange in/out',
+    '%(keyexch-packets-in)s (%(keyexch-bytes-in)s) / %(keyexch-packets-out)s (%(keyexch-bytes-out)s)'),
+   ('IP in/out',
+    '%(ip-packets-in)s (%(ip-bytes-in)s) / %(ip-packets-in)s (%(ip-bytes-in)s)'),
+   ('Rejected packets', '%(rejected-packets)s')]
+
+class PeerWindow (MyWindow):
+  """Show information about a peer."""
+  def __init__(me, monitor, peer):
+    MyWindow.__init__(me)
+    me.set_title('TrIPE statistics: %s' % peer.name)
+    me.mon = monitor
+    me.peer = peer
+    table = GridPacker()
+    me.add(table)
+    me.e = {}
+    def add(label, text = None):
+      me.e[label] = table.info(label, text, len = 42, newlinep = True)
+    add('Peer name', peer.name)
+    add('Tunnel', peer.tunnel)
+    add('Interface', peer.ifname)
+    add('Keepalives',
+        (peer.keepalive == '0' and 'never') or '%s s' % peer.keepalive)
+    add('Address', peer.addr)
+    add('Transport pings')
+    add('Encrypted pings')
+    for label, format in statslayout: add(label)
+    me.timeout = None
+    me.hook(me.mon.connecthook, me.tryupdate)
+    me.hook(me.mon.disconnecthook, me.stopupdate)
+    me.hook(me.closehook, me.stopupdate)
+    me.hook(me.peer.deadhook, me.dead)
+    me.hook(me.peer.pinghook, me.ping)
+    me.tryupdate()
+    me.ping()
+    me.show_all()
+  def update(me):
+    if not me.peer.alivep or not me.mon.connectedp: return False
+    stat = parseinfo(me.mon.simplecmd('STATS %s' % me.peer.name))
+    for s, trans in statsxlate:
+      stat[s] = trans(stat[s])
+    for label, format in statslayout:
+      me.e[label].set_text(format % stat)
+    return True
+  def tryupdate(me):
+    if me.timeout is None and me.update():
+      me.timeout = GO.timeout_add(1000, me.update)
+  def stopupdate(me):
+    if me.timeout is not None:
+      GO.source_remove(me.timeout)
+      me.timeout = None
+  def dead(me):
+    me.set_title('TrIPE statistics: %s [defunct]' % me.peer.name)
+    me.e['Peer name'].set_text('%s [defunct]' % me.peer.name)
+    me.stopupdate()
+  def ping(me):
+    for ping in me.peer.ping, me.peer.eping:
+      s = '%d/%d' % (ping.ngood, ping.n)
+      if ping.ngood:
+        s += '; %.2f ms (last %.1f ms)' % (ping.ttot/ping.ngood, ping.tlast);
+      me.e[ping.cmd].set_text(s)
+
+class AddPeerCommand (SimpleBackgroundCommand):
+  def __init__(me, conn, dlg, name, addr, port,
+               keepalive = None, tunnel = None):
+    me.name = name
+    me.addr = addr
+    me.port = port
+    me.keepalive = keepalive
+    me.tunnel = tunnel
+    cmd = StringIO()
+    cmd.write('ADD %s' % name)
+    cmd.write(' -background %s' % jobid())
+    if keepalive is not None: cmd.write(' -keepalive %s' % keepalive)
+    if tunnel is not None: cmd.write(' -tunnel %s' % tunnel)
+    cmd.write(' INET %s %s' % (addr, port))
+    SimpleBackgroundCommand.__init__(me, conn, cmd.getvalue())
+    me.hook(me.donehook, invoker(dlg.destroy))
+  def fail(me, err):
+    token, msg = getword(str(err))
+    if token in ('resolve-error', 'resolver-timeout'):
+      moanbox("Unable to resolve hostname `%s'" % me.addr)
+    elif token == 'peer-create-fail':
+      moanbox("Couldn't create new peer `%s'" % me.name)
+    elif token == 'peer-exists':
+      moanbox("Peer `%s' already exists" % me.name)
+    else:
+      moanbox("Unexpected error from server command `ADD': %s" % err)
+
+class AddPeerDialog (MyDialog):
+  def __init__(me, monitor):
+    MyDialog.__init__(me, 'Add peer',
+                      buttons = [(G.STOCK_CANCEL, me.destroy),
+                                 (G.STOCK_OK, me.ok)])
+    me.mon = monitor
+    table = GridPacker()
+    me.vbox.pack_start(table)
+    me.e_name = table.labelled('Name',
+                               ValidatingEntry(r'^[^\s.:]+$', '', 16),
+                               width = 3)
+    me.e_addr = table.labelled('Address',
+                               ValidatingEntry(r'^[a-zA-Z0-9.-]+$', '', 24),
+                               newlinep = True)
+    me.e_port = table.labelled('Port',
+                               ValidatingEntry(numericvalidate(0, 65535),
+                                               '22003',
+                                               5))
+    me.c_keepalive = G.CheckButton('Keepalives')
+    me.l_tunnel = table.labelled('Tunnel',
+                                 G.combo_box_new_text(),
+                                 newlinep = True, width = 3)
+    me.tuns = me.mon.simplecmd('TUNNELS')
+    for t in me.tuns:
+      me.l_tunnel.append_text(t)
+    me.l_tunnel.set_active(0)
+    table.pack(me.c_keepalive, newlinep = True, xopt = G.FILL)
+    me.c_keepalive.connect('toggled',
+                           lambda t: me.e_keepalive.set_sensitive\
+                                      (t.get_active()))
+    me.e_keepalive = ValidatingEntry(r'^\d+[hms]?$', '', 5)
+    me.e_keepalive.set_sensitive(False)
+    table.pack(me.e_keepalive, width = 3)
+    me.show_all()
+  def ok(me):
+    try:
+      if me.c_keepalive.get_active():
+        ka = me.e_keepalive.get_text()
+      else:
+        ka = None
+      t = me.l_tunnel.get_active()
+      if t == 0:
+        tun = None
+      else:
+        tun = me.tuns[t]
+      AddPeerCommand(me.mon, me,
+                     me.e_name.get_text(),
+                     me.e_addr.get_text(),
+                     me.e_port.get_text(),
+                     keepalive = ka,
+                     tunnel = tun)
+    except ValidationError:
+      GDK.beep()
+      return
+
+class ServInfo (MyWindow):
+  def __init__(me, monitor):
+    MyWindow.__init__(me)
+    me.set_title('TrIPE server info')
+    me.mon = monitor
+    me.table = GridPacker()
+    me.add(me.table)
+    me.e = {}
+    def add(label, tag, text = None, **kw):
+      me.e[tag] = me.table.info(label, text, **kw)
+    add('Implementation', 'implementation')
+    add('Version', 'version', newlinep = True)
+    me.update()
+    me.hook(me.mon.connecthook, me.update)
+    me.show_all()
+  def update(me):
+    info = parseinfo(me.mon.simplecmd('SERVINFO'))
+    for i in me.e:
+      me.e[i].set_text(info[i])
+
+class PingCommand (SimpleBackgroundCommand):
+  def __init__(me, conn, cmd, peer, func):
+    me.peer = peer
+    me.func = func
+    SimpleBackgroundCommand.__init__ \
+      (me, conn, '%s -background %s %s' % (cmd, jobid(), peer.name))
+  def ok(me):
+    tok, rest = getword(me.info[0])
+    if tok == 'ping-ok':
+      me.func(me.peer, float(rest))
+    else:
+      me.func(me.peer, None)
+    me.unhookall()
+  def fail(me, err): me.unhookall()
+  def lost(me): me.unhookall()
+
+class MonitorWindow (MyWindow):
+
+  def __init__(me, monitor):
+    MyWindow.__init__(me)
+    me.set_title('TrIPE monitor')
+    me.mon = monitor
+    me.hook(me.mon.errorhook, me.report)
+    me.warnings = WarningLogModel()
+    me.hook(me.mon.warnhook, me.warnings.notify)
+    me.trace = TraceLogModel()
+    me.hook(me.mon.tracehook, me.trace.notify)
+
+    me.warnview = WindowSlot(lambda: LogViewer(me.warnings))
+    me.traceview = WindowSlot(lambda: LogViewer(me.trace))
+    me.traceopts = WindowSlot(lambda: TraceOptions(me.mon))
+    me.addpeerwin = WindowSlot(lambda: AddPeerDialog(me.mon))
+    me.servinfo = WindowSlot(lambda: ServInfo(me.mon))
+
+    vbox = G.VBox()
+    me.add(vbox)
+
+    me.ui = G.UIManager()
+    actgroup = makeactiongroup('monitor',
+      [('file-menu', '_File', None, None),
+       ('connect', '_Connect', '<Alt>C', me.mon.connect),
+       ('disconnect', '_Disconnect', '<Alt>D', me.mon.disconnect),
+       ('quit', '_Quit', '<Alt>Q', me.close),
+       ('server-menu', '_Server', None, None),
+       ('daemon', 'Run in _background', None,
+          lambda: me.mon.simplecmd('DAEMON')),
+       ('server-version', 'Server version', None, me.servinfo.open),
+       ('server-quit', 'Terminate server', None,
+          lambda: me.mon.simplecmd('QUIT')),
+       ('logs-menu', '_Logs', None, None),
+       ('show-warnings', 'Show _warnings', '<Alt>W', me.warnview.open),
+       ('show-trace', 'Show _trace', '<Alt>T', me.traceview.open),
+       ('trace-options', 'Trace _options...', None, me.traceopts.open),
+       ('help-menu', '_Help', None, None),
+       ('about', '_About tripemon...', None, aboutbox.open),
+       ('add-peer', '_Add peer...', '<Alt>A', me.addpeerwin.open),
+       ('kill-peer', '_Kill peer', None, me.killpeer),
+       ('force-kx', 'Force key e_xchange', None, me.forcekx)])
+    uidef = '''
+      <ui>
+        <menubar>
+          <menu action="file-menu">
+            <menuitem action="quit"/>
+          </menu>
+          <menu action="server-menu">
+            <menuitem action="connect"/>
+            <menuitem action="disconnect"/>
+            <separator/>
+            <menuitem action="add-peer"/>
+            <menuitem action="daemon"/>
+            <menuitem action="server-version"/>
+            <separator/>
+            <menuitem action="server-quit"/>
+          </menu>
+          <menu action="logs-menu">
+            <menuitem action="show-warnings"/>
+            <menuitem action="show-trace"/>
+            <menuitem action="trace-options"/>
+          </menu>
+          <menu action="help-menu">
+            <menuitem action="about"/>
+          </menu>
+        </menubar>
+        <popup name="peer-popup">
+          <menuitem action="add-peer"/>
+          <menuitem action="kill-peer"/>
+          <menuitem action="force-kx"/>
+        </popup>
+      </ui>
+      '''
+    me.ui.insert_action_group(actgroup, 0)
+    me.ui.add_ui_from_string(uidef)
+    vbox.pack_start(me.ui.get_widget('/menubar'), expand = False)
+    me.add_accel_group(me.ui.get_accel_group())
+    me.status = G.Statusbar()
+
+    me.listmodel = G.ListStore(*(GO.TYPE_STRING,) * 6)
+    me.listmodel.set_sort_column_id(0, G.SORT_ASCENDING)
+    me.hook(me.mon.addpeerhook, me.addpeer)
+    me.hook(me.mon.delpeerhook, me.delpeer)
+
+    scr = G.ScrolledWindow()
+    scr.set_policy(G.POLICY_AUTOMATIC, G.POLICY_AUTOMATIC)
+    me.list = G.TreeView(me.listmodel)
+    me.list.append_column(G.TreeViewColumn('Peer name',
+                                           G.CellRendererText(),
+                                           text = 0))
+    me.list.append_column(G.TreeViewColumn('Address',
+                                           G.CellRendererText(),
+                                           text = 1))
+    me.list.append_column(G.TreeViewColumn('T-ping',
+                                           G.CellRendererText(),
+                                           text = 2,
+                                           foreground = 3))
+    me.list.append_column(G.TreeViewColumn('E-ping',
+                                           G.CellRendererText(),
+                                           text = 4,
+                                           foreground = 5))
+    me.list.get_column(1).set_expand(True)
+    me.list.connect('row-activated', me.activate)
+    me.list.connect('button-press-event', me.buttonpress)
+    me.list.set_reorderable(True)
+    me.list.get_selection().set_mode(G.SELECTION_NONE)
+    scr.add(me.list)
+    vbox.pack_start(scr)
+
+    vbox.pack_start(me.status, expand = False)
+    me.hook(me.mon.connecthook, me.connected)
+    me.hook(me.mon.disconnecthook, me.disconnected)
+    me.hook(me.mon.notehook, me.notify)
+    me.pinger = None
+    me.set_default_size(420, 180)
+    me.mon.connect()
+    me.show_all()
+
+  def addpeer(me, peer):
+    peer.i = me.listmodel.append([peer.name, peer.addr,
+                                  '???', 'green', '???', 'green'])
+    peer.win = WindowSlot(lambda: PeerWindow(me.mon, peer))
+    peer.pinghook = HookList()
+    peer.ping = pingstate(n = 0, ngood = 0, nmiss = 0, nmissrun = 0,
+                          tlast = 0, ttot = 0,
+                          tcol = 2, ccol = 3, cmd = 'Transport pings')
+    peer.eping = pingstate(n = 0, ngood = 0, nmiss = 0, nmissrun = 0,
+                           tlast = 0, ttot = 0,
+                           tcol = 4, ccol = 5, cmd = 'Encrypted pings')
+  def delpeer(me, peer):
+    me.listmodel.remove(peer.i)
+  def path_peer(me, path):
+    return me.mon.peers[me.listmodel[path][0]]
+
+  def activate(me, l, path, col):
+    peer = me.path_peer(path)
+    peer.win.open()
+  def buttonpress(me, l, ev):
+    if ev.button == 3:
+      r = me.list.get_path_at_pos(ev.x, ev.y)
+      for i in '/peer-popup/kill-peer', '/peer-popup/force-kx':
+        me.ui.get_widget(i).set_sensitive(me.mon.connectedp and
+                                          r is not None)
+      if r:
+        me.menupeer = me.path_peer(r[0])
+      else:
+        me.menupeer = None
+      me.ui.get_widget('/peer-popup').popup(None, None, None,
+                                            ev.button, ev.time)
+
+  def killpeer(me):
+    me.mon.simplecmd('KILL %s' % me.menupeer.name)
+  def forcekx(me):
+    me.mon.simplecmd('FORCEKX %s' % me.menupeer.name)
+
+  def reping(me):
+    if me.pinger is not None:
+      GO.source_remove(me.pinger)
+    me.pinger = GO.timeout_add(10000, me.ping)
+    me.ping()
+  def unping(me):
+    if me.pinger is not None:
+      GO.source_remove(me.pinger)
+      me.pinger = None
+  def ping(me):
+    for name in me.mon.peers:
+      p = me.mon.peers[name]
+      PingCommand(me.mon, 'PING', p, lambda p, t: me.pong(p, p.ping, t))
+      PingCommand(me.mon, 'EPING', p, lambda p, t: me.pong(p, p.eping, t))
+    return True
+  def pong(me, p, ping, t):
+    ping.n += 1
+    if t is None:
+      ping.nmiss += 1
+      ping.nmissrun += 1
+      me.listmodel[p.i][ping.tcol] = '(miss %d)' % ping.nmissrun
+      me.listmodel[p.i][ping.ccol] = 'red'
+    else:
+      ping.ngood += 1
+      ping.nmissrun = 0
+      ping.tlast = t
+      ping.ttot += t
+      me.listmodel[p.i][ping.tcol] = '%.1f ms' % t
+      me.listmodel[p.i][ping.ccol] = 'black'
+    p.pinghook.run()
+  def setstatus(me, status):
+    me.status.pop(0)
+    me.status.push(0, status)
+  def notify(me, note, rest):
+    if note == 'DAEMON':
+      me.ui.get_widget('/menubar/server-menu/daemon').set_sensitive(False)
+  def connected(me):
+    me.setstatus('Connected (port %s)' % me.mon.simplecmd('PORT')[0])
+    me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(False)
+    for i in ('/menubar/server-menu/disconnect',
+              '/menubar/server-menu/server-version',
+              '/menubar/server-menu/add-peer',
+              '/menubar/server-menu/server-quit',
+              '/menubar/logs-menu/trace-options'):
+      me.ui.get_widget(i).set_sensitive(True)
+    me.ui.get_widget('/menubar/server-menu/daemon'). \
+      set_sensitive(parseinfo(me.mon.simplecmd('SERVINFO'))['daemon'] ==
+                    'nil')
+    me.reping()
+  def disconnected(me):
+    me.setstatus('Disconnected')
+    me.ui.get_widget('/menubar/server-menu/connect').set_sensitive(True)
+    for i in ('/menubar/server-menu/disconnect',
+              '/menubar/server-menu/server-version',
+              '/menubar/server-menu/add-peer',
+              '/menubar/server-menu/daemon',
+              '/menubar/server-menu/server-quit',
+              '/menubar/logs-menu/trace-options'):
+      me.ui.get_widget(i).set_sensitive(False)
+    me.unping()
+  def destroy(me):
+    if me.pinger is not None:
+      GO.source_remove(me.pinger)    
+  def report(me, msg):
+    moanbox(msg)
+    return True
+
+#----- Parse options --------------------------------------------------------
+
+def version(fp = stdout):
+  """Print the program's version number."""
+  fp.write('%s, %s version %s\n' % (quis, PACKAGE, VERSION))
+
+def usage(fp):
+  """Print a brief usage message for the program."""
+  fp.write('Usage: %s [-d DIR] [-a SOCK]\n' % quis)
+
+def main():
+  global tripedir
+  if 'TRIPEDIR' in environ:
+    tripedir = environ['TRIPEDIR']
+  tripesock = '%s/%s' % (socketdir, 'tripesock')
+
+  try:
+    opts, args = O.getopt(argv[1:],
+                          'hvud:a:',
+                          ['help', 'version', 'usage',
+                           'directory=', 'admin-socket='])
+  except O.GetoptError, exc:
+    moan(exc)
+    usage(stderr)
+    exit(1)
+  for o, v in opts:
+    if o in ('-h', '--help'):
+      version(stdout)
+      print
+      usage(stdout)
+      print """
+Graphical monitor for TrIPE VPN.
+
+Options supported:
+
+-h, --help             Show this help message.
+-v, --version          Show the version number.
+-u, --usage            Show pointlessly short usage string.
+
+-d, --directory=DIR    Use TrIPE directory DIR.
+-a, --admin-socket=FILE        Select socket to connect to."""
+      exit(0)
+    elif o in ('-v', '--version'):
+      version(stdout)
+      exit(0)
+    elif o in ('-u', '--usage'):
+      usage(stdout)
+      exit(0)
+    elif o in ('-d', '--directory'):
+      tripedir = v
+    elif o in ('-a', '--admin-socket'):
+      tripesock = v
+    else:
+      raise "can't happen!"
+  if len(args) > 0:
+    usage(stderr)
+    exit(1)
+
+  OS.chdir(tripedir)
+  mon = Monitor(tripesock)
+  root = MonitorWindow(mon)
+  HookClient().hook(root.closehook, exit)
+  G.main()
+
+if __name__ == '__main__':
+  main()
+
index 5efe5ef..22f01e6 100644 (file)
--- a/tun-bsd.c
+++ b/tun-bsd.c
@@ -80,8 +80,8 @@ static void t_read(int fd, unsigned mode, void *v)
     return;
   }
   IF_TRACING(T_TUNNEL, {
-    trace(T_TUNNEL, "tunnel: packet arrived");
-    trace_block(T_PACKET, "tunnel: packet contents", buf_i, n);
+    trace(T_TUNNEL, "tun-bsd: packet arrived");
+    trace_block(T_PACKET, "tun-bsd: packet contents", buf_i, n);
   })
   buf_init(&b, buf_i, n);
   p_tun(t->p, &b);
@@ -141,7 +141,7 @@ static tunnel *t_create(peer *p)
   t->n = n;
   sel_initfile(&sel, &t->f, fd, SEL_READ, t_read, t);
   sel_addfile(&t->f);
-  T( trace(T_TUNNEL, "tunnel: attached interface %s to peer `%s'",
+  T( trace(T_TUNNEL, "tun-bsd: attached interface %s to peer `%s'",
           t_ifname(t), p_name(p)); )
   return (t);
 }
@@ -159,8 +159,8 @@ static tunnel *t_create(peer *p)
 static void t_inject(tunnel *t, buf *b)
 {
   IF_TRACING(T_TUNNEL, {
-    trace(T_TUNNEL, "tunnel: inject decrypted packet");
-    trace_block(T_PACKET, "tunnel: packet contents", BBASE(b), BLEN(b));
+    trace(T_TUNNEL, "tun-bsd: inject decrypted packet");
+    trace_block(T_PACKET, "tun-bsd: packet contents", BBASE(b), BLEN(b));
   })
   write(t->f.fd, BBASE(b), BLEN(b));
 }
index 8c7278c..bf44930 100644 (file)
@@ -72,8 +72,8 @@ static void t_read(int fd, unsigned mode, void *v)
     return;
   }
   IF_TRACING(T_TUNNEL, {
-    trace(T_TUNNEL, "tunnel: packet arrived");
-    trace_block(T_PACKET, "tunnel: packet contents", buf_i, n);
+    trace(T_TUNNEL, "tun-linux: packet arrived");
+    trace_block(T_PACKET, "tun-linux: packet contents", buf_i, n);
   })
   buf_init(&b, buf_i, n);
   p_tun(t->p, &b);
@@ -112,6 +112,7 @@ static tunnel *t_create(peer *p)
     return (0);
   }
   fdflags(fd, O_NONBLOCK, O_NONBLOCK, FD_CLOEXEC, FD_CLOEXEC);
+  memset(&iff, 0, sizeof(iff));
   iff.ifr_name[0] = 0;
   iff.ifr_flags = IFF_TUN | IFF_NO_PI;
   if ((f = ioctl(fd, TUNSETIFF, &iff)) < 0) {
@@ -126,7 +127,7 @@ static tunnel *t_create(peer *p)
   sel_addfile(&t->f);
   iff.ifr_name[IFNAMSIZ - 1] = 0;
   strcpy(t->ifn, iff.ifr_name);
-  T( trace(T_TUNNEL, "tunnel: attached interface %s to peer `%s'",
+  T( trace(T_TUNNEL, "tun-linux: attached interface %s to peer `%s'",
           t->ifn, p_name(p)); )
   return (t);
 }
@@ -153,7 +154,7 @@ static const char *t_ifname(tunnel *t) { return (t->ifn); }
 static void t_inject(tunnel *t, buf *b)
 {
   IF_TRACING(T_TUNNEL, {
-    trace(T_TUNNEL, "tunnel: inject decrypted packet");
+    trace(T_TUNNEL, "tun-linux: inject decrypted packet");
     trace_block(T_PACKET, "tunnel: packet contents", BBASE(b), BLEN(b));
   })
   write(t->f.fd, BBASE(b), BLEN(b));
index 2d94033..2b74a77 100644 (file)
@@ -110,7 +110,7 @@ static void t_read(int fd, unsigned mode, void *v)
     return;
   }
   IF_TRACING(T_TUNNEL, {
-    trace_block(T_PACKET, "tunnel: SLIP-encapsulated data",
+    trace_block(T_PACKET, "tun-slip: SLIP-encapsulated data",
                buf_t, n);
   })
 
@@ -128,11 +128,11 @@ static void t_read(int fd, unsigned mode, void *v)
        else if (st & ST_ESC)
          a_warn("TUN %s slip escape-end", t->sl->name);
        else if (q == t->buf) {
-         T( trace(T_TUNNEL, "tunnel: empty packet"); )
+         T( trace(T_TUNNEL, "tun-slip: empty packet"); )
        } else {
          IF_TRACING(T_TUNNEL, {
-           trace(T_TUNNEL, "tunnel: packet arrived");
-           trace_block(T_PACKET, "tunnel: packet contents",
+           trace(T_TUNNEL, "tun-slip: packet arrived");
+           trace_block(T_PACKET, "tun-slip: packet contents",
                        t->buf, q - t->buf);
          })
          buf_init(&b, t->buf, q - t->buf);
@@ -208,7 +208,7 @@ static void t_init(void)
   for (;;) {
     if (*p == '/' || *p == '.') {
       slipcmd = p;
-      T( trace(T_TUNNEL, "tunnel: declared slip command `%s'", slipcmd); )
+      T( trace(T_TUNNEL, "tun-slip: declared slip command `%s'", slipcmd); )
       break;
     }
     uli = strtoul(p, &q, 0);
@@ -237,7 +237,7 @@ static void t_init(void)
     sl->name[n] = 0;
     *tail = sl;
     tail = &sl->next;
-    T( trace(T_TUNNEL, "tunnel: declared slipif %d,%d=%s",
+    T( trace(T_TUNNEL, "tun-slip: declared slipif %d,%d=%s",
             sl->ifd, sl->ofd, sl->name); )
     p = q + n + 1;
     if (!*p)
@@ -273,7 +273,7 @@ static tunnel *t_create(peer *p)
 
   for (sl = slipifs; sl; sl = sl->next) {
     if (!(sl->f & F_INUSE)) {
-      T( trace(T_TUNNEL, "tunnel: %s using static slipif %s",
+      T( trace(T_TUNNEL, "tun-slip: %s using static slipif %s",
               p_name(p), sl->name); )
       goto found;
     }
@@ -327,7 +327,7 @@ static tunnel *t_create(peer *p)
   sl->kid = kid;
   sl->next = 0;
   sl->f = F_DYNAMIC;
-  T( trace(T_TUNNEL, "tunnel: %s using dynamic slipif %s",
+  T( trace(T_TUNNEL, "tun-slip: %s using dynamic slipif %s",
           p_name(p), sl->name); )
   fdflags(pout[0], O_NONBLOCK, O_NONBLOCK, FD_CLOEXEC, FD_CLOEXEC);
   fdflags(pin[1], O_NONBLOCK, 0, FD_CLOEXEC, FD_CLOEXEC);
@@ -387,8 +387,8 @@ static void t_inject(tunnel *t, buf *b)
   octet *q;
 
   IF_TRACING(T_TUNNEL, {
-    trace(T_TUNNEL, "tunnel: inject decrypted packet");
-    trace_block(T_PACKET, "tunnel: packet contents", BBASE(b), BLEN(b));
+    trace(T_TUNNEL, "tun-slip: inject decrypted packet");
+    trace_block(T_PACKET, "tun-slip: packet contents", BBASE(b), BLEN(b));
   })
 
   q = buf;
@@ -402,7 +402,7 @@ static void t_inject(tunnel *t, buf *b)
   }
   *q++ = SL_END;
   IF_TRACING(T_TUNNEL, {
-    trace_block(T_PACKET, "tunnel: SLIP-encapsulated contents",
+    trace_block(T_PACKET, "tun-slip: SLIP-encapsulated contents",
                buf, q - buf);
   })
   write(t->sl->ofd, buf, q - buf);
@@ -428,7 +428,7 @@ static void t_destroy(tunnel *t)
     sl->f &= ~F_INUSE;
   }
   if (sl && (sl->f & F_DYNAMIC)) {
-    T( trace(T_TUNNEL, "tunnel: releasing dynamic slipif %s", sl->name); )
+    T( trace(T_TUNNEL, "tun-slip: releasing dynamic slipif %s", sl->name); )
     close(sl->ofd);
     close(sl->ifd);
     kill(sl->kid, SIGTERM);
index a6b53eb..c93803e 100644 (file)
@@ -94,8 +94,8 @@ static void t_read(int fd, unsigned mode, void *v)
     return;
   }
   IF_TRACING(T_TUNNEL, {
-    trace(T_TUNNEL, "tunnel: packet arrived");
-    trace_block(T_PACKET, "tunnel: packet contents", buf_i, n);
+    trace(T_TUNNEL, "tun-unet: packet arrived");
+    trace_block(T_PACKET, "tun-unet: packet contents", buf_i, n);
   })
   buf_init(&b, buf_i, n);
   p_tun(t->p, &b);
@@ -145,7 +145,7 @@ static tunnel *t_create(peer *p)
   t->p = p;
   sel_initfile(&sel, &t->f, fd, SEL_READ, t_read, t);
   sel_addfile(&t->f);
-  T( trace(T_TUNNEL, "tunnel: attached interface %s to peer `%s'",
+  T( trace(T_TUNNEL, "tun-unet: attached interface %s to peer `%s'",
           t_ifname(t), p_name(p)); )
   return (t);
 }
@@ -163,8 +163,8 @@ static tunnel *t_create(peer *p)
 static void t_inject(tunnel *t, buf *b)
 {
   IF_TRACING(T_TUNNEL, {
-    trace(T_TUNNEL, "tunnel: inject decrypted packet");
-    trace_block(T_PACKET, "tunnel: packet contents", BBASE(b), BLEN(b));
+    trace(T_TUNNEL, "tun-unet: inject decrypted packet");
+    trace_block(T_PACKET, "tun-unet: packet contents", BBASE(b), BLEN(b));
   })
   write(t->f.fd, BBASE(b), BLEN(b));
 }