svc: Peer management services.
authorMark Wooding <mdw@distorted.org.uk>
Mon, 19 Apr 2010 20:11:05 +0000 (21:11 +0100)
committerMark Wooding <mdw@distorted.org.uk>
Mon, 19 Apr 2010 20:11:05 +0000 (21:11 +0100)
  * connect arranges to connect to named peers, and respond to incoming
    connections.

  * watch detects newly added peers, and configures interfaces and makes
    outgoing connections accordingly.

Also update the init script so as to start services found in
/etc/tripe/services.

18 files changed:
Makefile.am
debian/.gitignore
debian/control
debian/rules
debian/tripe-peer-services.dirs [new file with mode: 0644]
debian/tripe-peer-services.install [new file with mode: 0644]
debian/tripe.dirs
init/tripe-init.in
peerdb/peers.in.5.in
py/tripe.py.in
server/Makefile.am
svc/Makefile.am
svc/connect.8.in [new file with mode: 0644]
svc/connect.in [new file with mode: 0644]
svc/tripe-ifup.8.in [new file with mode: 0644]
svc/tripe-ifup.in [new file with mode: 0644]
svc/watch.8.in [new file with mode: 0644]
svc/watch.in [new file with mode: 0644]

index 4446113..7cbef36 100644 (file)
@@ -133,6 +133,10 @@ EXTRA_DIST         += debian/tripe-keys.install
 ## modules
 EXTRA_DIST             += debian/python-tripe.install
 
+## peer services
+EXTRA_DIST             += debian/tripe-peer-services.dirs
+EXTRA_DIST             += debian/tripe-peer-services.install
+
 ## monitor
 EXTRA_DIST             += debian/tripemon.install
 
index 50597ec..e8d38ce 100644 (file)
@@ -20,4 +20,4 @@ tripemon
 tripe-keys
 tripe-ethereal
 tripe-uslip
-
+tripe-peer-services
index 91626c2..b451153 100644 (file)
@@ -97,6 +97,18 @@ Description: Trivial IP Encryption: a simple virtual private network
  implementation, based on threads, is included, so small sites can probably
  manage without.
 
+Package: tripe-peer-services
+Architecture: all
+Depends: python (>= 2.4), python-mlib, tripe, python-cdb, python-tripe, pathmtu
+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 services for managing connections with peers:
+ .
+ connect       Register named peer correctly
+ watch         Configure interface and wake up remote end when peer arrives
+
 Package: tripe-keys
 Architecture: all
 Depends: python (>= 2.4), tripe, catacomb-bin, python-catacomb
index 12de3ab..0a422e8 100755 (executable)
@@ -44,6 +44,13 @@ cleanbuilddir::
        rm -f debian/tripe.init debian/tripe.default
 
 ###--------------------------------------------------------------------------
+### Install configuration files.
+
+install/tripe-peer-services::
+       install -m644 $(DEB_SRCDIR)/peerdb/peers.in \
+               debian/tripe-peer-services/etc/tripe/peers.d/10base
+
+###--------------------------------------------------------------------------
 ### Wireshark plugin.
 
 ## Grab the version we built against, so we can put it in the dependencies.
diff --git a/debian/tripe-peer-services.dirs b/debian/tripe-peer-services.dirs
new file mode 100644 (file)
index 0000000..b6462c3
--- /dev/null
@@ -0,0 +1 @@
+etc/tripe/peers.d
diff --git a/debian/tripe-peer-services.install b/debian/tripe-peer-services.install
new file mode 100644 (file)
index 0000000..6a8618f
--- /dev/null
@@ -0,0 +1,10 @@
+debian/tmp/usr/lib/tripe/services/connect
+debian/tmp/usr/share/man/man8/connect.8
+debian/tmp/usr/lib/tripe/services/watch
+debian/tmp/usr/share/man/man8/watch.8
+debian/tmp/usr/sbin/tripe-newpeers
+debian/tmp/usr/share/man/man8/tripe-newpeers.8
+debian/tmp/usr/share/man/man5/peers.in.5
+debian/tmp/usr/share/man/man5/peers.cdb.5
+debian/tmp/usr/sbin/tripe-ifup
+debian/tmp/usr/share/man/man8/tripe-ifup.8
index 0048c94..2683e2f 100644 (file)
@@ -1 +1,2 @@
 etc/tripe/peers
+etc/tripe/services
index dfa7e03..433f6a0 100755 (executable)
@@ -136,17 +136,32 @@ case "$1" in
       exit 1
     fi
     echo -n " tripe"
