chiark / gitweb /
svc/conntrack.in: Split out a base class from `InetAddress'.
[tripe] / svc / conntrack.in
1 #! @PYTHON@
2 ### -*-python-*-
3 ###
4 ### Service for automatically tracking network connection status
5 ###
6 ### (c) 2010 Straylight/Edgeware
7 ###
8
9 ###----- Licensing notice ---------------------------------------------------
10 ###
11 ### This file is part of Trivial IP Encryption (TrIPE).
12 ###
13 ### TrIPE is free software: you can redistribute it and/or modify it under
14 ### the terms of the GNU General Public License as published by the Free
15 ### Software Foundation; either version 3 of the License, or (at your
16 ### option) any later version.
17 ###
18 ### TrIPE is distributed in the hope that it will be useful, but WITHOUT
19 ### ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
20 ### FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
21 ### for more details.
22 ###
23 ### You should have received a copy of the GNU General Public License
24 ### along with TrIPE.  If not, see <https://www.gnu.org/licenses/>.
25
26 VERSION = '@VERSION@'
27
28 ###--------------------------------------------------------------------------
29 ### External dependencies.
30
31 from ConfigParser import RawConfigParser
32 from optparse import OptionParser
33 import os as OS
34 import sys as SYS
35 import socket as S
36 import mLib as M
37 import tripe as T
38 import dbus as D
39 import re as RX
40 for i in ['mainloop', 'mainloop.glib']:
41   __import__('dbus.%s' % i)
42 try: from gi.repository import GLib as G
43 except ImportError: import gobject as G
44 from struct import pack, unpack
45 from cStringIO import StringIO
46
47 SM = T.svcmgr
48 ##__import__('rmcr').__debug = True
49
50 ###--------------------------------------------------------------------------
51 ### Utilities.
52
53 class struct (object):
54   """A simple container object."""
55   def __init__(me, **kw):
56     me.__dict__.update(kw)
57
58 def loadb(s):
59   n = 0
60   for ch in s: n = 256*n + ord(ch)
61   return n
62
63 def storeb(n, wd = None):
64   if wd is None: wd = n.bit_length()
65   s = StringIO()
66   for i in xrange((wd - 1)&-8, -8, -8): s.write(chr((n >> i)&0xff))
67   return s.getvalue()
68
69 ###--------------------------------------------------------------------------
70 ### Address manipulation.
71 ###
72 ### I think this is the most demanding application, in terms of address
73 ### hacking, in the entire TrIPE suite.  At least we don't have to do it in
74 ### C.
75
76 class BaseAddress (object):
77   def __init__(me, addrstr, maskstr = None):
78     me._setaddr(addrstr)
79     if maskstr is None:
80       me.mask = -1
81     elif maskstr.isdigit():
82       me.mask = (1 << me.NBITS) - (1 << me.NBITS - int(maskstr))
83     else:
84       me._setmask(maskstr)
85     if me.addr&~me.mask:
86       raise ValueError('network contains bits set beyond mask')
87   def _addrstr_to_int(me, addrstr):
88     try: return loadb(S.inet_pton(me.AF, addrstr))
89     except S.error: raise ValueError('bad address syntax')
90   def _int_to_addrstr(me, n):
91     return S.inet_ntop(me.AF, storeb(me.addr, me.NBITS))
92   def _setmask(me, maskstr):
93     raise ValueError('only prefix masked supported')
94   def _maskstr(me):
95     raise ValueError('only prefix masked supported')
96   def sockaddr(me, port = 0):
97     if me.mask != -1: raise ValueError('not a simple address')
98     return me._sockaddr(port)
99   def __str__(me):
100     addrstr = me._addrstr()
101     if me.mask == -1:
102       return addrstr
103     else:
104       inv = me.mask ^ ((1 << me.NBITS) - 1)
105       if (inv&(inv + 1)) == 0:
106         return '%s/%d' % (addrstr, me.NBITS - inv.bit_length())
107       else:
108         return '%s/%s' % (addrstr, me._maskstr())
109   def withinp(me, net):
110     if type(net) != type(me): return False
111     if (me.mask&net.mask) != net.mask: return False
112     if (me.addr ^ net.addr)&net.mask: return False
113     return me._withinp(net)
114   def eq(me, other):
115     if type(me) != type(other): return False
116     if me.mask != other.mask: return False
117     if me.addr != other.addr: return False
118     return me._eq(other)
119   def _withinp(me, net):
120     return True
121   def _eq(me, other):
122     return True
123
124 class InetAddress (BaseAddress):
125   AF = S.AF_INET
126   AFNAME = 'IPv4'
127   NBITS = 32
128   def _addrstr_to_int(me, addrstr):
129     try: return loadb(S.inet_aton(addrstr))
130     except S.error: raise ValueError('bad address syntax')
131   def _setaddr(me, addrstr):
132     me.addr = me._addrstr_to_int(addrstr)
133   def _setmask(me, maskstr):
134     me.mask = me._addrstr_to_int(maskstr)
135   def _addrstr(me):
136     return me._int_to_addrstr(me.addr)
137   def _maskstr(me):
138     return me._int_to_addrstr(me.mask)
139   def _sockaddr(me, port = 0):
140     return (me._addrstr(), port)
141   @classmethod
142   def from_sockaddr(cls, sa):
143     addr, port = (lambda a, p: (a, p))(*sa)
144     return cls(addr), port
145
146 def parse_address(addrstr, maskstr = None):
147   return InetAddress(addrstr, maskstr)
148
149 def parse_net(netstr):
150   try: sl = netstr.index('/')
151   except ValueError: raise ValueError('missing mask')
152   return parse_address(netstr[:sl], netstr[sl + 1:])
153
154 def straddr(a): return a is None and '#<none>' or str(a)
155
156 ###--------------------------------------------------------------------------
157 ### Parse the configuration file.
158
159 ## Hmm.  Should I try to integrate this with the peers database?  It's not a
160 ## good fit; it'd need special hacks in tripe-newpeers.  And the use case for
161 ## this service are largely going to be satellite notes, I don't think
162 ## scalability's going to be a problem.
163
164 TESTADDRS = [InetAddress('1.2.3.4')]
165
166 CONFSYNTAX = [
167   ('COMMENT', RX.compile(r'^\s*($|[;#])')),
168   ('GRPHDR', RX.compile(r'^\s*\[(.*)\]\s*$')),
169   ('ASSGN', RX.compile(r'\s*([\w.-]+)\s*[:=]\s*(|\S|\S.*\S)\s*$'))]
170
171 class ConfigError (Exception):
172   def __init__(me, file, lno, msg):
173     me.file = file
174     me.lno = lno
175     me.msg = msg
176   def __str__(me):
177     return '%s:%d: %s' % (me.file, me.lno, me.msg)
178
179 class Config (object):
180   """
181   Represents a configuration file.
182
183   The most interesting thing is probably the `groups' slot, which stores a
184   list of pairs (NAME, PATTERNS); the NAME is a string, and the PATTERNS a
185   list of (TAG, PEER, NETS) triples.  The implication is that there should be
186   precisely one peer from the set, and that it should be named TAG, where
187   (TAG, PEER, NETS) is the first triple such that the host's primary IP
188   address (if PEER is None -- or the IP address it would use for
189   communicating with PEER) is within one of the networks defined by NETS.
190   """
191
192   def __init__(me, file):
193     """
194     Construct a new Config object, reading the given FILE.
195     """
196     me._file = file
197     me._fwatch = M.FWatch(file)
198     me._update()
199
200   def check(me):
201     """
202     See whether the configuration file has been updated.
203     """
204     if me._fwatch.update():
205       me._update()
206
207   def _update(me):
208     """
209     Internal function to update the configuration from the underlying file.
210     """
211
212     if T._debug: print '# reread config'
213
214     ## Initial state.
215     testaddrs = {}
216     groups = {}
217     grpname = None
218     grplist = []
219
220     ## Open the file and start reading.
221     with open(me._file) as f:
222       lno = 0
223       for line in f:
224         lno += 1
225         for tag, rx in CONFSYNTAX:
226           m = rx.match(line)
227           if m: break
228         else:
229           raise ConfigError(me._file, lno, 'failed to parse line: %r' % line)
230
231         if tag == 'COMMENT':
232           ## A comment.  Ignore it and hope it goes away.
233
234           continue
235
236         elif tag == 'GRPHDR':
237           ## A group header.  Flush the old group and start a new one.
238           newname = m.group(1)
239
240           if grpname is not None: groups[grpname] = grplist
241           if newname in groups:
242             raise ConfigError(me._file, lno,
243                               "duplicate group name `%s'" % newname)
244           grpname = newname
245           grplist = []
246
247         elif tag == 'ASSGN':
248            ## An assignment.  Deal with it.
249           name, value = m.group(1), m.group(2)
250
251           if grpname is None:
252             ## We're outside of any group, so this is a global configuration
253             ## tweak.
254
255             if name == 'test-addr':
256               for astr in value.split():
257                 try:
258                   a = parse_address(astr)
259                 except Exception, e:
260                   raise ConfigError(me._file, lno,
261                                     "invalid IP address `%s': %s" %
262                                     (astr, e))
263                 if a.AF in testaddrs:
264                   raise ConfigError(me._file, lno,
265                                     'duplicate %s test-address' % a.AFNAME)
266                 testaddrs[a.AF] = a
267             else:
268               raise ConfigError(me._file, lno,
269                                 "unknown global option `%s'" % name)
270
271           else:
272             ## Parse a pattern and add it to the group.
273             spec = value.split()
274             i = 0
275
276             ## Check for an explicit target address.
277             if i >= len(spec) or spec[i].find('/') >= 0:
278               peer = None
279               af = None
280             else:
281               try:
282                 peer = parse_address(spec[i])
283               except Exception, e:
284                 raise ConfigError(me._file, lno,
285                                   "invalid IP address `%s': %s" %
286                                   (spec[i], e))
287               af = peer.AF
288               i += 1
289
290             ## Parse the list of local networks.
291             nets = []
292             while i < len(spec):
293               try:
294                 net = parse_net(spec[i])
295               except Exception, e:
296                 raise ConfigError(me._file, lno,
297                                   "invalid IP network `%s': %s" %
298                                   (spec[i], e))
299               else:
300                 nets.append(net)
301               i += 1
302             if not nets:
303               raise ConfigError(me._file, lno, 'no networks defined')
304
305             ## Make sure that the addresses are consistent.
306             for net in nets:
307               if af is None:
308                 af = net.AF
309               elif net.AF != af:
310                 raise ConfigError(me._file, lno,
311                                   "net %s doesn't match" % net)
312
313             ## Add this entry to the list.
314             grplist.append((name, peer, nets))
315
316     ## Fill in the default test addresses if necessary.
317     for a in TESTADDRS: testaddrs.setdefault(a.AF, a)
318
319     ## Done.
320     if grpname is not None: groups[grpname] = grplist
321     me.testaddrs = testaddrs
322     me.groups = groups
323
324 ### This will be a configuration file.
325 CF = None
326
327 def cmd_showconfig():
328   T.svcinfo('test-addr=%s' %
329             ' '.join(str(a)
330                      for a in sorted(CF.testaddrs.itervalues(),
331                                      key = lambda a: a.AFNAME)))
332 def cmd_showgroups():
333   for g in sorted(CF.groups.iterkeys()):
334     T.svcinfo(g)
335 def cmd_showgroup(g):
336   try: pats = CF.groups[g]
337   except KeyError: raise T.TripeJobError('unknown-group', g)
338   for t, p, nn in pats:
339     T.svcinfo('peer', t,
340               'target', p and str(p) or '(default)',
341               'net', ' '.join(map(str, nn)))
342
343 ###--------------------------------------------------------------------------
344 ### Responding to a network up/down event.
345
346 def localaddr(peer):
347   """
348   Return the local IP address used for talking to PEER.
349   """
350   sk = S.socket(peer.AF, S.SOCK_DGRAM)
351   try:
352     try:
353       sk.connect(peer.sockaddr(1))
354       addr = sk.getsockname()
355       return type(peer).from_sockaddr(addr)[0]
356     except S.error:
357       return None
358   finally:
359     sk.close()
360
361 _kick = T.Queue()
362 _delay = None
363
364 def cancel_delay():
365   global _delay
366   if _delay is not None:
367     if T._debug: print '# cancel delayed kick'
368     G.source_remove(_delay)
369     _delay = None
370
371 def netupdown(upness, reason):
372   """
373   Add or kill peers according to whether the network is up or down.
374
375   UPNESS is true if the network is up, or false if it's down.
376   """
377
378   _kick.put((upness, reason))
379
380 def delay_netupdown(upness, reason):
381   global _delay
382   cancel_delay()
383   def _func():
384     global _delay
385     if T._debug: print '# delayed %s: %s' % (upness, reason)
386     _delay = None
387     netupdown(upness, reason)
388     return False
389   if T._debug: print '# delaying %s: %s' % (upness, reason)
390   _delay = G.timeout_add(2000, _func)
391
392 def kickpeers():
393   while True:
394     upness, reason = _kick.get()
395     if T._debug: print '# kickpeers %s: %s' % (upness, reason)
396     select = []
397     cancel_delay()
398
399     ## Make sure the configuration file is up-to-date.  Don't worry if we
400     ## can't do anything useful.
401     try:
402       CF.check()
403     except Exception, exc:
404       SM.warn('conntrack', 'config-file-error',
405               exc.__class__.__name__, str(exc))
406
407     ## Find the current list of peers.
408     peers = SM.list()
409
410     ## Work out the primary IP addresses.
411     locals = {}
412     if upness:
413       for af, remote in CF.testaddrs.iteritems():
414         local = localaddr(remote)
415         if local is not None: locals[af] = local
416       if not locals: upness = False
417     if not T._debug: pass
418     elif not locals: print '#   offline'
419     else:
420       for local in locals.itervalues():
421         print '#   local %s address = %s' % (local.AFNAME, local)
422
423     ## Now decide what to do.
424     changes = []
425     for g, pp in CF.groups.iteritems():
426       if T._debug: print '#   check group %s' % g
427
428       ## Find out which peer in the group ought to be active.
429       statemap = {}
430       want = None
431       matchp = False
432       for t, p, nn in pp:
433         af = nn[0].AF
434         if p is None or not upness: ip = locals.get(af)
435         else: ip = localaddr(p)
436         if T._debug:
437           info = 'peer = %s; target = %s; nets = %s; local = %s' % (
438             t, p or '(default)', ', '.join(map(str, nn)), straddr(ip))
439         if upness and not matchp and \
440            ip is not None and any(ip.withinp(n) for n in nn):
441           if T._debug: print '#     %s: SELECTED' % info
442           statemap[t] = 'up'
443           select.append('%s=%s' % (g, t))
444           if t == 'down' or t.startswith('down/'): want = None
445           else: want = t
446           matchp = True
447         else:
448           statemap[t] = 'down'
449           if T._debug: print '#     %s: skipped' % info
450
451       ## Shut down the wrong ones.
452       found = False
453       if T._debug: print '#   peer-map = %r' % statemap
454       for p in peers:
455         what = statemap.get(p, 'leave')
456         if what == 'up':
457           found = True
458           if T._debug: print '#   peer %s: already up' % p
459         elif what == 'down':
460           def _(p = p):
461             try:
462               SM.kill(p)
463             except T.TripeError, exc:
464               if exc.args[0] == 'unknown-peer':
465                 ## Inherently racy; don't worry about this.
466                 pass
467               else:
468                 raise
469           if T._debug: print '#   peer %s: bring down' % p
470           changes.append(_)
471
472       ## Start the right one if necessary.
473       if want is not None and not found:
474         def _(want = want):
475           try:
476             list(SM.svcsubmit('connect', 'active', want))
477           except T.TripeError, exc:
478             SM.warn('conntrack', 'connect-failed', want, *exc.args)
479         if T._debug: print '#   peer %s: bring up' % want
480         changes.append(_)
481
482     ## Commit the changes.
483     if changes:
484       SM.notify('conntrack', upness and 'up' or 'down', *select + reason)
485       for c in changes: c()
486
487 ###--------------------------------------------------------------------------
488 ### NetworkManager monitor.
489
490 DBPROPS_IFACE = 'org.freedesktop.DBus.Properties'
491
492 NM_NAME = 'org.freedesktop.NetworkManager'
493 NM_PATH = '/org/freedesktop/NetworkManager'
494 NM_IFACE = NM_NAME
495 NMCA_IFACE = NM_NAME + '.Connection.Active'
496
497 NM_STATE_CONNECTED = 3 #obsolete
498 NM_STATE_CONNECTED_LOCAL = 50
499 NM_STATE_CONNECTED_SITE = 60
500 NM_STATE_CONNECTED_GLOBAL = 70
501 NM_CONNSTATES = set([NM_STATE_CONNECTED,
502                      NM_STATE_CONNECTED_LOCAL,
503                      NM_STATE_CONNECTED_SITE,
504                      NM_STATE_CONNECTED_GLOBAL])
505
506 class NetworkManagerMonitor (object):
507   """
508   Watch NetworkManager signals for changes in network state.
509   """
510
511   ## Strategy.  There are two kinds of interesting state transitions for us.
512   ## The first one is the global are-we-connected state, which we'll use to
513   ## toggle network upness on a global level.  The second is which connection
514   ## has the default route, which we'll use to tweak which peer in the peer
515   ## group is active.  The former is most easily tracked using the signal
516   ## org.freedesktop.NetworkManager.StateChanged; for the latter, we track
517   ## org.freedesktop.NetworkManager.Connection.Active.PropertiesChanged and
518   ## look for when a new connection gains the default route.
519
520   def attach(me, bus):
521     try:
522       nm = bus.get_object(NM_NAME, NM_PATH)
523       state = nm.Get(NM_IFACE, 'State', dbus_interface = DBPROPS_IFACE)
524       if state in NM_CONNSTATES:
525         netupdown(True, ['nm', 'initially-connected'])
526       else:
527         netupdown(False, ['nm', 'initially-disconnected'])
528     except D.DBusException, e:
529       if T._debug: print '# exception attaching to network-manager: %s' % e
530     bus.add_signal_receiver(me._nm_state, 'StateChanged',
531                             NM_IFACE, NM_NAME, NM_PATH)
532     bus.add_signal_receiver(me._nm_connchange, 'PropertiesChanged',
533                             NMCA_IFACE, NM_NAME, None)
534
535   def _nm_state(me, state):
536     if state in NM_CONNSTATES:
537       delay_netupdown(True, ['nm', 'connected'])
538     else:
539       delay_netupdown(False, ['nm', 'disconnected'])
540
541   def _nm_connchange(me, props):
542     if props.get('Default', False) or props.get('Default6', False):
543       delay_netupdown(True, ['nm', 'default-connection-change'])
544
545 ##--------------------------------------------------------------------------
546 ### Connman monitor.
547
548 CM_NAME = 'net.connman'
549 CM_PATH = '/'
550 CM_IFACE = 'net.connman.Manager'
551
552 class ConnManMonitor (object):
553   """
554   Watch ConnMan signls for changes in network state.
555   """
556
557   ## Strategy.  Everything seems to be usefully encoded in the `State'
558   ## property.  If it's `offline', `idle' or `ready' then we don't expect a
559   ## network connection.  During handover from one network to another, the
560   ## property passes through `ready' to `online'.
561
562   def attach(me, bus):
563     try:
564       cm = bus.get_object(CM_NAME, CM_PATH)
565       props = cm.GetProperties(dbus_interface = CM_IFACE)
566       state = props['State']
567       netupdown(state == 'online', ['connman', 'initially-%s' % state])
568     except D.DBusException, e:
569       if T._debug: print '# exception attaching to connman: %s' % e
570     bus.add_signal_receiver(me._cm_state, 'PropertyChanged',
571                             CM_IFACE, CM_NAME, CM_PATH)
572
573   def _cm_state(me, prop, value):
574     if prop != 'State': return
575     delay_netupdown(value == 'online', ['connman', value])
576
577 ###--------------------------------------------------------------------------
578 ### Maemo monitor.
579
580 ICD_NAME = 'com.nokia.icd'
581 ICD_PATH = '/com/nokia/icd'
582 ICD_IFACE = ICD_NAME
583
584 class MaemoICdMonitor (object):
585   """
586   Watch ICd signals for changes in network state.
587   """
588
589   ## Strategy.  ICd only handles one connection at a time in steady state,
590   ## though when switching between connections, it tries to bring the new one
591   ## up before shutting down the old one.  This makes life a bit easier than
592   ## it is with NetworkManager.  On the other hand, the notifications are
593   ## relative to particular connections only, and the indicator that the old
594   ## connection is down (`IDLE') comes /after/ the new one comes up
595   ## (`CONNECTED'), so we have to remember which one is active.
596
597   def attach(me, bus):
598     try:
599       icd = bus.get_object(ICD_NAME, ICD_PATH)
600       try:
601         iap = icd.get_ipinfo(dbus_interface = ICD_IFACE)[0]
602         me._iap = iap
603         netupdown(True, ['icd', 'initially-connected', iap])
604       except D.DBusException:
605         me._iap = None
606         netupdown(False, ['icd', 'initially-disconnected'])
607     except D.DBusException, e:
608       if T._debug: print '# exception attaching to icd: %s' % e
609       me._iap = None
610     bus.add_signal_receiver(me._icd_state, 'status_changed', ICD_IFACE,
611                             ICD_NAME, ICD_PATH)
612
613   def _icd_state(me, iap, ty, state, hunoz):
614     if state == 'CONNECTED':
615       me._iap = iap
616       delay_netupdown(True, ['icd', 'connected', iap])
617     elif state == 'IDLE' and iap == me._iap:
618       me._iap = None
619       delay_netupdown(False, ['icd', 'idle'])
620
621 ###--------------------------------------------------------------------------
622 ### D-Bus connection tracking.
623
624 class DBusMonitor (object):
625   """
626   Maintains a connection to the system D-Bus, and watches for signals.
627
628   If the connection is initially down, or drops for some reason, we retry
629   periodically (every five seconds at the moment).  If the connection
630   resurfaces, we reattach the monitors.
631   """
632
633   def __init__(me):
634     """
635     Initialise the object and try to establish a connection to the bus.
636     """
637     me._mons = []
638     me._loop = D.mainloop.glib.DBusGMainLoop()
639     me._state = 'startup'
640     me._reconnect()
641
642   def addmon(me, mon):
643     """
644     Add a monitor object to watch for signals.
645
646     MON.attach(BUS) is called, with BUS being the connection to the system
647     bus.  MON should query its service's current status and watch for
648     relevant signals.
649     """
650     me._mons.append(mon)
651     if me._bus is not None:
652       mon.attach(me._bus)
653
654   def _reconnect(me, hunoz = None):
655     """
656     Start connecting to the bus.
657
658     If we fail the first time, retry periodically.
659     """
660     if me._state == 'startup':
661       T.aside(SM.notify, 'conntrack', 'dbus-connection', 'startup')
662     elif me._state == 'connected':
663       T.aside(SM.notify, 'conntrack', 'dbus-connection', 'lost')
664     else:
665       T.aside(SM.notify, 'conntrack', 'dbus-connection',
666               'state=%s' % me._state)
667     me._state == 'reconnecting'
668     me._bus = None
669     if me._try_connect():
670       G.timeout_add_seconds(5, me._try_connect)
671
672   def _try_connect(me):
673     """
674     Actually make a connection attempt.
675
676     If we succeed, attach the monitors.
677     """
678     try:
679       addr = OS.getenv('TRIPE_CONNTRACK_BUS')
680       if addr == 'SESSION':
681         bus = D.SessionBus(mainloop = me._loop, private = True)
682       elif addr is not None:
683         bus = D.bus.BusConnection(addr, mainloop = me._loop)
684       else:
685         bus = D.SystemBus(mainloop = me._loop, private = True)
686       for m in me._mons:
687         m.attach(bus)
688     except D.DBusException, e:
689       return True
690     me._bus = bus
691     me._state = 'connected'
692     bus.call_on_disconnection(me._reconnect)
693     T.aside(SM.notify, 'conntrack', 'dbus-connection', 'connected')
694     return False
695
696 ###--------------------------------------------------------------------------
697 ### TrIPE service.
698
699 class GIOWatcher (object):
700   """
701   Monitor I/O events using glib.
702   """
703   def __init__(me, conn, mc = G.main_context_default()):
704     me._conn = conn
705     me._watch = None
706     me._mc = mc
707   def connected(me, sock):
708     me._watch = G.io_add_watch(sock, G.IO_IN,
709                                lambda *hunoz: me._conn.receive())
710   def disconnected(me):
711     G.source_remove(me._watch)
712     me._watch = None
713   def iterate(me):
714     me._mc.iteration(True)
715
716 SM.iowatch = GIOWatcher(SM)
717
718 def init():
719   """
720   Service initialization.
721
722   Add the D-Bus monitor here, because we might send commands off immediately,
723   and we want to make sure the server connection is up.
724   """
725   global DBM
726   T.Coroutine(kickpeers, name = 'kickpeers').switch()
727   DBM = DBusMonitor()
728   DBM.addmon(NetworkManagerMonitor())
729   DBM.addmon(ConnManMonitor())
730   DBM.addmon(MaemoICdMonitor())
731   G.timeout_add_seconds(30, lambda: (_delay is not None or
732                                      netupdown(True, ['interval-timer']) or
733                                      True))
734
735 def parse_options():
736   """
737   Parse the command-line options.
738
739   Automatically changes directory to the requested configdir, and turns on
740   debugging.  Returns the options object.
741   """
742   op = OptionParser(usage = '%prog [-a FILE] [-d DIR]',
743                     version = '%%prog %s' % VERSION)
744
745   op.add_option('-a', '--admin-socket',
746                 metavar = 'FILE', dest = 'tripesock', default = T.tripesock,
747                 help = 'Select socket to connect to [default %default]')
748   op.add_option('-d', '--directory',
749                 metavar = 'DIR', dest = 'dir', default = T.configdir,
750                 help = 'Select current diretory [default %default]')
751   op.add_option('-c', '--config',
752                 metavar = 'FILE', dest = 'conf', default = 'conntrack.conf',
753                 help = 'Select configuration [default %default]')
754   op.add_option('--daemon', dest = 'daemon',
755                 default = False, action = 'store_true',
756                 help = 'Become a daemon after successful initialization')
757   op.add_option('--debug', dest = 'debug',
758                 default = False, action = 'store_true',
759                 help = 'Emit debugging trace information')
760   op.add_option('--startup', dest = 'startup',
761                 default = False, action = 'store_true',
762                 help = 'Being called as part of the server startup')
763
764   opts, args = op.parse_args()
765   if args: op.error('no arguments permitted')
766   OS.chdir(opts.dir)
767   T._debug = opts.debug
768   return opts
769
770 ## Service table, for running manually.
771 def cmd_updown(upness):
772   return lambda *args: T.defer(netupdown, upness, ['manual'] + list(args))
773 service_info = [('conntrack', VERSION, {
774   'up': (0, None, '', cmd_updown(True)),
775   'down': (0, None, '', cmd_updown(False)),
776   'show-config': (0, 0, '', cmd_showconfig),
777   'show-groups': (0, 0, '', cmd_showgroups),
778   'show-group': (1, 1, 'GROUP', cmd_showgroup)
779 })]
780
781 if __name__ == '__main__':
782   opts = parse_options()
783   CF = Config(opts.conf)
784   T.runservices(opts.tripesock, service_info,
785                 init = init, daemon = opts.daemon)
786
787 ###----- That's all, folks --------------------------------------------------