From a62f8e8a94bf56194539f7140a1215bc74309b36 Mon Sep 17 00:00:00 2001 Message-Id: From: Mark Wooding Date: Mon, 19 Apr 2010 21:11:05 +0100 Subject: [PATCH] svc: Peer management services. Organization: Straylight/Edgeware From: Mark Wooding * 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. --- Makefile.am | 4 + debian/.gitignore | 2 +- debian/control | 12 + debian/rules | 7 + debian/tripe-peer-services.dirs | 1 + debian/tripe-peer-services.install | 10 + debian/tripe.dirs | 1 + init/tripe-init.in | 23 +- peerdb/peers.in.5.in | 72 +++ py/tripe.py.in | 2 +- server/Makefile.am | 2 +- svc/Makefile.am | 43 ++ svc/connect.8.in | 361 ++++++++++++++ svc/connect.in | 249 ++++++++++ svc/tripe-ifup.8.in | 155 ++++++ svc/tripe-ifup.in | 94 ++++ svc/watch.8.in | 480 +++++++++++++++++++ svc/watch.in | 734 +++++++++++++++++++++++++++++ 18 files changed, 2245 insertions(+), 7 deletions(-) create mode 100644 debian/tripe-peer-services.dirs create mode 100644 debian/tripe-peer-services.install create mode 100644 svc/connect.8.in create mode 100644 svc/connect.in create mode 100644 svc/tripe-ifup.8.in create mode 100644 svc/tripe-ifup.in create mode 100644 svc/watch.8.in create mode 100644 svc/watch.in diff --git a/Makefile.am b/Makefile.am index 4446113f..7cbef362 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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 diff --git a/debian/.gitignore b/debian/.gitignore index 50597ec9..e8d38ced 100644 --- a/debian/.gitignore +++ b/debian/.gitignore @@ -20,4 +20,4 @@ tripemon tripe-keys tripe-ethereal tripe-uslip - +tripe-peer-services diff --git a/debian/control b/debian/control index 91626c2f..b451153f 100644 --- a/debian/control +++ b/debian/control @@ -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 diff --git a/debian/rules b/debian/rules index 12de3ab1..0a422e83 100755 --- a/debian/rules +++ b/debian/rules @@ -43,6 +43,13 @@ install/tripe:: 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. diff --git a/debian/tripe-peer-services.dirs b/debian/tripe-peer-services.dirs new file mode 100644 index 00000000..b6462c35 --- /dev/null +++ b/debian/tripe-peer-services.dirs @@ -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 index 00000000..6a8618f9 --- /dev/null +++ b/debian/tripe-peer-services.install @@ -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 diff --git a/debian/tripe.dirs b/debian/tripe.dirs index 0048c947..2683e2f8 100644 --- a/debian/tripe.dirs +++ b/debian/tripe.dirs @@ -1 +1,2 @@ etc/tripe/peers +etc/tripe/services diff --git a/init/tripe-init.in b/init/tripe-init.in index dfa7e031..433f6a00 100755 --- a/init/tripe-init.in +++ b/init/tripe-init.in @@ -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:" diff --git a/peerdb/peers.in.5.in b/peerdb/peers.in.5.in index 7b0127b1..fbc7c5c5 100644 --- a/peerdb/peers.in.5.in +++ b/peerdb/peers.in.5.in @@ -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). . .\"-------------------------------------------------------------------------- diff --git a/py/tripe.py.in b/py/tripe.py.in index 79060106..e88f379f 100644 --- a/py/tripe.py.in +++ b/py/tripe.py.in @@ -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. diff --git a/server/Makefile.am b/server/Makefile.am index b6df4280..4bebcd9e 100644 --- a/server/Makefile.am +++ b/server/Makefile.am @@ -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 -------------------------------------------------- diff --git a/svc/Makefile.am b/svc/Makefile.am index f37ecb17..3e392b4e 100644 --- a/svc/Makefile.am +++ b/svc/Makefile.am @@ -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 index 00000000..77357f52 --- /dev/null +++ b/svc/connect.8.in @@ -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, +. +.\"----- That's all, folks -------------------------------------------------- diff --git a/svc/connect.in b/svc/connect.in new file mode 100644 index 00000000..dae11620 --- /dev/null +++ b/svc/connect.in @@ -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 index 00000000..0fe89d82 --- /dev/null +++ b/svc/tripe-ifup.8.in @@ -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, +. +.\"----- That's all, folks -------------------------------------------------- diff --git a/svc/tripe-ifup.in b/svc/tripe-ifup.in new file mode 100644 index 00000000..fc902dab --- /dev/null +++ b/svc/tripe-ifup.in @@ -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 index 00000000..3e402510 --- /dev/null +++ b/svc/watch.8.in @@ -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, +. +.\"----- That's all, folks -------------------------------------------------- diff --git a/svc/watch.in b/svc/watch.in new file mode 100644 index 00000000..bfad160c --- /dev/null +++ b/svc/watch.in @@ -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 -------------------------------------------------- -- [mdw]