-    for i in $TRIPEDIR/peers/*; do
+    sep=" services [" end=""
+    [ -d $TRIPEDIR/services ] && for i in $TRIPEDIR/services/*; do
+      [ -x $i ] || continue
+      name=`basename $i`
+      case $name in *~|\#*) continue;; esac
+      if $i --daemon --startup; then
+       echo -n "$sep$name"
+      else
+        echo -n "$sep($name failed)"
+      fi
+      sep=" " end="]"
+    done
+    echo -n "$end"
+    sep=" peers [" end=""
+    [ -d $TRIPEDIR/peers ] && for i in $TRIPEDIR/peers/*; do
       [ -x $i ] || continue
       name=`basename $i`
       case $name in *~|\#*) continue;; esac
       if $i; then
-       echo -n " $name"
+       echo -n "$sep$name"
       else
-       echo -n " ($name failed)"
+        echo -n "$sep($name failed)"
       fi
+      sep=" " end="]"
     done
-    echo " done"
+    echo "$end done"
     ;;
   stop)
     echo -n "Stopping TrIPE VPN daemon:"
index 7b0127b..fbc7c5c 100644 (file)
@@ -126,13 +126,83 @@ details.
 If true, include the peer in the
 .B %AUTO
 record.  Used by
+.BR connect (8)
+and
 .BR tripe-newpeers (8);
 described below.
 .TP
+.B connect
+Shell command for initiating connection to this peer.  Used by
+.BR watch (8).
+.TP
+.B cork
+Don't initiate immediate key exchange..  Used by
+.BR connect (8).
+.TP
+.B every
+Interval for checking that the peer is still alive and well.  Used by
+.BR watch (8).
+.TP
+.B ifdown
+Script to bring down tunnel interface connected to the peer.  Used by
+.BR watch (8).
+.TP
+.B ifname
+Interface name to set for the tunnel interface to the peer.  Used by
+.BR tripe-ifup (8).
+.TP
+.B ifup
+Script to bring up tunnel interface connected to the peer.  Used by
+.BR watch (8).
+.TP
+.B ifupextra
+Script containing additional interface setup.  Used by
+.BR tripe-ifup (8).
+.TP
+.B laddr
+Local address for the tunnel interface to the peer.  Used by
+.BR tripe-ifup (8).
+.TP
+.B keepalive
+Interval for sending keepalive pings.  Used by
+.BR connect (8).
+.TP
+.B mtu
+Maximum transmission unit for the tunnel interface.  Used by
+.BR tripe-ifup (8).
+.TP
+.B nets
+Networks to be routed over the tunnel interface.  Used by
+.BR tripe-ifup (8).
+.TP
+.B peer
+Network address for this peer, or
+.BR PASSIVE .
+Used by
+.BR connect (8).
+.TP
+.B raddr
+Remote address for the tunnel interface to the peer.  Used by
+.BR tripe-ifup (8).
+.TP
+.B retries
+Number of failed ping attempts before attempting reconnection.  Used by
+.BR watch (8).
+.TP
+.B timeout
+Timeout for ping probes.  Used by
+.BR watch (8).
+.TP
+.B tunnel
+Tunnel driver to use when adding the peer.  Used by
+.BR connect (8)).
+.TP
 .B user
 Peer will make active connection as
 .IR user .
 Used by
+.BR connect (8)
+and
 .BR tripe-newpeers (8);
 described below.
 .SS "Conversion"
@@ -201,6 +271,8 @@ is created whose contents is the section name.
 .PP
 .BR tripe-newpeers (8),
 .BR peers.cdb (5),
+.BR connect (8),
+.BR watch (8),
 .BR tripe-ifup (8).
 .
 .\"--------------------------------------------------------------------------
index 7906010..e88f379 100644 (file)
@@ -31,7 +31,7 @@ implementing services.
 Rather than end up in lost in a storm of little event-driven classes, or a
 morass of concurrent threads, the module uses coroutines to present a fairly
 simple function call/return interface to potentially long-running commands
-which must run without blocking the main process.  It sassumes a coroutine
+which must run without blocking the main process.  It assumes a coroutine
 module presenting a subset of the `greenlet' interface: if actual greenlets
 are available, they are used; otherwise there's an implementation in terms of
 threads (with lots of locking) which will do instead.
index b6df428..4bebcd9 100644 (file)
@@ -66,6 +66,6 @@ EXTRA_DIST            += tripe.8.in
 ## The admin protocol manual page.
 man_MANS               += tripe-admin.5
 CLEANFILES             += tripe-admin.5
-EXTRA_DIST             += tripe-admin.5.in make-summary
+EXTRA_DIST             += tripe-admin.5.in
 
 ###----- That's all, folks --------------------------------------------------
index f37ecb1..3e392b4 100644 (file)
@@ -25,6 +25,9 @@
 
 include $(top_srcdir)/vars.am
 
+servicesdir = ${pkglibdir}/services
+
+services_SCRIPTS        =
 man_MANS                =
 
 ###--------------------------------------------------------------------------
@@ -35,4 +38,44 @@ man_MANS             += tripe-service.7
 CLEANFILES             += tripe-service.7
 EXTRA_DIST             += tripe-service.7.in
 
+## Handle dynamic connections.
+services_SCRIPTS       += connect
+CLEANFILES             += connect
+EXTRA_DIST             += connect.in
+
+man_MANS               += connect.8
+CLEANFILES             += connect.8
+EXTRA_DIST             += connect.8.in
+
+connect: connect.in Makefile
+       $(confsubst) $(srcdir)/connect.in >$@.new $(SUBSTITUTIONS) && \
+               chmod +x $@.new && mv $@.new $@
+
+## Watch for peers arriving and disconnecting.
+services_SCRIPTS       += watch
+CLEANFILES             += watch
+EXTRA_DIST             += watch.in
+
+man_MANS               += watch.8
+CLEANFILES             += watch.8
+EXTRA_DIST             += watch.8.in
+
+watch: watch.in Makefile
+       $(confsubst) $(srcdir)/watch.in >$@.new $(SUBSTITUTIONS) && \
+               chmod +x $@.new && mv $@.new $@
+
+## Bring up an interface.
+sbin_SCRIPTS            = tripe-ifup
+CLEANFILES             += tripe-ifup
+EXTRA_DIST             += tripe-ifup.in
+
+man_MANS               += tripe-ifup.8
+CLEANFILES             += tripe-ifup.8
+EXTRA_DIST             += tripe-ifup.8.in
+
+tripe-ifup: tripe-ifup.in Makefile
+       $(confsubst) $(srcdir)/tripe-ifup.in >$@.new $(SUBSTITUTIONS) && \
+               chmod +x $@.new && mv $@.new $@
+
 ###----- That's all, folks --------------------------------------------------
+
diff --git a/svc/connect.8.in b/svc/connect.8.in
new file mode 100644 (file)
index 0000000..77357f5
--- /dev/null
@@ -0,0 +1,361 @@
+.\" -*-nroff-*-
+.\".
+.\" Manual for the connect service
+.\"
+.\" (c) 2008 Straylight/Edgeware
+.\"
+.
+.\"----- Licensing notice ---------------------------------------------------
+.\"
+.\" This file is part of Trivial IP Encryption (TrIPE).
+.\"
+.\" TrIPE 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.
+.\"
+.\" TrIPE 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 TrIPE; if not, write to the Free Software Foundation,
+.\" Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+.
+.\"--------------------------------------------------------------------------
+.so ../defs.man.in \"@@@PRE@@@
+.
+.\"--------------------------------------------------------------------------
+.TH connect 8 "8 January 2007" "Straylight/Edgeware" "TrIPE: Trivial IP Encryption"
+.
+.\"--------------------------------------------------------------------------
+.SH "NAME"
+.
+connect \- tripe service to make connections to peers
+.
+.\"--------------------------------------------------------------------------
+.SH "SYNOPSIS"
+.
+.B connect
+.RB [ \-a
+.IR socket ]
+.RB [ \-d
+.IR dir ]
+.RB [ \-p
+.IR file ]
+.br
+\&     \c
+.RB [ \-\-daemon ]
+.RB [ \-\-debug ]
+.RB [ \-\-startup ]
+.
+.\"--------------------------------------------------------------------------
+.SH "DESCRIPTION"
+.
+The
+.B connect
+service registers new peers with the
+.BR tripe (8)
+server.
+.PP
+A peer may participate
+.I actively
+or
+.I passively
+in a connection.  A peer participating actively (an
+.IR "active peer" )
+must already know its peer's connection details \(en its server's IP
+address and port.  Active connection is suitable when the peer is a
+well-known server with stable details.
+.PP
+A server participating passively (a
+.IR "passive peer" )
+waits to be contacted by its peer, and discovers the peer's IP address
+and port as a result of a simple protocol described below.  Passive
+connection is suitable when the peer's IP address or port can vary over
+time \(en e.g., if its IP address is assigned dynamically by DHCP or
+PPP, or if it is hidden behind a NAT firewall.
+.PP
+If both peers are active, we say that they establish an
+.IR "static connection" ;
+if one is passive, we say that they establish a
+.IR "dynamic connection" .
+At least one of the peers must be active; it is not possible to
+establish a connection if both peers are passive.
+.SS "Command line"
+In addition to the standard options described in
+.BR tripe-service (7),
+the following command-line options are recognized.
+.TP
+.BI "\-p, \-\-peerdb=" file
+Use
+.I file
+as the (CDB format) peer database.  In the absence of this option, the
+file named by the
+.B TRIPEPEERDB
+environment variable is used; if that's not set either, then the default
+default of
+.B peers.cdb
+in the current working directory is used instead.
+.SS "Dynamic connection protocol"
+Dynamic connections are used when the peer's address or port are
+unknown, e.g., when it is hidden behind a NAT firewall.
+.PP
+The protocol for passive connection works as follows.
+.hP 1.
+The active peer
+.BR ADD s
+its partner, typically using the
+.B \-cork
+option to suppress the key-exchange message which the server usually
+sends immediately, since otherwise the passive peer will warn about it.
+.hP 2.
+The active peer somehow issues the command
+.RS
+.IP
+.B SVCSUBMIT connect passive
+.I user
+.PP
+to the passive peer's server.  (Here,
+.I user
+is a name identifying the active peer; see below.)  This may be handled
+by the
+.BR watch (8)
+service.
+.RE
+.hP 3.
+The
+.B connect
+service on the passive peer responds with a
+.I challenge
+\(en a short Base64-encoded string.  Somehow this challenge is sent back
+to the passive peer without being intercepted.
+.hP 4.
+The active peer sends a
+.BR GREET ing
+containing the challenge to its passive partner.  The passive server
+announces the arrival of this message, and the originating address and
+port.
+.hP 5.
+The
+.B connect
+service running on the passive host receives the notification, matches
+it up with the
+.I user
+from the initial connection request, and
+.BR ADD s
+the appropriate peer, with the address from the
+.BR GREET ing.
+.PP
+The
+.BR watch (8)
+service is capable of performing the active-peer part of this protocol,
+sending the correct
+.B GREET
+command once the challenge has been obtained.  The remaining difficulty
+is in collecting the challenge from the passive peer.
+.
+.\"--------------------------------------------------------------------------
+.SH "SERVICE COMMAND REFERENCE"
+.
+.\"* 10 Service commands
+The commands provided by the service are as follows.
+.SP
+.BI "active " peer
+Make an active connection to the named
+.IR peer .
+The service will submit the command
+.RS
+.IP
+.B ADD
+.RB [ \-cork ]
+.RB [ \-keepalive
+.IR time ]
+.RB [ \-tunnel
+.IR driver ]
+.I address
+.PP
+Specifically:
+.hP \*o
+The option
+.B \-cork
+is provided if the peer's database record assigns the
+.B cork
+key one of the values
+.BR t ,
+.BR true ,
+.BR y ,
+.BR yes,
+or
+.BR on .
+.hP \*o
+The option
+.B \-keepalive
+.I time
+is provided if the database record assigns a value
+.I time
+to the
+.B keepalive
+key.
+.hP \*o
+The option
+.B \-tunnel
+.I driver
+is provided if the database record assigns a value
+.I driver
+to the
+.B tunnel
+key.
+.hP \*o
+The
+.I address
+is the value assigned to the
+.B peer
+key in the database record.
+.RE
+.SP
+.BI "info " peer
+Lists the database record for the named
+.IR peer .
+For each key/value pair, a line
+.RS
+.IP
+.B INFO
+.IB key = value
+.PP
+is output.  The key/value pairs are output in an arbitrary order.
+.RE
+.TP
+.B "list"
+Output a list of peers in the database.  For each peer name
+.IR peer ,
+a line
+.RS
+.IP
+.B INFO
+.I peer
+.PP
+is output.
+.RE
+.SP
+.BI "passive \fR[" options "\fR]\fP " user
+If the database contains a user record mapping
+.I user
+to some
+.I peer
+then an
+.B INFO
+line is written containing a freshly chosen challenge string.  If the
+server receives a
+.BR GREET ing
+message quoting this challenge within 30 seconds, the
+.B connect
+service will issue an
+.B ADD
+request for the peer, as for the
+.B active
+command, except that the origin of the
+.BR GREET ing
+packet is used as the peer's address.
+.RS
+.\"+opts
+.PP
+The following option is recognized.
+.TP
+.BI "\-timeout " time
+Wait for
+.I time
+instead of 30 seconds.  The
+.I time
+is expressed as a non-negative integer followed by
+.BR d ,
+.BR h ,
+.BR m ,
+or
+.B s
+for days, hours, minutes or seconds respectively; if no suffix is given,
+seconds are assumed.
+.\"-opts
+.RE
+.
+.\"--------------------------------------------------------------------------
+.SH "ERROR MESSAGES"
+.
+.\"* 20 Error messages (FAIL codes)
+The following error codes may be reported.
+.SP
+.B "connect-timeout"
+(For
+.BR passive .)
+No
+.BR GREET ing
+was received within the timeout period (default 30 seconds).
+.SP
+.BI "malformed-peer " peer " missing-key " key
+The database record for
+.I peer
+has no value for the
+.I key
+but one was expected.
+.SP
+.BI "passive-peer " peer
+(For
+.BR active .)
+An active connection to
+.I peer
+was requested, but the database record indicates that it is passive,
+i.e., its
+.B peer
+key has the value
+.BR PASSIVE .
+.SP
+.BI "unknown-peer " peer
+The
+.I peer
+has no record in the database.
+.SP
+.BI "unknown-user " user
+(For
+.BR passive .)
+There is no record of
+.I user
+in the database.
+.
+.\"--------------------------------------------------------------------------
+.SH "WARNINGS"
+.
+.\"* 40 Warning broadcasts (WARN codes)
+All warnings issued by
+.B connect
+begin with the tokens
+.BR "USER connect" .
+.SP
+.BI "USER connect auto-add-failed " name " " error\fR...
+The attempt to add the peer
+.I name
+automatically failed: the
+.B ADD
+command reported
+.B FAIL
+.IR error ...
+.
+.\"--------------------------------------------------------------------------
+.SH "SUMMARY"
+.
+.\"= summary
+.
+.\"--------------------------------------------------------------------------
+.SH "SEE ALSO"
+.
+.BR tripe-service (7),
+.BR peers.in (5),
+.BR watch (8),
+.BR tripe (8).
+.
+.\"--------------------------------------------------------------------------
+.SH "AUTHOR"
+.
+Mark Wooding, <mdw@distorted.org.uk>
+.
+.\"----- That's all, folks --------------------------------------------------
diff --git a/svc/connect.in b/svc/connect.in
new file mode 100644 (file)
index 0000000..dae1162
--- /dev/null
@@ -0,0 +1,249 @@
+#! @PYTHON@
+### -*-python-*-
+###
+### Service for establishing dynamic connections
+###
+### (c) 2006 Straylight/Edgeware
+###
+
+###----- Licensing notice ---------------------------------------------------
+###
+### This file is part of Trivial IP Encryption (TrIPE).
+###
+### TrIPE 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.
+###
+### TrIPE 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 TrIPE; if not, write to the Free Software Foundation,
+### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+VERSION = '@VERSION@'
+
+###--------------------------------------------------------------------------
+### External dependencies.
+
+from optparse import OptionParser
+import tripe as T
+import os as OS
+import cdb as CDB
+import mLib as M
+from time import time
+
+S = T.svcmgr
+
+###--------------------------------------------------------------------------
+### Main service machinery.
+
+_magic = ['_magic']                     # An object distinct from all others
+
+class Peer (object):
+  """Representation of a peer in the database."""
+
+  def __init__(me, peer, cdb = None):
+    """
+    Create a new peer, named PEER.
+
+    Information about the peer is read from the database CDB, or the default
+    one given on the command-line.
+    """
+    me.name = peer
+    try:
+      record = (cdb or CDB.init(opts.cdb))['P' + peer]
+    except KeyError:
+      raise T.TripeJobError('unknown-peer', peer)
+    me.__dict__.update(M.URLDecode(record, semip = True))
+
+  def get(me, key, default = _magic):
+    """
+    Get the information stashed under KEY from the peer's database record.
+
+    If DEFAULT is given, then use it if the database doesn't contain the
+    necessary information.  If no DEFAULT is given, then report an error.
+    """
+    attr = me.__dict__.get(key, default)
+    if attr is _magic:
+      raise T.TripeJobError('malformed-peer', me.name, 'missing-key', key)
+    return attr
+
+  def list(me):
+    """
+    Iterate over the available keys in the peer's database record.
+    """
+    return me.__dict__.iterkeys()
+
+def addpeer(peer, addr):
+  """
+  Process a connect request from a new peer PEER on address ADDR.
+
+  Any existing peer with this name is disconnected from the server.
+  """
+  if peer.name in S.list():
+    S.kill(peer.name)
+  try:
+    S.add(peer.name,
+          tunnel = peer.get('tunnel', None),
+          keepalive = peer.get('keepalive', None),
+          cork = peer.get('cork', 'nil') in ['t', 'true', 'y', 'yes', 'on'],
+          *addr)
+  except T.TripeError, exc:
+    raise T.TripeJobError(*exc.args)
+
+def cmd_active(name):
+  """
+  active NAME: Handle an active connection request for the peer called NAME.
+
+  The appropriate address is read from the database automatically.
+  """
+  peer = Peer(name)
+  addr = peer.get('peer')
+  if addr == 'PASSIVE':
+    raise T.TripeJobError('passive-peer', name)
+  addpeer(peer, M.split(addr, quotep = True)[0])
+
+def cmd_list():
+  """
+  list: Report a list of the available active peers.
+  """
+  cdb = CDB.init(opts.cdb)
+  for key in cdb.keys():
+    if key.startswith('P') and Peer(key[1:]).get('peer', '') != 'PASSIVE':
+      T.svcinfo(key[1:])
+
+def cmd_info(name):
+  """
+  info NAME: Report the database entries for the named peer.
+  """
+  peer = Peer(name)
+  items = list(peer.list())
+  items.sort()
+  for i in items:
+    T.svcinfo('%s=%s' % (i, peer.get(i)))
+
+## Dictionary mapping challenges to waiting passive-connection coroutines.
+chalmap = {}
+
+def cmd_passive(*args):
+  """
+  passive [OPTIONS] USER: Await the arrival of the named USER.
+
+  Report a challenge; when (and if!) the server receives a greeting quoting
+  this challenge, add the corresponding peer to the server.
+  """
+  timeout = 30
+  op = T.OptParse(args, ['-timeout'])
+  for opt in op:
+    if opt == '-timeout':
+      timeout = T.timespec(op.arg())
+  user, = op.rest(1, 1)
+  try:
+    peer = CDB.init(opts.cdb)['U' + user]
+  except KeyError:
+    raise T.TripeJobError('unknown-user', user)
+  chal = S.getchal()
+  cr = T.Coroutine.getcurrent()
+  timer = M.SelTimer(time() + timeout, lambda: cr.switch(None))
+  try:
+    T.svcinfo(chal)
+    chalmap[chal] = cr
+    addr = cr.parent.switch()
+    if addr is None:
+      raise T.TripeJobError('connect-timeout')
+    addpeer(Peer(peer), addr)
+  finally:
+    del chalmap[chal]
+
+def notify(_, code, *rest):
+  """
+  Watch for notifications.
+
+  In particular, if a GREETing appears quoting a challenge in the chalmap
+  then wake up the corresponding coroutine.
+  """
+  if code != 'GREET':
+    return
+  chal = rest[0]
+  addr = rest[1:]
+  if chal in chalmap:
+    chalmap[chal].switch(addr)
+
+###--------------------------------------------------------------------------
+### Start up.
+
+def setup():
+  """
+  Service setup.
+
+  Register the notification-watcher, and add the automatic active peers.
+  """
+  S.handler['NOTE'] = notify
+  S.watch('+n')
+  if opts.startup:
+    cdb = CDB.init(opts.cdb)
+    try:
+      autos = cdb['%AUTO']
+    except KeyError:
+      autos = ''
+    for name in M.split(autos)[0]:
+      try:
+        peer = Peer(name, cdb)
+        addpeer(peer, M.split(peer.get('peer'), quotep = True)[0])
+      except T.TripeJobError, err:
+        S.warn('connect', 'auto-add-failed', name, *err.args)
+
+def parse_options():
+  """
+  Parse the command-line options.
+
+  Automatically changes directory to the requested configdir, and turns on
+  debugging.  Returns the options object.
+  """
+  op = OptionParser(usage = '%prog [-a FILE] [-d DIR]',
+                    version = '%%prog %s' % VERSION)
+
+  op.add_option('-a', '--admin-socket',
+                metavar = 'FILE', dest = 'tripesock', default = T.tripesock,
+                help = 'Select socket to connect to [default %default]')
+  op.add_option('-d', '--directory',
+                metavar = 'DIR', dest = 'dir', default = T.configdir,
+                help = 'Select current diretory [default %default]')
+  op.add_option('-p', '--peerdb',
+                metavar = 'FILE', dest = 'cdb', default = T.peerdb,
+                help = 'Select peers database [default %default]')
+  op.add_option('--daemon', dest = 'daemon',
+                default = False, action = 'store_true',
+                help = 'Become a daemon after successful initialization')
+  op.add_option('--debug', dest = 'debug',
+                default = False, action = 'store_true',
+                help = 'Emit debugging trace information')
+  op.add_option('--startup', dest = 'startup',
+                default = False, action = 'store_true',
+                help = 'Being called as part of the server startup')
+
+  opts, args = op.parse_args()
+  if args: op.error('no arguments permitted')
+  OS.chdir(opts.dir)
+  T._debug = opts.debug
+  return opts
+
+## Service table, for running manually.
+service_info = [('connect', VERSION, {
+  'passive': (1, None, '[OPTIONS] USER', cmd_passive),
+  'active': (1, 1, 'PEER', cmd_active),
+  'info': (1, 1, 'PEER', cmd_info),
+  'list': (0, 0, '', cmd_list)
+})]
+
+if __name__ == '__main__':
+  opts = parse_options()
+  T.runservices(opts.tripesock, service_info,
+                setup = setup,
+                daemon = opts.daemon)
+
+###----- That's all, folks --------------------------------------------------
diff --git a/svc/tripe-ifup.8.in b/svc/tripe-ifup.8.in
new file mode 100644 (file)
index 0000000..0fe89d8
--- /dev/null
@@ -0,0 +1,155 @@
+.\" -*-nroff-*-
+.\".
+.\" Manual for the watch service
+.\"
+.\" (c) 2008 Straylight/Edgeware
+.\"
+.
+.\"----- Licensing notice ---------------------------------------------------
+.\"
+.\" This file is part of Trivial IP Encryption (TrIPE).
+.\"
+.\" TrIPE 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.
+.\"
+.\" TrIPE 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 TrIPE; if not, write to the Free Software Foundation,
+.\" Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+.
+.\"--------------------------------------------------------------------------
+.so ../defs.man.in \"@@@PRE@@@
+.
+.\"--------------------------------------------------------------------------
+.TH tripe-ifup 8 "20 December 2008" "Straylight/Edgeware" "TrIPE: Trivial IP Encryption"
+.
+.\"--------------------------------------------------------------------------
+.SH "NAME"
+.
+tripe-ifup \- configure VPN network interfaces and routes
+.
+.\"--------------------------------------------------------------------------
+.SH "SYNOPSIS"
+.
+.B tripe-ifup
+.I peer
+.I ifname
+.I address-family
+.IR addr ...
+.
+.\"--------------------------------------------------------------------------
+.SH "DESCRIPTION"
+.
+The
+.B tripe-ifup
+program configures network interfaces and routes for
+.BR tripe (8).
+It expects a number of values to be passed as environment variables.  It
+is usually invoked by the
+.BR watch (8)
+service, which provides values for these environment variables by
+consulting the peer database
+.BR peers.cdb (5).
+These parameters are therefore described in terms of their keys in the
+peer's database record; the corresponding environment variable name is
+formed by converting letters to uppercase and prefixing with
+.RB ` P_ '.
+.PP
+The command-line arguments are as follows.
+.TP
+.I peer
+The name of the peer, as known to the
+.BR tripe (8)
+server and various services.  This is used to notify the server of
+changes, and to announce final success.
+.TP
+.I ifname
+The current name of the interface, as known to the kernel.
+.TP
+.IR address-family " and " addr
+The address, in the format described in
+.BR tripe-admin (5).
+Currently only the
+.B INET
+address family is supported.
+.SS Procedure
+In the following, a name in
+.I italics
+is used to represent the value of the correspondingly named key in the
+peer's record.  For example,then
+.I nets
+denotes the value assigned to the
+.B nets
+key, as passed in the
+.B T_NETS
+environment variable.
+.PP
+The network interface is configured as follows.
+.hP 1.
+The network interface name is set.  If
+.I ifname
+is set, then the network interface is renamed to
+.IR ifname ;
+a
+.B SETIFNAME
+command is issued to keep the server informed.  Further configuration is
+performed using the new interface name.
+.hP 2.
+The point-to-point interface is configured.  If
+.I laddr
+and
+.I raddr
+are set, then the interface is configured to be a point-to-point link
+from
+.I laddr
+to
+.IR raddr .
+Both are expected to be network addresses in dotted-quad form.  The
+interface MTU is configured based on the path MTU to the peer's external
+address and the cryptographic algorithms in use by the
+.BR tripe (8)
+server; this can be overridden by setting the
+.I mtu
+key.
+.hP 3.
+Establish routes.  If the interface was configured, and
+.I nets
+is set, then
+.I nets
+is split into space-separated networks.  For each network, of the form
+.IB address / mask \fR,
+a route is configured to the given network, via the remote address of
+the link, over the tunnel interface.
+.hP 4.
+Invoke user hook.  If
+.I ifupextra
+is set, it is interpreted as a Bourne shell command and evaluated.
+.hP 5.
+Notify services.  A notification
+.RS
+.IP
+.B USER tripe-ifup configured
+.I peer
+.PP
+is issued.
+.RE
+.
+.\"--------------------------------------------------------------------------
+.SH "SEE ALSO"
+.
+.BR peers.in (5),
+.BR watch (8),
+.BR tripe (8).
+.
+.\"--------------------------------------------------------------------------
+.SH "AUTHOR"
+.
+Mark Wooding, <mdw@distorted.org.uk>
+.
+.\"----- That's all, folks --------------------------------------------------
diff --git a/svc/tripe-ifup.in b/svc/tripe-ifup.in
new file mode 100644 (file)
index 0000000..fc902da
--- /dev/null
@@ -0,0 +1,94 @@
+#! /bin/sh
+###
+### TrIPE interface initialization script
+###   suitable for Linux; other operating systems probably want something
+###   similar
+
+###----- Licensing notica ---------------------------------------------------
+###
+### Redistribution, modification and use of this file is permitted without
+### limitation.
+###
+### This file 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.
+
+set -e
+
+: ${bindir=@bindir@}
+: ${tripectl=$bindir/tripectl}
+PATH=/usr/bin:/usr/sbin:/bin:/sbin:$bindir
+export PATH TRIPEDIR
+
+###--------------------------------------------------------------------------
+### Collect arguments.
+
+## Collect the simple arguments.
+if [ $# -lt 3 ]; then
+  echo >&2 "usage: $0 PEER IFNAME ADDR..."; exit 1
+fi
+peer=$1 ifname=$2 family=$3; shift 3
+
+## Parse the address family.
+case "$family,$#" in
+  INET,1) addr=$1 port=4070 ;;
+  INET,2) addr=$1 port=$2 ;;
+  INET,*) echo >&2 "$0: bad INET address"; exit 1 ;;
+  *)      echo >&2 "$0: unknown address family $family"; exit 1 ;;
+esac
+
+###--------------------------------------------------------------------------
+### Set the interface name.
+
+case "${P_IFNAME+set}" in
+  set)
+    ip link set "$ifname" name "$P_IFNAME"
+    ifname=$P_IFNAME
+    $tripectl setifname "$peer" "$ifname"
+    ;;
+esac
+
+###--------------------------------------------------------------------------
+### Configure the point-to-point link.
+
+ifup=no
+case "${P_LADDR+set},${P_RADDR+set}" in
+  set,set)
+    case "${P_MTU+set}" in
+      set) mtu=$P_MTU;;
+      *)
+       pathmtu=$(pathmtu "$addr")
+       mtu=$(expr "$pathmtu" - 33 - $A_CIPHER_BLKSZ - $A_MAC_TAGSZ)
+       ;;
+    esac
+    ifconfig "$ifname" "$P_LADDR" pointopoint "$P_RADDR" up mtu "$mtu"
+    ifup=yes
+    ;;
+esac
+
+###--------------------------------------------------------------------------
+### Set up routing.
+
+case "$ifup,${P_NETS+set}" in
+  yes,set)
+    for net in $P_NETS; do
+      route add -net $net gw "$P_RADDR" dev "$ifname" metric 2
+    done
+    ;;
+esac
+
+###--------------------------------------------------------------------------
+### Maybe invoke a follow-on script.
+
+case "${P_IFUPEXTRA+set}" in
+  set)
+    eval "$P_IFUPEXTRA"
+    ;;
+esac
+
+###--------------------------------------------------------------------------
+### Issue a notification that we've won.
+
+$tripectl notify tripe-ifup configured "$peer"
+
+###----- That's all, folks --------------------------------------------------
diff --git a/svc/watch.8.in b/svc/watch.8.in
new file mode 100644 (file)
index 0000000..3e40251
--- /dev/null
@@ -0,0 +1,480 @@
+.\" -*-nroff-*-
+.\".
+.\" Manual for the watch service
+.\"
+.\" (c) 2008 Straylight/Edgeware
+.\"
+.
+.\"----- Licensing notice ---------------------------------------------------
+.\"
+.\" This file is part of Trivial IP Encryption (TrIPE).
+.\"
+.\" TrIPE 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.
+.\"
+.\" TrIPE 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 TrIPE; if not, write to the Free Software Foundation,
+.\" Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+.
+.\"--------------------------------------------------------------------------
+.so ../defs.man.in \"@@@PRE@@@
+.
+.\"--------------------------------------------------------------------------
+.TH watch 8 "11 December 2007" "Straylight/Edgeware" "TrIPE: Trivial IP Encryption"
+.
+.\"--------------------------------------------------------------------------
+.SH "NAME"
+.
+watch \- tripe service handle addition and removal of peers
+.
+.\"--------------------------------------------------------------------------
+.SH "SYNOPSIS"
+.
+.B watch
+.RB [ \-a
+.IR socket ]
+.RB [ \-d
+.IR dir ]
+.RB [ \-p
+.IR file ]
+.br
+\&     \c
+.RB [ \-\-daemon ]
+.RB [ \-\-debug ]
+.RB [ \-\-startup ]
+.
+.\"--------------------------------------------------------------------------
+.SH "DESCRIPTION"
+.
+The
+.B watch
+service tracks associations with peers and performs various actions at
+appropriate stages in the assocations' lifecycles.
+.PP
+For example:
+.hP \*o
+When a peer is added, it arranges to configure the corresponding network
+interface correctly, and (if necessary) to initiate a dynamic
+connection.
+.hP \*o
+When a peer is removed, it arranges to bring down the network interface.
+.hP \*o
+While the peer is known, it
+.BR PING s
+it at regular intervals.  If the peer fails to respond, it can be
+removed or reconnected.
+.SS "Command line"
+In addition to the standard options described in
+.BR tripe-service (7),
+the following command-line options are recognized.
+.TP
+.BI "\-p, \-\-peerdb=" file
+Use
+.I file
+as the (CDB format) peer database.  In the absence of this option, the
+file named by the
+.B TRIPEPEERDB
+environment variable is used; if that's not set either, then the default
+default of
+.B peers.cdb
+in the current working directory is used instead.
+.
+.\"--------------------------------------------------------------------------
+.SH "BEHAVIOUR"
+.
+.SS "Adoption"
+The
+.B watch
+service maintains a list of peers which it has adopted.  A peer is
+.I eligible for adoption
+if it has a record in the peer database
+.BR peers.cdb (5)
+in which the
+.B watch
+key is assigned the value
+.BR t ,
+.BR true ,
+.BR y ,
+.BR yes ,
+or
+.BR on .
+.PP
+The service pings adopted peers periodically in order to ensure that
+they are alive, and takes appropriate action if no replies are received.
+.PP
+A peer is said to be
+.I adopted
+when it is added to this list, and
+.I disowned
+when it removed.
+.SS "Configuring interfaces"
+The
+.B watch
+service configures network interfaces by invoking an
+.B ifup
+script.  The script is invoked as
+.IP
+.I script
+.IR args ...
+.I peer
+.I ifname
+.IR addr ...
+.PP
+where the elements are as described below.
+.TP
+.IR script " and " args
+The peer's database record is retrieved; the value assigned to the
+.B ifup
+key is split into words (quoting is allowed; see
+.BR tripe-admin (5)
+for details).  The first word is the
+.IR script ;
+subsequent words are gathered to form the
+.IR args .
+.TP
+.I peer
+The name of the peer.
+.TP
+.I ifname
+The name of the network interface associated with the peer, as returned
+by the
+.B IFNAME
+administration command (see
+.BR tripe-admin (5)).
+.TP
+.I addr
+The network address of the peer's TrIPE server, in the form output by
+the
+.B ADDR
+administration command (see
+.BR tripe-admin (5)).
+The first word of
+.I addr
+is therefore a network address family, e.g.,
+.BR INET .
+.PP
+The
+.B watch
+service deconfigures interfaces by invoking an
+.B ifdown
+script, in a similar manner.  The script is invoked as
+.IP
+.I script
+.IR args ...
+.I peer
+.PP
+where the elements are as above, except that
+.I script
+and
+.I args
+are formed by splitting the value associated with the peer record's
+.B ifdown
+key.
+.PP
+In both of the above cases, if the relevant key (either
+.B ifup
+or
+.BR ifdown )
+is absent, no action is taken.
+.PP
+The key/value pairs in the peer's database record and the server's
+response to the
+.B ALGS
+administration command (see
+.BR tripe-admin (5))
+are passed to the
+.B ifup
+and
+.B ifdown
+scripts as environment variables.  The environment variable name
+corresponding to a key is determined as follows:
+.hP \*o
+Convert all letters to upper-case.
+.hP \*o Convert all sequences of one or more non-alphanumeric characters
+to an underscore
+.RB ` _ '.
+.hP \*o Prefix the resulting name by
+.RB ` P_ '
+or
+.RB ` A_ '
+depending on whether it came from the peer's database record or the
+.B ALGS
+output respectively.
+.PP
+For example,
+.B ifname
+becomes
+.BR P_IFNAME ;
+and
+.B cipher-blksz
+becomes
+.BR A_CIPHER_BLKSZ .
+.SS "Dynamic connection"
+If a peer's database record assigns a value to the
+.B connect
+key, then the
+.B watch
+service will attempt to establish a connection dynamically with the
+peer.  The value of the
+.B connect
+key is invoked as a Bourne shell command, i.e.,
+.IP
+.B /bin/sh \-c
+.I connect
+.PP
+is executed.  The command is expected to contact the remote server and
+report, on standard output, a challenge string.  The
+.B watch
+service reads this challenge, and submits the command
+.IP
+.B GREET
+.I peer
+.I challenge
+.PP
+Typically, the
+.B connect
+command will issue a command such as
+.IP
+.B SVCSUBMIT connect passive
+.I our-name
+.PP
+where
+.I our-name
+is the remote peer's name for this host.
+.SS "Operation"
+On startup,
+.B watch
+requests a list of current peers from the
+.BR tripe (8)
+server, and adopts any eligible peers.  If the
+.B \-\-startup
+flag was passed on the command line, 
+the newly adopted peers have their interfaces configured and connection
+attempts are made.
+.PP
+Adopted peers are pinged at regular intervals (using the
+.B PING
+administrative command; see
+.BR tripe-admin (5)).
+This process can be configured by assigning values to keys in the peer's
+database record.  Some of these parameters are time intervals,
+expressed as a nonnegative integer followed optionally by
+.BR d ,
+.BR h ,
+.BR m ,
+or
+.B s
+for days, hours, minutes, or seconds, respectively; if no suffix is
+given, seconds are assumed.
+.PP
+The parameters are as follows.
+.TP
+.B every
+A time interval: how often to ping the peer to ensure that it's still
+alive.  The default is 2 minutes.
+.TP
+.B timeout
+A time interval: how long to wait for a reply before retrying or giving
+up.  The default is 10 seconds.
+.TP
+.B retries
+An integer: how many failed attempts to make before deciding that the
+peer is unreachable and taking action.  The default is 5 attempts.
+.PP
+The algorithm is as follows.  Send up to
+.I retries
+pings; if a reply is received before the
+.I timeout
+then the peer is alive; wait
+.I every
+and check again.  If no reply is received within the
+.IR timeout ,
+then try again up to
+.I retries
+times.  If no attempt succeeds, the peer is declared unreachable.  If
+the peer has a
+.B connect
+command (i.e., it connects dynamically) then another connection attempt
+is made.  Otherwise the peer is killed.
+.
+.\"--------------------------------------------------------------------------
+.SH "SERVICE COMMAND REFERENCE"
+.
+.\"* 10 Service commands
+The commands provided by the service are as follows.
+.SP
+.B adopted
+For each peer being tracked by the
+.B watch
+service, write a line
+.B INFO
+.IR name .
+(Compatibility note: it's possible that further information will be
+provided about each peer, in the form of subsequent tokens.  Clients
+should be prepared to ignore such tokens.)
+.SP
+.BI "kick " peer
+If
+.I peer
+is currently added, and its record in the peer database contains a
+.B connect
+key (see
+.BR peers.in )
+then force a reconnection attempt.  See
+.BR "Dynamic connection" .
+.
+.\"--------------------------------------------------------------------------
+.SH "NOTIFICATIONS"
+.
+.\"* 30 Notification broadcasts (NOTE codes)
+All notifications issued by
+.B watch
+begin with the tokens
+.BR "USER watch" .
+.SP
+.B "USER watch peerdb-update"
+The peer database has changed.  Other interested clients should reopen
+the database.
+.SP
+.BI "USER watch ping-failed " peer " " error\fR...
+An attempt to
+.B PING
+the named
+.I peer
+failed; the server replied
+.B FAIL
+.IR error ...
+.SP
+.BI "USER watch " process\fR... " stdout " line
+The
+.I process
+spawned by the
+.B watch
+service unexpectedly wrote
+.I line
+to its standard output.
+.
+.\"--------------------------------------------------------------------------
+.SH "WARNINGS"
+.
+.\"* 40 Warning broadcasts (WARN codes)
+All warnings issued by
+.B watch
+begin with the tokens
+.BR "USER watch" .
+.SP
+.BI "USER watch ping-ok " peer
+A reply was received to a
+.B PING
+sent to the
+.IR peer ,
+though earlier attempts had failed.
+.SP
+.BI "USER watch ping-timeout " peer " attempt " i " of " n
+No reply was received to a
+.B PING
+sent to the
+.IR peer .
+So far,
+.I i
+.BR PING s
+have been sent; if a total of
+.I n
+consecutive attempts time out, the
+.B watch
+service will take further action.
+.SP
+.B "USER watch reconnecting " peer
+The dynamically connected
+.I peer
+seems to be unresponsive.  The
+.B watch
+service will attempt to reconnect.
+.SP
+.BI "USER watch " process\fR... " stderr " line
+The
+.I process
+spawned by the
+.B watch
+service wrote
+.I line
+to its standard error.
+.SP
+.BI "USER watch " process\fR... " exit-nonzero " code
+The
+.I process
+spawned by the
+.B watch
+service exited with the nonzero status
+.IR code .
+.SP
+.BI "USER watch " process\fR... " exit-signal S" code
+The
+.I process
+spawned by the
+.B watch
+service was killed by signal
+.IR code .
+Here,
+.I code
+is the numeric value of the fatal signal.
+.SP
+.BI "USER watch " process\fR... " exit-unknown " status
+The
+.I process
+spawned by the
+.B watch
+service exited with an unknown
+.IR status .
+Here,
+.I status
+is the raw exit status, as returned by
+.BR waitpid (2),
+in hexadecimal.
+.
+.\"--------------------------------------------------------------------------
+.SH "CHILD PROCESS IDENTIFIERS"
+.
+.\"* 50 Child process identifiers
+Some of the warnings and notifications refer to processes spawned by
+.B watch
+under various circumstances.  The process identifiers are as follows.
+.SP
+.BI "connect " peer
+A child spawned in order to establish a dynamic connection with
+.IR peer .
+.SP
+.BI "ifdown " peer
+A child spawned to deconfigure the network interface for
+.IR peer .
+.SP
+.BI "ifup " peer
+A child spawned to configure the network interface for
+.IR peer .
+.
+.\"--------------------------------------------------------------------------
+.SH "SUMMARY"
+.
+.\"= summary
+.
+.\"--------------------------------------------------------------------------
+.SH "SEE ALSO"
+.
+.BR tripe-service (7),
+.BR peers.in (5),
+.BR connect (8),
+.BR tripe (8).
+.
+.\"--------------------------------------------------------------------------
+.SH "AUTHOR"
+.
+Mark Wooding, <mdw@distorted.org.uk>
+.
+.\"----- That's all, folks --------------------------------------------------
diff --git a/svc/watch.in b/svc/watch.in
new file mode 100644 (file)
index 0000000..bfad160
--- /dev/null
@@ -0,0 +1,734 @@
+#! @PYTHON@
+### -*-python-*-
+###
+### Watch arrival and departure of peers
+###
+### (c) 2007 Straylight/Edgeware
+###
+
+###----- Licensing notice ---------------------------------------------------
+###
+### This file is part of Trivial IP Encryption (TrIPE).
+###
+### TrIPE 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.
+###
+### TrIPE 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 TrIPE; if not, write to the Free Software Foundation,
+### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+VERSION = '@VERSION@'
+
+###--------------------------------------------------------------------------
+### External dependencies.
+
+from optparse import OptionParser
+import tripe as T
+import os as OS
+import signal as SIG
+import errno as E
+import cdb as CDB
+import mLib as M
+import re as RX
+from time import time
+import subprocess as PROC
+
+S = T.svcmgr
+
+###--------------------------------------------------------------------------
+### Running auxiliary commands.
+
+class SelLineQueue (M.SelLineBuffer):
+  """Glues the select-line-buffer into the coroutine queue system."""
+
+  def __new__(cls, file, queue, tag, kind):
+    """See __init__ for documentation."""
+    return M.SelLineBuffer.__new__(cls, file.fileno())
+
+  def __init__(me, file, queue, tag, kind):
+    """
+    Initialize a new line-reading adaptor.
+
+    The adaptor reads lines from FILE.  Each line is inserted as a message of
+    the stated KIND, bearing the TAG, into the QUEUE.  End-of-file is
+    represented as None.
+    """
+    me._q = queue
+    me._file = file
+    me._tag = tag
+    me._kind = kind
+    me.enable()
+
+  @T._callback
+  def line(me, line):
+    me._q.put((me._tag, me._kind, line))
+
+  @T._callback
+  def eof(me):
+    me.disable()
+    me._q.put((me._tag, me._kind, None))
+
+class ErrorWatch (T.Coroutine):
+  """
+  An object which watches stderr streams for errors and converts them into
+  warnings of the form
+
+    WARN watch INFO stderr LINE
+
+  The INFO is a list of tokens associated with the file when it was
+  registered.
+
+  Usually there is a single ErrorWatch object, called errorwatch.
+  """
+
+  def __init__(me):
+    """Initialization: there are no arguments."""
+    T.Coroutine.__init__(me)
+    me._q = T.Queue()
+    me._map = {}
+    me._seq = 1
+
+  def watch(me, file, info):
+    """
+    Adds FILE to the collection of files to watch.
+
+    INFO will be written in the warning messages from this FILE.  Returns a
+    sequence number which can be used to unregister the file again.
+    """
+    seq = me._seq
+    me._seq += 1
+    me._map[seq] = info, SelLineQueue(file, me._q, seq, 'stderr')
+    return seq
+
+  def unwatch(me, seq):
+    """Stop watching the file with sequence number SEQ."""
+    del me._map[seq]
+    return me
+
+  def run(me):
+    """
+    Coroutine function: read items from the queue and report them.
+
+    Unregisters files automatically when they reach EOF.
+    """
+    while True:
+      seq, _, line = me._q.get()
+      if line is None:
+        me.unwatch(seq)
+      else:
+        S.warn(*['watch'] + me._map[seq][0] + ['stderr', line])
+
+def dbwatch():
+  """
+  Coroutine function: wake up every second and notice changes to the
+  database.  When a change happens, tell the Pinger (q.v.) to rescan its
+  peers.
+  """
+  cr = T.Coroutine.getcurrent()
+  main = cr.parent
+  fw = M.FWatch(opts.cdb)
+  while True:
+    timer = M.SelTimer(time() + 1, lambda: cr.switch())
+    main.switch()
+    if fw.update():
+      pinger.rescan(False)
+      S.notify('watch', 'peerdb-update')
+
+class ChildWatch (M.SelSignal):
+  """
+  An object which watches for specified processes exiting and reports
+  terminations by writing items of the form (TAG, 'exit', RESULT) to a queue.
+
+  There is usually only one ChildWatch object, called childwatch.
+  """
+
+  def __new__(cls):
+    """Initialize the child-watcher."""
+    return M.SelSignal.__new__(cls, SIG.SIGCHLD)
+
+  def __init__(me):
+    """Initialize the child-watcher."""
+    me._pid = {}
+    me.enable()
+
+  def watch(me, pid, queue, tag):
+    """
+    Register PID as a child to watch.  If it exits, write (TAG, 'exit', CODE)
+    to the QUEUE, where CODE is one of
+
+      * None (successful termination)
+      * ['exit-nonzero', CODE] (CODE is a string!)
+      * ['exit-signal', 'S' + CODE] (CODE is the signal number as a string)
+      * ['exit-unknown', STATUS] (STATUS is the entire exit status, in hex)
+    """
+    me._pid[pid] = queue, tag
+    return me
+
+  def unwatch(me, pid):
+    """Unregister PID as a child to watch."""
+    del me._pid[pid]
+    return me
+
+  @T._callback
+  def signalled(me):
+    """
+    Called when child processes exit: collect exit statuses and report
+    failures.
+    """
+    while True:
+      try:
+        pid, status = OS.waitpid(-1, OS.WNOHANG)
+      except OSError, exc:
+        if exc.errno == E.ECHILD:
+          break
+      if pid == 0:
+        break
+      if pid not in me._pid:
+        continue
+      queue, tag = me._pid[pid]
+      if OS.WIFEXITED(status):
+        exit = OS.WEXITSTATUS(status)
+        if exit == 0:
+          code = None
+        else:
+          code = ['exit-nonzero', str(exit)]
+      elif OS.WIFSIGNALED(status):
+        code = ['exit-signal', 'S' + str(OS.WTERMSIG(status))]
+      else:
+        code = ['exit-unknown', hex(status)]
+      queue.put((tag, 'exit', code))
+
+class Command (object):
+  """
+  Represents a running command.
+
+  This class is the main interface to the machery provided by the ChildWatch
+  and ErrorWatch objects.  See also potwatch.
+  """
+
+  def __init__(me, info, queue, tag, args, env):
+    """
+    Start a new child process.
+
+    The ARGS are a list of arguments to be given to the child process.  The
+    ENV is either None or a dictionary of environment variable assignments to
+    override the extant environment.  INFO is a list of tokens to be included
+    in warnings about the child's stderr output.  If the child writes a line
+    to standard output, put (TAG, 'stdout', LINE) to the QUEUE.  When the
+    child exits, write (TAG, 'exit', CODE) to the QUEUE.
+    """
+    me._info = info
+    me._q = queue
+    me._tag = tag
+    myenv = OS.environ.copy()
+    if env: myenv.update(env)
+    me._proc = PROC.Popen(args = args, env = myenv, bufsize = 1,
+                          stdout = PROC.PIPE, stderr = PROC.PIPE)
+    me._lq = SelLineQueue(me._proc.stdout, queue, tag, 'stdout')
+    errorwatch.watch(me._proc.stderr, info)
+    childwatch.watch(me._proc.pid, queue, tag)
+
+  def __del__(me):
+    """
+    If I've been forgotten then stop watching for termination.
+    """
+    childwatch.unwatch(me._proc.pid)
+
+def potwatch(what, name, q):
+  """
+  Watch the queue Q for activity as reported by a Command object.
+
+  Information from the process's stdout is reported as
+
+    NOTE WHAT NAME stdout LINE
+
+  abnormal termination is reported as
+
+    WARN WHAT NAME CODE
+
+  where CODE is what the ChildWatch wrote.
+  """
+  eofp = deadp = False
+  while not deadp or not eofp:
+    _, kind, more = q.get()
+    if kind == 'stdout':
+      if more is None:
+        eofp = True
+      else:
+        S.notify('watch', what, name, 'stdout', more)
+    elif kind == 'exit':
+      if more: S.warn('watch', what, name, *more)
+      deadp = True
+
+###--------------------------------------------------------------------------
+### Peer database utilities.
+
+def timespec(info, key, default):
+  """Parse INFO[KEY] as a timespec, or return DEFAULT."""
+  try:
+    return T.timespec(info[key])
+  except (KeyError, T.TripeJobError):
+    return default
+
+def integer(info, key, default):
+  """Parse INFO[KEY] as an integer, or return DEFAULT."""
+  try:
+    return int(info[key])
+  except (KeyError, ValueError):
+    return default
+
+def boolean(info, key, default):
+  """Parse INFO[KEY] as a boolean, or return DEFAULT."""
+  try:
+    return info[key] in ['t', 'true', 'y', 'yes', 'on']
+  except (KeyError, ValueError):
+    return default
+
+def peerinfo(peer):
+  """
+  Return a dictionary containing information about PEER from the database.
+  """
+  return dict(M.URLDecode(CDB.init(opts.cdb)['P' + peer], semip = True))
+
+###--------------------------------------------------------------------------
+### Waking up and watching peers.
+
+def connect(peer, conn = None):
+  """
+  Start the job of connecting to the passive PEER.
+
+  The CONN string is a shell command which will connect to the peer (via some
+  back-channel, say ssh and userv), issue a command
+
+    SVCSUBMIT connect passive [OPTIONS] USER
+
+  and write the resulting challenge to standard error.
+  """
+  if conn is None:
+    try:
+      conn = peerinfo(peer)['connect']
+    except KeyError:
+      return
+  q = T.Queue()
+  cmd = Command(['connect', peer], q, 'connect',
+                ['/bin/sh', '-c', conn], None)
+  _, kind, more = q.peek()
+  if kind == 'stdout':
+    if more is None:
+      S.warn('watch', 'connect', peer, 'unexpected-eof')
+    else:
+      chal = more
+      S.greet(peer, chal)
+      q.get()
+  potwatch('connect', peer, q)
+
+_pingseq = 0
+class PingPeer (object):
+  """
+  Object representing a peer which we are pinging to ensure that it is still
+  present.
+
+  PingPeer objects are held by the Pinger (q.v.).  The Pinger maintains an
+  event queue -- which saves us from having an enormous swarm of coroutines
+  -- but most of the actual work is done here.
+
+  In order to avoid confusion between different PingPeer instances for the
+  same actual peer, each PingPeer has a sequence number (its `seq'
+  attribute).  Events for the PingPeer are identified by a (PEER, SEQ) pair.
+  (Using the PingPeer instance itself will prevent garbage collection of
+  otherwise defunct instances.)
+  """
+
+  def __init__(me, pinger, queue, peer, info, pingnow):
+    """
+    Create a new PingPeer.
+
+    The PINGER is the Pinger object we should send the results to.  This is
+    used when we remove ourselves, if the peer has been explicitly removed.
+
+    The QUEUE is the event queue on which timer and ping-command events
+    should be written.
+
+    The PEER is just the peer's name, as a string.
+
+    The INFO is the database record for the peer, as a dictionary, or None if
+    it's not readily available.  (This is just a tweak to save multiple
+    probes if we don't really need them.)
+
+    If PINGNOW is true, then immediately start pinging the peer.  Otherwise
+    wait until the usual retry interval.
+    """
+    global _pingseq
+    me._pinger = pinger
+    me._q = queue
+    me._peer = peer
+    me.update(info)
+    me.seq = _pingseq
+    _pingseq += 1
+    me._failures = 0
+    if pingnow:
+      me._timer = None
+      me._ping()
+    else:
+      me._timer = M.SelTimer(time() + me._every, me._time)
+
+  def update(me, info):
+    """
+    Refreshes the timer parameters for this peer.  We don't, however,
+    immediately reschedule anything: that will happen next time anything
+    interesting happens.
+    """
+    if info is None:
+      info = peerinfo(me._peer)
+    me._every = timespec(info, 'every', 120)
+    me._timeout = timespec(info, 'timeout', 10)
+    me._retries = integer(info, 'retries', 5)
+    me._connectp = 'connect' in info
+    return me
+
+  def _ping(me):
+    """
+    Send a ping to the peer; the result is sent to the Pinger's event queue.
+    """
+    S.rawcommand(T.TripeAsynchronousCommand(
+      me._q, (me._peer, me.seq),
+      ['PING',
+       '-background', S.bgtag(),
+       '-timeout', str(me._timeout),
+       '--',
+       me._peer]))
+
+  def event(me, code, stuff):
+    """
+    Respond to an event which happened to this peer.
+
+    Timer events indicate that we should start a new ping.  (The server has
+    its own timeout which detects lost packets.)
+
+    We trap unknown-peer responses and detach from the Pinger.
+
+    If the ping fails and we run out of retries, we attempt to restart the
+    connection.
+    """
+    if code == 'TIMER':
+      me._failures = 0
+      me._ping()
+    elif code == 'FAIL':
+      S.notify('watch', 'ping-failed', me._peer, *stuff)
+      if stuff and stuff[0] == 'unknown-peer':
+        me._pinger.kill(me._peer)
+    elif code == 'INFO':
+      if stuff[0] == 'ping-ok':
+        if me._failures > 0:
+          S.warn('watch', 'ping-ok', me._peer)
+        me._timer = M.SelTimer(time() + me._every, me._time)
+      elif stuff[0] == 'ping-timeout':
+        me._failures += 1
+        S.warn('watch', 'ping-timeout', me._peer,
+               'attempt', str(me._failures), 'of', str(me._retries))
+        if me._failures < me._retries:
+          me._ping()
+        else:
+          info = peerinfo(me._peer)
+          if 'connect' in info:
+            S.warn('watch', 'reconnecting', me._peer)
+            S.forcekx(me._peer)
+            T.spawn(T.Coroutine(connect), me._peer)
+            me._timer = M.SelTimer(time() + me._every, me._time)
+          else:
+            S.kill(me._peer)
+      elif stuff[0] == 'ping-peer-died':
+        me._pinger.kill(me._peer)
+
+  @T._callback
+  def _time(me):
+    """
+    Handle timer callbacks by posting a timeout event on the queue.
+    """
+    me._timer = None
+    me._q.put(((me._peer, me.seq), 'TIMER', None))
+
+  def __str__(me):
+    return 'PingPeer(%s, %d, f = %d)' % (me._peer, me.seq, me._failures)
+  def __repr__(me):
+    return str(me)
+
+class Pinger (T.Coroutine):
+  """
+  The Pinger keeps track of the peers which we expect to be connected and
+  takes action if they seem to stop responding.
+
+  There is usually only one Pinger, called pinger.
+
+  The Pinger maintains a collection of PingPeer objects, and an event queue.
+  The PingPeers direct the results of their pings, and timer events, to the
+  event queue.  The Pinger's coroutine picks items off the queue and
+  dispatches them back to the PingPeers as appropriate.
+  """
+
+  def __init__(me):
+    """Initialize the Pinger."""
+    T.Coroutine.__init__(me)
+    me._peers = {}
+    me._q = T.Queue()
+
+  def run(me):
+    """
+    Coroutine function: reads the pinger queue and sends events to the
+    PingPeer objects they correspond to.
+    """
+    while True:
+      (peer, seq), code, stuff = me._q.get()
+      if peer in me._peers and seq == me._peers[peer].seq:
+        me._peers[peer].event(code, stuff)
+
+  def add(me, peer, info, pingnow):
+    """
+    Add PEER to the collection of peers under the Pinger's watchful eye.
+    The arguments are as for PingPeer: see above.
+    """
+    me._peers[peer] = PingPeer(me, me._q, peer, info, pingnow)
+    return me
+
+  def kill(me, peer):
+    """Remove PEER from the peers being watched by the Pinger."""
+    del me._peers[peer]
+    return me
+
+  def rescan(me, startup):
+    """
+    General resynchronization method.
+
+    We scan the list of peers (with connect scripts) known at the server.
+    Any which are known to the Pinger but aren't known to the server are
+    removed from our list; newly arrived peers are added.  (Note that a peer
+    can change state here either due to the server sneakily changing its list
+    without issuing notifications or, more likely, the database changing its
+    idea of whether a peer is interesting.)  Finally, PingPeers which are
+    still present are prodded to update their timing parameters.
+
+    This method is called once at startup to pick up the peers already
+    installed, and again by the dbwatcher coroutine when it detects a change
+    to the database.
+    """
+    correct = {}
+    for peer in S.list():
+      try:
+        info = peerinfo(peer)
+      except KeyError:
+        continue
+      if boolean(info, 'watch', False):
+        correct[peer] = info
+    for peer, obj in me._peers.items():
+      if peer in correct:
+        obj.update(correct[peer])
+      else:
+        del me._peers[peer]
+    for peer, info in correct.iteritems():
+      if peer not in me._peers:
+        if startup:
+          ifname = S.ifname(peer)
+          addr = S.addr(peer)
+          addpeer(info, peer, ifname, *addr)
+        else:
+          me.add(peer, info, True)
+    return me
+
+  def adopted(me):
+    """
+    Returns the list of peers being watched by the Pinger.
+    """
+    return me._peers.keys()
+
+###--------------------------------------------------------------------------
+### New connections.
+
+def encode_envvars(env, prefix, vars):
+  """
+  Encode the variables in VARS suitably for including in a program
+  environment.  Lowercase letters in variable names are forced to uppercase;
+  runs of non-alphanumeric characters are replaced by single underscores; and
+  the PREFIX is prepended.  The resulting variables are written to ENV.
+  """
+  for k, v in vars.iteritems():
+    env[prefix + r_bad.sub('_', k.upper())] = v
+
+r_bad = RX.compile(r'[\W_]+')
+def envvars(info):
+  """
+  Translate the database INFO dictionary for a peer into a dictionary of
+  environment variables with plausible upper-case names and a P_ prefix.
+  Also collect the crypto information into A_ variables.
+  """
+  env = {}
+  encode_envvars(env, 'P_', info)
+  encode_envvars(env, 'A_', S.algs())
+  return env
+
+def ifupdown(what, peer, info, *args):
+  """
+  Run the interface up/down script for a peer.
+
+  WHAT is 'ifup' or 'ifdown'.  PEER names the peer in question.  INFO is the
+  database record dictionary.  ARGS is a list of arguments to pass to the
+  script, in addition to the peer name.
+
+  The command is run and watched in the background by potwatch.
+  """
+  q = T.Queue()
+  c = Command([what, peer], q, what,
+              M.split(info[what], quotep = True)[0] +
+              [peer] + list(args),
+              envvars(info))
+  potwatch(what, peer, q)
+
+def addpeer(info, peer, ifname, *addr):
+  """
+  Add a new peer to our collection.
+
+  INFO is the peer information dictionary, or None if we don't have one yet.
+
+  PEER names the peer; IFNAME is the interface name for its tunnel; and ADDR
+  is the list of tokens representing its address.
+
+  We try to bring up the interface and provoke a connection to the peer if
+  it's passive.
+  """
+  if info is None:
+    try:
+      info = peerinfo(peer)
+    except KeyError:
+      return
+  if 'ifup' in info:
+    T.Coroutine(ifupdown).switch('ifup', peer, info, ifname, *addr)
+  if 'connect' in info:
+    T.Coroutine(connect).switch(peer, info['connect'])
+  if boolean(info, 'watch', False):
+    pinger.add(peer, info, False)
+
+def delpeer(peer):
+  """Drop the PEER from the Pinger and put its interface to bed."""
+  try:
+    info = peerinfo(peer)
+  except KeyError:
+    return
+  try:
+    pinger.kill(peer)
+  except KeyError:
+    pass
+  if 'ifdown' in info:
+    T.Coroutine(ifupdown).switch('ifdown', peer, info)
+
+def notify(_, code, *rest):
+  """
+  Watch for notifications.
+
+  We trap ADD and KILL notifications, and send them straight to addpeer and
+  delpeer respectively.
+  """
+  if code == 'ADD':
+    addpeer(None, *rest)
+  elif code == 'KILL':
+    delpeer(*rest)
+
+###--------------------------------------------------------------------------
+### Command stubs.
+
+def cmd_stub(*args):
+  raise T.TripeJobError('not-implemented')
+
+def cmd_kick(peer):
+  """
+  kick PEER: Force a new connection attempt for PEER
+  """
+  if peer not in pinger.adopted():
+    raise T.TripeJobError('peer-not-adopted', peer)
+  T.spawn(T.Coroutine(connect), peer)
+
+def cmd_adopted():
+  """
+  adopted: Report a list of adopted peers.
+  """
+  for peer in pinger.adopted():
+    T.svcinfo(peer)
+
+###--------------------------------------------------------------------------
+### Start up.
+
+def setup():
+  """
+  Service setup.
+
+  Register the notification watcher, and rescan the peers.
+  """
+  S.handler['NOTE'] = notify
+  S.watch('+n')
+  pinger.rescan(opts.startup)
+
+def init():
+  """
+  Initialization to be done before service startup.
+  """
+  global errorwatch, childwatch, pinger
+  errorwatch = ErrorWatch()
+  childwatch = ChildWatch()
+  pinger = Pinger()
+  T.Coroutine(dbwatch).switch()
+  errorwatch.switch()
+  pinger.switch()
+
+def parse_options():
+  """
+  Parse the command-line options.
+
+  Automatically changes directory to the requested configdir, and turns on
+  debugging.  Returns the options object.
+  """
+  op = OptionParser(usage = '%prog [-a FILE] [-d DIR]',
+                    version = '%%prog %s' % VERSION)
+
+  op.add_option('-a', '--admin-socket',
+                metavar = 'FILE', dest = 'tripesock', default = T.tripesock,
+                help = 'Select socket to connect to [default %default]')
+  op.add_option('-d', '--directory',
+                metavar = 'DIR', dest = 'dir', default = T.configdir,
+                help = 'Select current diretory [default %default]')
+  op.add_option('-p', '--peerdb',
+                metavar = 'FILE', dest = 'cdb', default = T.peerdb,
+                help = 'Select peers database [default %default]')
+  op.add_option('--daemon', dest = 'daemon',
+                default = False, action = 'store_true',
+                help = 'Become a daemon after successful initialization')
+  op.add_option('--debug', dest = 'debug',
+                default = False, action = 'store_true',
+                help = 'Emit debugging trace information')
+  op.add_option('--startup', dest = 'startup',
+                default = False, action = 'store_true',
+                help = 'Being called as part of the server startup')
+
+  opts, args = op.parse_args()
+  if args: op.error('no arguments permitted')
+  OS.chdir(opts.dir)
+  T._debug = opts.debug
+  return opts
+
+## Service table, for running manually.
+service_info = [('watch', T.VERSION, {
+  'adopted': (0, 0, '', cmd_adopted),
+  'kick': (1, 1, 'PEER', cmd_kick)
+})]
+
+if __name__ == '__main__':
+  opts = parse_options()
+  T.runservices(opts.tripesock, service_info,
+                init = init, setup = setup,
+                daemon = opts.daemon)
+
+###----- That's all, folks --------------------------------------------------