chiark / gitweb /
svc/conntrack.in: New service to track connection status.
authorMark Wooding <mdw@distorted.org.uk>
Sat, 8 May 2010 23:58:19 +0000 (00:58 +0100)
committerMark Wooding <mdw@distorted.org.uk>
Sun, 9 May 2010 14:23:52 +0000 (15:23 +0100)
Works through D-Bus and suchlike overly modern things.

debian/tripe-peer-services.install
svc/Makefile.am
svc/conntrack.8.in [new file with mode: 0644]
svc/conntrack.in [new file with mode: 0644]

index 6a8618f..3227e15 100644 (file)
@@ -1,5 +1,7 @@
 debian/tmp/usr/lib/tripe/services/connect
 debian/tmp/usr/share/man/man8/connect.8
+debian/tmp/usr/lib/tripe/services/conntrack
+debian/tmp/usr/share/man/man8/conntrack.8
 debian/tmp/usr/lib/tripe/services/watch
 debian/tmp/usr/share/man/man8/watch.8
 debian/tmp/usr/sbin/tripe-newpeers
index 3e392b4..ffe74d0 100644 (file)
@@ -64,6 +64,19 @@ watch: watch.in Makefile
        $(confsubst) $(srcdir)/watch.in >$@.new $(SUBSTITUTIONS) && \
                chmod +x $@.new && mv $@.new $@
 
+## Watch D-Bus to keep track of external connectivity.
+services_SCRIPTS       += conntrack
+CLEANFILES             += conntrack
+EXTRA_DIST             += conntrack.in
+
+man_MANS               += conntrack.8
+CLEANFILES             += conntrack.8
+EXTRA_DIST             += conntrack.8.in
+
+conntrack: conntrack.in Makefile
+       $(confsubst) $(srcdir)/conntrack.in >$@.new $(SUBSTITUTIONS) && \
+               chmod +x $@.new && mv $@.new $@
+
 ## Bring up an interface.
 sbin_SCRIPTS            = tripe-ifup
 CLEANFILES             += tripe-ifup
