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