diff --git a/svc/conntrack.8.in b/svc/conntrack.8.in
new file mode 100644 (file)
index 0000000..9c0fe1b
--- /dev/null
@@ -0,0 +1,273 @@
+.\" -*-nroff-*-
+.\".
+.\" Manual for the conntrack service
+.\"
+.\" (c) 2010 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 ../common/defs.man \"@@@PRE@@@
+.
+.\"--------------------------------------------------------------------------
+.TH connect 8 "8 January 2007" "Straylight/Edgeware" "TrIPE: Trivial IP Encryption"
+.
+.\"--------------------------------------------------------------------------
+.SH "NAME"
+.
+conntrack \- tripe service to start/stop peers depending on external connectivity
+.
+.\"--------------------------------------------------------------------------
+.SH "SYNOPSIS"
+.
+.B conntrack
+.RB [ \-a
+.IR socket ]
+.RB [ \-d
+.IR dir ]
+.RB [ \-f
+.IR file ]
+.br
+\&     \c
+.RB [ \-\-daemon ]
+.RB [ \-\-debug ]
+.RB [ \-\-startup ]
+.
+.\"--------------------------------------------------------------------------
+.SH "DESCRIPTION"
+.
+The
+.B conntrack
+service watches D-Bus network management services like
+.BR NetworkManager (8)
+and Nokia's
+.BR ICd ,
+bringing peers up and down automatically.  It's designed to be useful on
+a mobile device, such as a laptop; I expect servers to stay where
+they're put and be configured statically.
+.SS "Configuration"
+The
+.B conntrack
+service reads a configuration file, by default
+.BR conntrack.conf ,
+explaining which peers to bring up under which circumstances.  The
+configuration file is automatically re-read if it's changed.
+.PP
+The
+configuration is split into sections, each describing a
+.IR "peer group" . 
+A section begins with the peer group name in square brackets:
+.IP
+.BI [ group ]
+.PP
+The group name is entirely arbitrary, and affects nothing else.  This is
+followed by peer definitions, each of which looks like this:
+.IP
+.I tag
+.B =
+.RI [ remote-addr ]
+.IB network / mask
+.PP
+This means that the peer
+.I tag
+should be selected if the host's current IP address is within the
+network indicated by
+.IB network / mask \fR.  
+Here,
+.I network
+is an IP address in dotted-quad form, and
+.I mask
+is a netmask, either in dotted-quad form, or as a number of 1-bits.
+Only one peer in each group may be connected at any given time; if a
+change is needed, any existing peer in the group is killed before
+connecting the new one.  If no match is found in a particular group,
+then no peers in the group are connected.  Strange and unhelpful things
+will happen if you put the same peer in several different groups.
+.PP
+The notion of `current IP address' is somewhat vague.  The
+.B conntrack
+service calculates it as the source address that the host would put on
+an IP packet sent to an arbitrarily chosen remote address.  The default
+remote address is 1.2.3.4 (which is unlikely ever to be assigned); this
+should determine an IP address on the network interface closest to the
+default gateway.  You can influence this process in two ways.  Firstly,
+you can change the default remote address used by adding a line
+.IP
+.B "test-addr ="
+.I remote-addr
+.PP
+before the first peer group section.  Secondly, you can specify a
+particular
+.I remote-addr
+to use when checking whether a particular peer is applicable.
+.PP
+The peer definitions can be in any order.  They are checked
+most-specific first, and searching stops as soon as a match is found.
+Therefore a default definition can be added as
+.IP
+.I tag
+.B =
+.B 0/0
+.PP
+without fear of overriding any more specific definitions.  For avoidance
+of doubt, one peer definition is
+.I more specific
+than another if either the former has a specified
+.I remote-addr
+and the latter has not, or the former is wholly contained within the
+latter.  (Overlapping definitions are not recommended, and will be
+processed in an arbitrary order.)
+.PP
+Peers are connected using the
+.BR connect (8)
+service:
+.IP
+.B SVCSUBMIT connect active
+.I peer
+.SS "Command line"
+In addition to the standard options described in
+.BR tripe-service (7),
+the following command-line options are recognized.
+.TP
+.BI "\-f, \-\-config=" file
+Use
+.I file
+as the configuration file.  In the absence of this option, the
+file named
+.B conntrack.conf
+in the current working directory is used instead.
+.
+.\"--------------------------------------------------------------------------
+.SH "SERVICE COMMAND REFERENCE"
+.
+.\"* 10 Service commands
+The commands provided by the service are as follows.
+.SP
+.BI "up " reason\fR...
+Informs the service that the network connection has been established:
+peer groups should be connected.  The
+.I reason
+is quoted in the status notification.
+.SP
+.BI "down " reason\fR...
+Informs the service that the network connection has been lost:
+peer groups should be disconnected.  The
+.I reason
+is quoted in the status notification.
+.
+.\"--------------------------------------------------------------------------
+.SH "NOTIFICATIONS"
+.
+.\"* 30 Notification broadcasts (NOTE codes)
+All notifications issued by
+.B conntrack
+begin with the tokens
+.BR "USER conntrack" .
+.SP
+.BI "USER conntrack " up \fR| down " " reason\fR...
+The network connection has apparently gone up or down, and
+.B conntrack
+is about to kill and/or connect peers accordingly.  The
+.I reason
+is one of the following.
+.RS
+.TP
+.B "nm initially-connected"
+NetworkManager was detected on startup, and has an active network
+connection.
+.TP
+.B "nm initially-disconnected"
+NetworkManager was detected on startup, and has no active network
+connection.
+.TP
+.B "nm connected"
+NetworkManager has acquired an active network connection.
+.TP
+.B "nm disconnected"
+NetworkManager has lost its active network connection.
+.TP
+.B "nm default-connection-change"
+NetworkManager has changed its default route.
+.TP
+.BI "icd initially-connected " iap
+Maemo ICd was detected on startup, and has an active network connection
+identified by
+.IR iap .
+.TP
+.B "icd initially-disconnected"
+Maemo ICd was detected on startup, and has no active network connection.
+.TP
+.BI "icd connected " iap
+Maemo ICd has acquired an active network connection, identified by
+.IR iap .
+.TP
+.B "icd idle"
+Maemo ICd has lost its active network connection.
+.TP
+.B interval-timer
+A change was detected during
+.BR conntrack 's
+periodic status check.  This usually means that the network connection
+was reconfigured manually without informing
+.BR conntrack .
+.TP
+.BI "manual " reason\fR...
+The connection status was changed manually, using the
+.B up
+or
+.B down
+service command.
+.RE
+.
+.\"--------------------------------------------------------------------------
+.SH "WARNINGS"
+.
+.\"* 40 Warning broadcasts (WARN codes)
+All warnings issued by
+.B conntrack
+begin with the tokens
+.BR "USER conntrack" .
+.SP
+.BI "USER conntrack config-file-error " exception " " error-text
+The configuration file is invalid.  The
+.I exception
+token names a Python exception; the
+.I error-text
+describes the problem encountered, though it may not be very useful.
+.
+.\"--------------------------------------------------------------------------
+.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/conntrack.in b/svc/conntrack.in
new file mode 100644 (file)
index 0000000..5f4cc98
--- /dev/null
@@ -0,0 +1,516 @@
+#! @PYTHON@
+### -*-python-*-
+###
+### Service for automatically tracking network connection status
+###
+### (c) 2010 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 ConfigParser import RawConfigParser
+from optparse import OptionParser
+import os as OS
+import sys as SYS
+import socket as S
+import mLib as M
+import tripe as T
+import dbus as D
+for i in ['mainloop', 'mainloop.glib']:
+  __import__('dbus.%s' % i)
+import gobject as G
+from struct import pack, unpack
+
+SM = T.svcmgr
+##__import__('rmcr').__debug = True
+
+###--------------------------------------------------------------------------
+### Utilities.
+
+class struct (object):
+  """A simple container object."""
+  def __init__(me, **kw):
+    me.__dict__.update(kw)
+
+def toposort(cmp, things):
+  """
+  Generate the THINGS in an order consistent with a given partial order.
+
+  The function CMP(X, Y) should return true if X must precede Y, and false if
+  it doesn't care.  If X and Y are equal then it should return false.
+
+  The THINGS may be any finite iterable; it is converted to a list
+  internally.
+  """
+
+  ## Make sure we can index the THINGS, and prepare an ordering table.
+  ## What's going on?  The THINGS might not have a helpful equality
+  ## predicate, so it's easier to work with indices.  The ordering table will
+  ## remember which THINGS (by index) are considered greater than other
+  ## things.
+  things = list(things)
+  n = len(things)
+  order = [{} for i in xrange(n)]
+  rorder = [{} for i in xrange(n)]
+  for i in xrange(n):
+    for j in xrange(n):
+      if i != j and cmp(things[i], things[j]):
+        order[j][i] = True
+        rorder[i][j] = True
+
+  ## Now we can do the sort.
+  out = []
+  while True:
+    done = True
+    for i in xrange(n):
+      if order[i] is not None:
+        done = False
+        if len(order[i]) == 0:
+          for j in rorder[i]:
+            del order[j][i]
+          yield things[i]
+          order[i] = None
+    if done:
+      break
+
+###--------------------------------------------------------------------------
+### Parse the configuration file.
+
+## Hmm.  Should I try to integrate this with the peers database?  It's not a
+## good fit; it'd need special hacks in tripe-newpeers.  And the use case for
+## this service are largely going to be satellite notes, I don't think
+## scalability's going to be a problem.
+
+class Config (object):
+  """
+  Represents a configuration file.
+
+  The most interesting thing is probably the `groups' slot, which stores a
+  list of pairs (NAME, PATTERNS); the NAME is a string, and the PATTERNS a
+  list of (TAG, PEER, ADDR, MASK) triples.  The implication is that there
+  should be precisely one peer with a name matching NAME-*, and that it
+  should be NAME-TAG, where (TAG, PEER, ADDR, MASK) is the first triple such
+  that the host's primary IP address (if PEER is None -- or the IP address it
+  would use for communicating with PEER) is within the network defined by
+  ADDR/MASK.
+  """
+
+  def __init__(me, file):
+    """
+    Construct a new Config object, reading the given FILE.
+    """
+    me._file = file
+    me._fwatch = M.FWatch(file)
+    me._update()
+
+  def check(me):
+    """
+    See whether the configuration file has been updated.
+    """
+    if me._fwatch.update():
+      me._update()
+
+  def _update(me):
+    """
+    Internal function to update the configuration from the underlying file.
+    """
+
+    ## Read the configuration.  We have no need of the fancy substitutions,
+    ## so turn them all off.
+    cp = RawConfigParser()
+    cp.read(me._file)
+
+    ## Save the test address.  Make sure it's vaguely sensible.  The default
+    ## is probably good for most cases, in fact, since that address isn't
+    ## actually in use.  Note that we never send packets to the test address;
+    ## we just use it to discover routing information.
+    if cp.has_option('DEFAULT', 'test-addr'):
+      testaddr = cp.get('DEFAULT', 'test-addr')
+      S.inet_aton(testaddr)
+    else:
+      testaddr = '1.2.3.4'
+
+    ## Scan the configuration file and build the groups structure.
+    groups = []
+    for sec in cp.sections():
+      pats = []
+      for tag in cp.options(sec):
+        spec = cp.get(sec, tag).split()
+
+        ## Parse the entry into peer and network.
+        if len(spec) == 1:
+          peer = None
+          net = spec[0]
+        else:
+          peer, net = spec
+
+        ## Syntax of a net is ADDRESS/MASK, where ADDRESS is a dotted-quad,
+        ## and MASK is either a dotted-quad or a single integer N indicating
+        ## a mask with N leading ones followed by trailing zeroes.
+        slash = net.index('/')
+        addr, = unpack('>L', S.inet_aton(net[:slash]))
+        if net.find('.', slash + 1) >= 0:
+          mask, = unpack('>L', S.inet_aton(net[:slash]))
+        else:
+          n = int(net[slash + 1:], 10)
+          mask = (1 << 32) - (1 << 32 - n)
+        pats.append((tag, peer, addr & mask, mask))
+
+      ## Annoyingly, RawConfigParser doesn't preserve the order of options.
+      ## In order to make things vaguely sane, we topologically sort the
+      ## patterns so that more specific patterns are checked first.
+      pats = list(toposort(lambda (t, p, a, m), (tt, pp, aa, mm): \
+                             (p and not pp) or \
+                             (p == pp and m == (m | mm) and aa == (a & mm)),
+                           pats))
+      groups.append((sec, pats))
+
+    ## Done.
+    me.testaddr = testaddr
+    me.groups = groups
+
+### This will be a configuration file.
+CF = None
+
+###--------------------------------------------------------------------------
+### Responding to a network up/down event.
+
+def localaddr(peer):
+  """
+  Return the local IP address used for talking to PEER.
+  """
+  sk = S.socket(S.AF_INET, S.SOCK_DGRAM)
+  try:
+    try:
+      sk.connect((peer, 1))
+      addr, _ = sk.getsockname()
+      addr, = unpack('>L', S.inet_aton(addr))
+      return addr
+    except S.error:
+      return None
+  finally:
+    sk.close()
+
+_kick = T.Queue()
+def kickpeers():
+  while True:
+    upness, reason = _kick.get()
+
+    ## Make sure the configuration file is up-to-date.  Don't worry if we
+    ## can't do anything useful.
+    try:
+      CF.check()
+    except Exception, exc:
+      SM.warn('conntrack', 'config-file-error',
+              exc.__class__.__name__, str(exc))
+
+    ## Find the current list of peers.
+    peers = SM.list()
+
+    ## Work out the primary IP address.
+    if upness:
+      addr = localaddr(CF.testaddr)
+      if addr is None:
+        upness = False
+
+    ## Now decide what to do.
+    changes = []
+    for g, pp in CF.groups:
+
+      ## Find out which peer in the group ought to be active.
+      want = None                       # unequal to any string
+      if upness:
+        for t, p, a, m in pp:
+          if p is None:
+            aq = addr
+          else:
+            aq = localaddr(p)
+          if aq is not None and (aq & m) == a:
+            want = t
+            break
+
+      ## Shut down the wrong ones.
+      found = False
+      for p in peers:
+        if p == want:
+          found = True
+        elif p.startswith(g) and p != want:
+          changes.append(lambda p=p: SM.kill(p))
+
+      ## Start the right one if necessary.
+      if want is not None and not found:
+        changes.append(lambda: T._simple(SM.svcsubmit('connect', 'active',
+                                                      want)))
+
+    ## Commit the changes.
+    if changes:
+      SM.notify('conntrack', upness and 'up' or 'down', *reason)
+      for c in changes: c()
+
+def netupdown(upness, reason):
+  """
+  Add or kill peers according to whether the network is up or down.
+
+  UPNESS is true if the network is up, or false if it's down.
+  """
+
+  _kick.put((upness, reason))
+
+###--------------------------------------------------------------------------
+### NetworkManager monitor.
+
+NM_NAME = 'org.freedesktop.NetworkManager'
+NM_PATH = '/org/freedesktop/NetworkManager'
+NM_IFACE = NM_NAME
+NMCA_IFACE = NM_NAME + '.Connection.Active'
+
+NM_STATE_CONNECTED = 3
+
+class NetworkManagerMonitor (object):
+  """
+  Watch NetworkManager signals for changes in network state.
+  """
+
+  ## Strategy.  There are two kinds of interesting state transitions for us.
+  ## The first one is the global are-we-connected state, which we'll use to
+  ## toggle network upness on a global level.  The second is which connection
+  ## has the default route, which we'll use to tweak which peer in the peer
+  ## group is active.  The former is most easily tracked using the signal
+  ## org.freedesktop.NetworkManager.StateChanged; for the latter, we track
+  ## org.freedesktop.NetworkManager.Connection.Active.PropertiesChanged and
+  ## look for when a new connection gains the default route.
+
+  def attach(me, bus):
+    try:
+      nm = bus.get_object(NM_NAME, NM_PATH)
+      state = nm.Get(NM_IFACE, 'State')
+      if state == NM_STATE_CONNECTED:
+        netupdown(True, ['nm', 'initially-connected'])
+      else:
+        netupdown(False, ['nm', 'initially-disconnected'])
+    except D.DBusException:
+      pass
+    bus.add_signal_receiver(me._nm_state, 'StateChanged', NM_IFACE,
+                            NM_NAME, NM_PATH)
+    bus.add_signal_receiver(me._nm_connchange,
+                            'PropertiesChanged', NMCA_IFACE,
+                            NM_NAME, None)
+
+  def _nm_state(me, state):
+    if state == NM_STATE_CONNECTED:
+      netupdown(True, ['nm', 'connected'])
+    else:
+      netupdown(False, ['nm', 'disconnected'])
+
+  def _nm_connchange(me, props):
+    if props.get('Default', False):
+      netupdown(True, ['nm', 'default-connection-change'])
+
+###--------------------------------------------------------------------------
+### Maemo monitor.
+
+ICD_NAME = 'com.nokia.icd'
+ICD_PATH = '/com/nokia/icd'
+ICD_IFACE = ICD_NAME
+
+class MaemoICdMonitor (object):
+  """
+  Watch ICd signals for changes in network state.
+  """
+
+  ## Strategy.  ICd only handles one connection at a time in steady state,
+  ## though when switching between connections, it tries to bring the new one
+  ## up before shutting down the old one.  This makes life a bit easier than
+  ## it is with NetworkManager.  On the other hand, the notifications are
+  ## relative to particular connections only, and the indicator that the old
+  ## connection is down (`IDLE') comes /after/ the new one comes up
+  ## (`CONNECTED'), so we have to remember which one is active.
+
+  def attach(me, bus):
+    try:
+      icd = bus.get_object(ICD_NAME, ICD_PATH)
+      try:
+        iap = icd.get_ipinfo(dbus_interface = ICD_IFACE)[0]
+        me._iap = iap
+        netupdown(True, ['icd', 'initially-connected', iap])
+      except D.DBusException:
+        me._iap = None
+        netupdown(False, ['icd', 'initially-disconnected'])
+    except D.DBusException:
+      me._iap = None
+    bus.add_signal_receiver(me._icd_state, 'status_changed', ICD_IFACE,
+                            ICD_NAME, ICD_PATH)
+
+  def _icd_state(me, iap, ty, state, hunoz):
+    if state == 'CONNECTED':
+      me._iap = iap
+      netupdown(True, ['icd', 'connected', iap])
+    elif state == 'IDLE' and iap == me._iap:
+      me._iap = None
+      netupdown(False, ['icd', 'idle'])
+
+###--------------------------------------------------------------------------
+### D-Bus connection tracking.
+
+class DBusMonitor (object):
+  """
+  Maintains a connection to the system D-Bus, and watches for signals.
+
+  If the connection is initially down, or drops for some reason, we retry
+  periodically (every five seconds at the moment).  If the connection
+  resurfaces, we reattach the monitors.
+  """
+
+  def __init__(me):
+    """
+    Initialise the object and try to establish a connection to the bus.
+    """
+    me._mons = []
+    me._loop = D.mainloop.glib.DBusGMainLoop()
+    me._reconnect()
+
+  def addmon(me, mon):
+    """
+    Add a monitor object to watch for signals.
+
+    MON.attach(BUS) is called, with BUS being the connection to the system
+    bus.  MON should query its service's current status and watch for
+    relevant signals.
+    """
+    me._mons.append(mon)
+    if me._bus is not None:
+      mon.attach(me._bus)
+
+  def _reconnect(me):
+    """
+    Start connecting to the bus.
+
+    If we fail the first time, retry periodically.
+    """
+    me._bus = None
+    if me._try_connect():
+      G.timeout_add_seconds(5, me._try_connect)
+
+  def _try_connect(me):
+    """
+    Actually make a connection attempt.
+
+    If we succeed, attach the monitors.
+    """
+    try:
+      bus = D.SystemBus(mainloop = me._loop, private = True)
+    except D.DBusException:
+      return True
+    me._bus = bus
+    bus.call_on_disconnection(me._reconnect)
+    for m in me._mons:
+      m.attach(bus)
+    return False
+
+###--------------------------------------------------------------------------
+### TrIPE service.
+
+class GIOWatcher (object):
+  """
+  Monitor I/O events using glib.
+  """
+  def __init__(me, conn, mc = G.main_context_default()):
+    me._conn = conn
+    me._watch = None
+    me._mc = mc
+  def connected(me, sock):
+    me._watch = G.io_add_watch(sock, G.IO_IN,
+                               lambda *hunoz: me._conn.receive())
+  def disconnected(me):
+    G.source_remove(me._watch)
+    me._watch = None
+  def iterate(me):
+    me._mc.iteration(True)
+
+SM.iowatch = GIOWatcher(SM)
+
+def init():
+  """
+  Service initialization.
+
+  Add the D-Bus monitor here, because we might send commands off immediately,
+  and we want to make sure the server connection is up.
+  """
+  T.Coroutine(kickpeers).switch()
+  dbm = DBusMonitor()
+  dbm.addmon(NetworkManagerMonitor())
+  dbm.addmon(MaemoICdMonitor())
+  G.timeout_add_seconds(300, lambda: (netupdown(True, ['interval-timer'])
+                                      or True))
+
+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('-c', '--config',
+                metavar = 'FILE', dest = 'conf', default = 'conntrack.conf',
+                help = 'Select configuration [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.
+def cmd_updown(upness):
+  return lambda *args: T.defer(netupdown, upness, ['manual'] + list(args))
+service_info = [('conntrack', VERSION, {
+  'up': (0, None, '', cmd_updown(True)),
+  'down': (0, None, '', cmd_updown(False))
+})]
+
+if __name__ == '__main__':
+  opts = parse_options()
+  CF = Config(opts.conf)
+  T.runservices(opts.tripesock, service_info,
+                init = init, daemon = opts.daemon)
+
+###----- That's all, folks --------------------------------------------------